pytest-dsl 0.14.0__py3-none-any.whl → 0.15.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,402 @@
1
+ """
2
+ pytest-dsl关键字加载器
3
+
4
+ 提供统一的关键字加载和管理功能,包括:
5
+ - 内置关键字加载
6
+ - 插件关键字发现和加载
7
+ - 本地关键字扫描
8
+ - 项目自定义关键字扫描
9
+ - 远程关键字支持
10
+ - 关键字分类和信息获取
11
+ """
12
+
13
+ import os
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import Dict, Any, Optional, List
17
+
18
+ from pytest_dsl.core.plugin_discovery import load_all_plugins, scan_local_keywords
19
+ from pytest_dsl.core.keyword_manager import keyword_manager
20
+ from pytest_dsl.core.lexer import get_lexer
21
+ from pytest_dsl.core.parser import get_parser
22
+
23
+
24
+ class KeywordLoader:
25
+ """关键字加载器类"""
26
+
27
+ def __init__(self):
28
+ self._project_custom_keywords = None
29
+
30
+ def load_all_keywords(self, include_remote: bool = False) -> Dict[str, Any]:
31
+ """加载所有可用的关键字
32
+
33
+ Args:
34
+ include_remote: 是否包含远程关键字,默认为False
35
+
36
+ Returns:
37
+ 项目自定义关键字信息字典
38
+ """
39
+ # 首先导入内置关键字模块,确保内置关键字被注册
40
+ try:
41
+ import pytest_dsl.keywords # noqa: F401
42
+ print("内置关键字模块加载完成")
43
+ except ImportError as e:
44
+ print(f"加载内置关键字模块失败: {e}")
45
+
46
+ # 加载已安装的关键字插件
47
+ load_all_plugins()
48
+
49
+ # 扫描本地关键字
50
+ scan_local_keywords()
51
+
52
+ # 自动导入项目中的resources目录
53
+ self._load_resources_directory()
54
+
55
+ # 扫描项目中的自定义关键字(.resource文件中定义的)
56
+ project_custom_keywords = self.scan_project_custom_keywords()
57
+ if project_custom_keywords:
58
+ print(f"发现 {len(project_custom_keywords)} 个项目自定义关键字")
59
+ self._load_resource_files(project_custom_keywords)
60
+
61
+ # 根据参数决定是否加载远程关键字
62
+ if include_remote:
63
+ print("正在扫描远程关键字...")
64
+ # 这里可以添加远程关键字的扫描逻辑
65
+ # 目前远程关键字是通过DSL文件中的@remote导入指令动态加载的
66
+ else:
67
+ print("跳过远程关键字扫描")
68
+
69
+ self._project_custom_keywords = project_custom_keywords
70
+ return project_custom_keywords
71
+
72
+ def _load_resources_directory(self):
73
+ """自动导入项目中的resources目录"""
74
+ try:
75
+ from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
76
+
77
+ # 尝试从多个可能的项目根目录位置导入resources
78
+ possible_roots = [
79
+ os.getcwd(), # 当前工作目录
80
+ os.path.dirname(os.getcwd()), # 上级目录
81
+ ]
82
+
83
+ # 尝试每个可能的根目录
84
+ for project_root in possible_roots:
85
+ if project_root and os.path.exists(project_root):
86
+ resources_dir = os.path.join(project_root, "resources")
87
+ if (os.path.exists(resources_dir) and
88
+ os.path.isdir(resources_dir)):
89
+ custom_keyword_manager.auto_import_resources_directory(
90
+ project_root)
91
+ break
92
+ except Exception as e:
93
+ print(f"自动导入resources目录时出现警告: {str(e)}")
94
+
95
+ def _load_resource_files(self, project_custom_keywords: Dict[str, Any]):
96
+ """加载.resource文件中的关键字到关键字管理器"""
97
+ from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
98
+
99
+ project_root = Path(os.getcwd())
100
+ resource_files = list(project_root.glob('**/*.resource'))
101
+
102
+ for resource_file in resource_files:
103
+ try:
104
+ custom_keyword_manager.load_resource_file(str(resource_file))
105
+ print(f"已加载资源文件: {resource_file}")
106
+ except Exception as e:
107
+ print(f"加载资源文件失败 {resource_file}: {e}")
108
+
109
+ def scan_project_custom_keywords(self, project_root: Optional[str] = None) -> Dict[str, Any]:
110
+ """扫描项目中.resource文件中的自定义关键字
111
+
112
+ Args:
113
+ project_root: 项目根目录,默认为当前工作目录
114
+
115
+ Returns:
116
+ 自定义关键字信息,格式为
117
+ {keyword_name: {'file': file_path, 'node': ast_node, 'parameters': [...]}}
118
+ """
119
+ if project_root is None:
120
+ project_root = os.getcwd()
121
+
122
+ project_root = Path(project_root)
123
+ custom_keywords = {}
124
+
125
+ # 查找所有.resource文件
126
+ resource_files = list(project_root.glob('**/*.resource'))
127
+
128
+ if not resource_files:
129
+ return custom_keywords
130
+
131
+ lexer = get_lexer()
132
+ parser = get_parser()
133
+
134
+ for file_path in resource_files:
135
+ try:
136
+ # 读取并解析文件
137
+ content = self._read_file(str(file_path))
138
+ ast = parser.parse(content, lexer=lexer)
139
+
140
+ # 查找自定义关键字定义
141
+ keywords_in_file = self._extract_custom_keywords_from_ast(
142
+ ast, str(file_path)
143
+ )
144
+ custom_keywords.update(keywords_in_file)
145
+
146
+ except Exception as e:
147
+ print(f"解析资源文件 {file_path} 时出错: {e}")
148
+
149
+ return custom_keywords
150
+
151
+ def _read_file(self, filename: str) -> str:
152
+ """读取文件内容"""
153
+ with open(filename, 'r', encoding='utf-8') as f:
154
+ return f.read()
155
+
156
+ def _extract_custom_keywords_from_ast(self, ast, file_path: str) -> Dict[str, Any]:
157
+ """从AST中提取自定义关键字定义
158
+
159
+ Args:
160
+ ast: 抽象语法树
161
+ file_path: 文件路径
162
+
163
+ Returns:
164
+ 自定义关键字信息字典
165
+ """
166
+ custom_keywords = {}
167
+
168
+ if ast.type != 'Start' or len(ast.children) < 2:
169
+ return custom_keywords
170
+
171
+ # 遍历语句节点
172
+ statements_node = ast.children[1]
173
+ if statements_node.type != 'Statements':
174
+ return custom_keywords
175
+
176
+ for node in statements_node.children:
177
+ # 支持两种格式:CustomKeyword(旧格式)和Function(新格式)
178
+ if node.type in ['CustomKeyword', 'Function']:
179
+ keyword_name = node.value
180
+
181
+ # 提取参数信息
182
+ params_node = node.children[0] if node.children else None
183
+ parameters = []
184
+
185
+ if params_node:
186
+ for param in params_node:
187
+ param_name = param.value
188
+ param_default = None
189
+
190
+ # 检查是否有默认值
191
+ if param.children and param.children[0]:
192
+ param_default = param.children[0].value
193
+
194
+ param_info = {
195
+ 'name': param_name,
196
+ 'mapping': param_name,
197
+ 'description': f'自定义关键字参数 {param_name}'
198
+ }
199
+
200
+ if param_default is not None:
201
+ param_info['default'] = param_default
202
+
203
+ parameters.append(param_info)
204
+
205
+ custom_keywords[keyword_name] = {
206
+ 'file': file_path,
207
+ 'node': node,
208
+ 'type': 'project_custom',
209
+ 'parameters': parameters
210
+ }
211
+
212
+ return custom_keywords
213
+
214
+ def categorize_keyword(self, keyword_name: str, keyword_info: Dict[str, Any],
215
+ project_custom_keywords: Optional[Dict[str, Any]] = None) -> str:
216
+ """判断关键字的类别
217
+
218
+ Args:
219
+ keyword_name: 关键字名称
220
+ keyword_info: 关键字信息
221
+ project_custom_keywords: 项目自定义关键字信息
222
+
223
+ Returns:
224
+ 关键字类别:'builtin', 'plugin', 'custom', 'project_custom', 'remote'
225
+ """
226
+ # 优先使用存储的来源信息
227
+ source_type = keyword_info.get('source_type')
228
+ if source_type:
229
+ if source_type == 'builtin':
230
+ return 'builtin'
231
+ elif source_type == 'plugin':
232
+ return 'plugin'
233
+ elif source_type in ['external', 'local']:
234
+ return 'custom'
235
+ elif source_type == 'project_custom':
236
+ return 'project_custom'
237
+
238
+ # 向后兼容:使用原有的判断逻辑
239
+ if keyword_info.get('remote', False):
240
+ return 'remote'
241
+
242
+ # 检查是否是项目自定义关键字(DSL文件中定义的)
243
+ if project_custom_keywords and keyword_name in project_custom_keywords:
244
+ return 'project_custom'
245
+
246
+ # 检查是否是内置关键字(通过检查函数所在模块)
247
+ func = keyword_info.get('func')
248
+ if func and hasattr(func, '__module__'):
249
+ module_name = func.__module__
250
+ if module_name and module_name.startswith('pytest_dsl.keywords'):
251
+ return 'builtin'
252
+
253
+ return 'custom'
254
+
255
+ def get_keyword_source_info(self, keyword_info: Dict[str, Any]) -> Dict[str, Any]:
256
+ """获取关键字的详细来源信息
257
+
258
+ Args:
259
+ keyword_info: 关键字信息
260
+
261
+ Returns:
262
+ 来源信息字典
263
+ """
264
+ source_type = keyword_info.get('source_type', 'unknown')
265
+ source_name = keyword_info.get('source_name', '未知')
266
+
267
+ return {
268
+ 'type': source_type,
269
+ 'name': source_name,
270
+ 'display_name': source_name,
271
+ 'module': keyword_info.get('module_name', ''),
272
+ 'plugin_module': keyword_info.get('plugin_module', '')
273
+ }
274
+
275
+ def group_keywords_by_source(self, keywords_dict: Dict[str, Any],
276
+ project_custom_keywords: Optional[Dict[str, Any]] = None) -> Dict[str, Dict[str, List]]:
277
+ """按来源分组关键字
278
+
279
+ Args:
280
+ keywords_dict: 关键字字典
281
+ project_custom_keywords: 项目自定义关键字信息
282
+
283
+ Returns:
284
+ 分组后的关键字字典,格式为 {source_group: {source_name: [keywords]}}
285
+ """
286
+ groups = {
287
+ 'builtin': {},
288
+ 'plugin': {},
289
+ 'custom': {},
290
+ 'project_custom': {},
291
+ 'remote': {}
292
+ }
293
+
294
+ for keyword_name, keyword_info in keywords_dict.items():
295
+ category = self.categorize_keyword(
296
+ keyword_name, keyword_info, project_custom_keywords
297
+ )
298
+ source_info = self.get_keyword_source_info(keyword_info)
299
+
300
+ # 特殊处理项目自定义关键字
301
+ if category == 'project_custom' and project_custom_keywords:
302
+ custom_info = project_custom_keywords[keyword_name]
303
+ source_name = custom_info['file']
304
+ else:
305
+ source_name = source_info['name']
306
+
307
+ if source_name not in groups[category]:
308
+ groups[category][source_name] = []
309
+
310
+ groups[category][source_name].append({
311
+ 'name': keyword_name,
312
+ 'info': keyword_info,
313
+ 'source_info': source_info
314
+ })
315
+
316
+ return groups
317
+
318
+ def get_all_keywords(self) -> Dict[str, Any]:
319
+ """获取所有已注册的关键字
320
+
321
+ Returns:
322
+ 所有关键字的字典
323
+ """
324
+ return keyword_manager._keywords
325
+
326
+ def get_project_custom_keywords(self) -> Optional[Dict[str, Any]]:
327
+ """获取项目自定义关键字信息
328
+
329
+ Returns:
330
+ 项目自定义关键字信息,如果尚未加载则返回None
331
+ """
332
+ return self._project_custom_keywords
333
+
334
+
335
+ # 创建全局实例
336
+ keyword_loader = KeywordLoader()
337
+
338
+
339
+ # 便捷函数
340
+ def load_all_keywords(include_remote: bool = False) -> Dict[str, Any]:
341
+ """加载所有可用的关键字
342
+
343
+ Args:
344
+ include_remote: 是否包含远程关键字,默认为False
345
+
346
+ Returns:
347
+ 项目自定义关键字信息字典
348
+ """
349
+ return keyword_loader.load_all_keywords(include_remote=include_remote)
350
+
351
+
352
+ def categorize_keyword(keyword_name: str, keyword_info: Dict[str, Any],
353
+ project_custom_keywords: Optional[Dict[str, Any]] = None) -> str:
354
+ """判断关键字的类别
355
+
356
+ Args:
357
+ keyword_name: 关键字名称
358
+ keyword_info: 关键字信息
359
+ project_custom_keywords: 项目自定义关键字信息
360
+
361
+ Returns:
362
+ 关键字类别
363
+ """
364
+ return keyword_loader.categorize_keyword(keyword_name, keyword_info, project_custom_keywords)
365
+
366
+
367
+ def get_keyword_source_info(keyword_info: Dict[str, Any]) -> Dict[str, Any]:
368
+ """获取关键字的详细来源信息
369
+
370
+ Args:
371
+ keyword_info: 关键字信息
372
+
373
+ Returns:
374
+ 来源信息字典
375
+ """
376
+ return keyword_loader.get_keyword_source_info(keyword_info)
377
+
378
+
379
+ def group_keywords_by_source(keywords_dict: Dict[str, Any],
380
+ project_custom_keywords: Optional[Dict[str, Any]] = None) -> Dict[str, Dict[str, List]]:
381
+ """按来源分组关键字
382
+
383
+ Args:
384
+ keywords_dict: 关键字字典
385
+ project_custom_keywords: 项目自定义关键字信息
386
+
387
+ Returns:
388
+ 分组后的关键字字典
389
+ """
390
+ return keyword_loader.group_keywords_by_source(keywords_dict, project_custom_keywords)
391
+
392
+
393
+ def scan_project_custom_keywords(project_root: Optional[str] = None) -> Dict[str, Any]:
394
+ """扫描项目中.resource文件中的自定义关键字
395
+
396
+ Args:
397
+ project_root: 项目根目录,默认为当前工作目录
398
+
399
+ Returns:
400
+ 自定义关键字信息字典
401
+ """
402
+ return keyword_loader.scan_project_custom_keywords(project_root)
@@ -18,7 +18,7 @@ class KeywordManager:
18
18
 
19
19
  def register(self, name: str, parameters: List[Dict], source_info: Optional[Dict] = None):
20
20
  """关键字注册装饰器
21
-
21
+
22
22
  Args:
23
23
  name: 关键字名称
24
24
  parameters: 参数列表
@@ -29,23 +29,29 @@ class KeywordManager:
29
29
  def wrapper(**kwargs):
30
30
  # 获取自定义步骤名称,如果未指定则使用关键字名称
31
31
  step_name = kwargs.pop('step_name', name)
32
-
32
+
33
+ # 检查是否已经在DSL执行器的步骤中,避免重复记录
34
+ skip_logging = kwargs.pop('skip_logging', False)
35
+
33
36
  with allure.step(f"{step_name}"):
34
37
  try:
35
38
  result = func(**kwargs)
36
- self._log_execution(step_name, kwargs, result)
39
+ if not skip_logging:
40
+ self._log_execution(step_name, kwargs, result)
37
41
  return result
38
42
  except Exception as e:
39
- self._log_failure(step_name, kwargs, e)
43
+ if not skip_logging:
44
+ self._log_failure(step_name, kwargs, e)
40
45
  raise
41
46
 
42
47
  param_list = [Parameter(**p) for p in parameters]
43
48
  mapping = {p.name: p.mapping for p in param_list}
44
- defaults = {p.mapping: p.default for p in param_list if p.default is not None}
45
-
49
+ defaults = {
50
+ p.mapping: p.default for p in param_list if p.default is not None}
51
+
46
52
  # 自动添加 step_name 到 mapping 中
47
53
  mapping["步骤名称"] = "step_name"
48
-
54
+
49
55
  # 构建关键字信息,包含来源信息
50
56
  keyword_info = {
51
57
  'func': wrapper,
@@ -53,14 +59,14 @@ class KeywordManager:
53
59
  'parameters': param_list,
54
60
  'defaults': defaults # 存储默认值
55
61
  }
56
-
62
+
57
63
  # 添加来源信息
58
64
  if source_info:
59
65
  keyword_info.update(source_info)
60
66
  else:
61
67
  # 尝试从函数模块推断来源信息
62
68
  keyword_info.update(self._infer_source_info(func))
63
-
69
+
64
70
  self._keywords[name] = keyword_info
65
71
  return wrapper
66
72
  return decorator
@@ -68,11 +74,11 @@ class KeywordManager:
68
74
  def _infer_source_info(self, func: Callable) -> Dict:
69
75
  """从函数推断来源信息"""
70
76
  source_info = {}
71
-
77
+
72
78
  if hasattr(func, '__module__'):
73
79
  module_name = func.__module__
74
80
  source_info['module_name'] = module_name
75
-
81
+
76
82
  if module_name.startswith('pytest_dsl.keywords'):
77
83
  # 内置关键字
78
84
  source_info['source_type'] = 'builtin'
@@ -90,13 +96,13 @@ class KeywordManager:
90
96
  source_info['source_name'] = parts[0]
91
97
  else:
92
98
  source_info['source_name'] = module_name
93
-
99
+
94
100
  return source_info
95
101
 
96
- def register_with_source(self, name: str, parameters: List[Dict],
97
- source_type: str, source_name: str, **kwargs):
102
+ def register_with_source(self, name: str, parameters: List[Dict],
103
+ source_type: str, source_name: str, **kwargs):
98
104
  """带来源信息的关键字注册装饰器
99
-
105
+
100
106
  Args:
101
107
  name: 关键字名称
102
108
  parameters: 参数列表
@@ -116,18 +122,18 @@ class KeywordManager:
116
122
  keyword_info = self._keywords.get(keyword_name)
117
123
  if not keyword_info:
118
124
  raise KeyError(f"未注册的关键字: {keyword_name}")
119
-
125
+
120
126
  # 应用默认值
121
127
  final_params = {}
122
128
  defaults = keyword_info.get('defaults', {})
123
-
129
+
124
130
  # 首先设置所有默认值
125
131
  for param_key, default_value in defaults.items():
126
132
  final_params[param_key] = default_value
127
-
133
+
128
134
  # 然后用传入的参数覆盖默认值
129
135
  final_params.update(params)
130
-
136
+
131
137
  return keyword_info['func'](**final_params)
132
138
 
133
139
  def get_keyword_info(self, keyword_name: str) -> Dict:
@@ -135,7 +141,7 @@ class KeywordManager:
135
141
  keyword_info = self._keywords.get(keyword_name)
136
142
  if not keyword_info:
137
143
  return None
138
-
144
+
139
145
  # 动态添加step_name参数到参数列表中
140
146
  if not any(p.name == "步骤名称" for p in keyword_info['parameters']):
141
147
  keyword_info['parameters'].append(Parameter(
@@ -143,19 +149,19 @@ class KeywordManager:
143
149
  mapping="step_name",
144
150
  description="自定义的步骤名称,用于在报告中显示"
145
151
  ))
146
-
152
+
147
153
  return keyword_info
148
154
 
149
155
  def get_keywords_by_source(self) -> Dict[str, List[str]]:
150
156
  """按来源分组获取关键字"""
151
157
  by_source = {}
152
-
158
+
153
159
  for name, info in self._keywords.items():
154
160
  source_name = info.get('source_name', '未知来源')
155
161
  if source_name not in by_source:
156
162
  by_source[source_name] = []
157
163
  by_source[source_name].append(name)
158
-
164
+
159
165
  return by_source
160
166
 
161
167
  def _log_execution(self, keyword_name: str, params: Dict, result: Any) -> None: