pytest-dsl 0.12.1__py3-none-any.whl → 0.14.0__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.
pytest_dsl/cli.py CHANGED
@@ -78,7 +78,7 @@ def parse_args():
78
78
  list_parser.add_argument(
79
79
  '--category',
80
80
  choices=[
81
- 'builtin', 'plugin', 'custom',
81
+ 'builtin', 'plugin', 'custom',
82
82
  'project_custom', 'remote', 'all'
83
83
  ],
84
84
  default='all',
@@ -108,7 +108,7 @@ def parse_args():
108
108
  parser.add_argument(
109
109
  '--category',
110
110
  choices=[
111
- 'builtin', 'plugin', 'custom',
111
+ 'builtin', 'plugin', 'custom',
112
112
  'project_custom', 'remote', 'all'
113
113
  ],
114
114
  default='all'
@@ -157,6 +157,29 @@ def load_all_keywords(include_remote=False):
157
157
  # 扫描本地关键字
158
158
  scan_local_keywords()
159
159
 
160
+ # 自动导入项目中的resources目录
161
+ try:
162
+ from pytest_dsl.core.custom_keyword_manager import (
163
+ custom_keyword_manager)
164
+
165
+ # 尝试从多个可能的项目根目录位置导入resources
166
+ possible_roots = [
167
+ os.getcwd(), # 当前工作目录
168
+ os.path.dirname(os.getcwd()), # 上级目录
169
+ ]
170
+
171
+ # 尝试每个可能的根目录
172
+ for project_root in possible_roots:
173
+ if project_root and os.path.exists(project_root):
174
+ resources_dir = os.path.join(project_root, "resources")
175
+ if (os.path.exists(resources_dir) and
176
+ os.path.isdir(resources_dir)):
177
+ custom_keyword_manager.auto_import_resources_directory(
178
+ project_root)
179
+ break
180
+ except Exception as e:
181
+ print(f"自动导入resources目录时出现警告: {str(e)}")
182
+
160
183
  # 扫描项目中的自定义关键字(.resource文件中定义的)
161
184
  project_custom_keywords = scan_project_custom_keywords()
162
185
  if project_custom_keywords:
@@ -701,12 +724,17 @@ def list_keywords(output_format='json', name_filter=None,
701
724
 
702
725
  def load_yaml_variables(args):
703
726
  """从命令行参数加载YAML变量"""
704
- # 使用统一的加载函数,包含远程服务器自动连接功能
727
+ # 使用统一的加载函数,包含远程服务器自动连接功能和hook支持
705
728
  try:
729
+ # 尝试从环境变量获取环境名称
730
+ environment = (os.environ.get('PYTEST_DSL_ENVIRONMENT') or
731
+ os.environ.get('ENVIRONMENT'))
732
+
706
733
  load_yaml_variables_from_args(
707
734
  yaml_files=args.yaml_vars,
708
735
  yaml_vars_dir=args.yaml_vars_dir,
709
- project_root=os.getcwd() # CLI模式下使用当前工作目录作为项目根目录
736
+ project_root=os.getcwd(), # CLI模式下使用当前工作目录作为项目根目录
737
+ environment=environment
710
738
  )
711
739
  except Exception as e:
712
740
  print(f"加载YAML变量失败: {str(e)}")
@@ -751,6 +779,33 @@ def run_dsl_tests(args):
751
779
  # 加载YAML变量(包括远程服务器自动连接)
752
780
  load_yaml_variables(args)
753
781
 
782
+ # 支持hook机制的执行
783
+ from pytest_dsl.core.hookable_executor import hookable_executor
784
+
785
+ # 检查是否有hook提供的用例列表
786
+ hook_cases = hookable_executor.list_dsl_cases()
787
+ if hook_cases:
788
+ # 如果有hook提供的用例,优先执行这些用例
789
+ print(f"通过Hook发现 {len(hook_cases)} 个DSL用例")
790
+ failures = 0
791
+ for case in hook_cases:
792
+ case_id = case.get('id') or case.get('name', 'unknown')
793
+ try:
794
+ print(f"执行用例: {case.get('name', case_id)}")
795
+ hookable_executor.execute_dsl(str(case_id))
796
+ print(f"✓ 用例 {case.get('name', case_id)} 执行成功")
797
+ except Exception as e:
798
+ print(f"✗ 用例 {case.get('name', case_id)} 执行失败: {e}")
799
+ failures += 1
800
+
801
+ if failures > 0:
802
+ print(f"总计 {failures}/{len(hook_cases)} 个测试失败")
803
+ sys.exit(1)
804
+ else:
805
+ print(f"所有 {len(hook_cases)} 个测试成功完成")
806
+ return
807
+
808
+ # 如果没有hook用例,使用传统的文件执行方式
754
809
  lexer = get_lexer()
755
810
  parser = get_parser()
756
811
  executor = DSLExecutor()
@@ -15,6 +15,7 @@ class CustomKeywordManager:
15
15
  """初始化自定义关键字管理器"""
16
16
  self.resource_cache = {} # 缓存已加载的资源文件
17
17
  self.resource_paths = [] # 资源文件搜索路径
18
+ self.auto_imported_resources = set() # 记录已自动导入的资源文件
18
19
 
19
20
  def add_resource_path(self, path: str) -> None:
20
21
  """添加资源文件搜索路径
@@ -25,6 +26,141 @@ class CustomKeywordManager:
25
26
  if path not in self.resource_paths:
26
27
  self.resource_paths.append(path)
27
28
 
29
+ def auto_import_resources_directory(
30
+ self, project_root: str = None) -> None:
31
+ """自动导入项目中的resources目录
32
+
33
+ Args:
34
+ project_root: 项目根目录,默认为当前工作目录
35
+ """
36
+ if project_root is None:
37
+ project_root = os.getcwd()
38
+
39
+ # 查找resources目录
40
+ resources_dir = os.path.join(project_root, "resources")
41
+
42
+ if (not os.path.exists(resources_dir) or
43
+ not os.path.isdir(resources_dir)):
44
+ # 如果没有resources目录,静默返回
45
+ return
46
+
47
+ print(f"发现resources目录: {resources_dir}")
48
+
49
+ # 递归查找所有.resource文件
50
+ resource_files = []
51
+ for root, dirs, files in os.walk(resources_dir):
52
+ for file in files:
53
+ if file.endswith('.resource'):
54
+ resource_files.append(os.path.join(root, file))
55
+
56
+ if not resource_files:
57
+ print("resources目录中没有找到.resource文件")
58
+ return
59
+
60
+ print(f"在resources目录中发现 {len(resource_files)} 个资源文件")
61
+
62
+ # 按照依赖关系排序并加载资源文件
63
+ sorted_files = self._sort_resources_by_dependencies(resource_files)
64
+
65
+ for resource_file in sorted_files:
66
+ try:
67
+ # 检查是否已经自动导入过
68
+ absolute_path = os.path.abspath(resource_file)
69
+ if absolute_path not in self.auto_imported_resources:
70
+ self.load_resource_file(resource_file)
71
+ self.auto_imported_resources.add(absolute_path)
72
+ print(f"自动导入资源文件: {resource_file}")
73
+ except Exception as e:
74
+ print(f"自动导入资源文件失败 {resource_file}: {e}")
75
+
76
+ def _sort_resources_by_dependencies(self, resource_files):
77
+ """根据依赖关系对资源文件进行排序
78
+
79
+ Args:
80
+ resource_files: 资源文件列表
81
+
82
+ Returns:
83
+ list: 按依赖关系排序后的资源文件列表
84
+ """
85
+ # 简单的拓扑排序实现
86
+ dependencies = {}
87
+ all_files = set()
88
+
89
+ # 分析每个文件的依赖关系
90
+ for file_path in resource_files:
91
+ all_files.add(file_path)
92
+ dependencies[file_path] = self._extract_dependencies(file_path)
93
+
94
+ # 拓扑排序
95
+ sorted_files = []
96
+ visited = set()
97
+ temp_visited = set()
98
+
99
+ def visit(file_path):
100
+ if file_path in temp_visited:
101
+ # 检测到循环依赖,跳过
102
+ return
103
+ if file_path in visited:
104
+ return
105
+
106
+ temp_visited.add(file_path)
107
+
108
+ # 访问依赖的文件
109
+ for dep in dependencies.get(file_path, []):
110
+ if dep in all_files: # 只处理在当前文件列表中的依赖
111
+ visit(dep)
112
+
113
+ temp_visited.remove(file_path)
114
+ visited.add(file_path)
115
+ sorted_files.append(file_path)
116
+
117
+ # 访问所有文件
118
+ for file_path in resource_files:
119
+ if file_path not in visited:
120
+ visit(file_path)
121
+
122
+ return sorted_files
123
+
124
+ def _extract_dependencies(self, file_path):
125
+ """提取资源文件的依赖关系
126
+
127
+ Args:
128
+ file_path: 资源文件路径
129
+
130
+ Returns:
131
+ list: 依赖的文件路径列表
132
+ """
133
+ dependencies = []
134
+
135
+ try:
136
+ with open(file_path, 'r', encoding='utf-8') as f:
137
+ content = f.read()
138
+
139
+ # 解析文件获取导入信息
140
+ lexer = get_lexer()
141
+ parser = get_parser()
142
+ ast = parser.parse(content, lexer=lexer)
143
+
144
+ if ast.type == 'Start' and ast.children:
145
+ metadata_node = ast.children[0]
146
+ if metadata_node.type == 'Metadata':
147
+ for item in metadata_node.children:
148
+ if item.type == '@import':
149
+ imported_file = item.value
150
+ # 处理相对路径
151
+ if not os.path.isabs(imported_file):
152
+ imported_file = os.path.join(
153
+ os.path.dirname(file_path), imported_file)
154
+ # 规范化路径
155
+ imported_file = os.path.normpath(imported_file)
156
+ dependencies.append(imported_file)
157
+
158
+ except Exception as e:
159
+ # 如果解析失败,返回空依赖列表
160
+ print(f"解析资源文件依赖失败 {file_path}: {e}")
161
+
162
+ return dependencies
163
+
28
164
  def load_resource_file(self, file_path: str) -> None:
29
165
  """加载资源文件
30
166
 
@@ -64,23 +200,37 @@ class CustomKeywordManager:
64
200
  with open(file_path, 'r', encoding='utf-8') as f:
65
201
  content = f.read()
66
202
 
67
- # 解析资源文件
68
- lexer = get_lexer()
69
- parser = get_parser()
70
- ast = parser.parse(content, lexer=lexer)
71
-
72
- # 标记为已加载
203
+ # 标记为已加载(在解析前标记,避免循环导入)
73
204
  self.resource_cache[absolute_path] = True
74
205
 
75
- # 处理导入指令
76
- self._process_imports(ast, os.path.dirname(file_path))
206
+ # 使用公共方法解析和处理资源文件内容
207
+ self._process_resource_file_content(content, file_path)
77
208
 
78
- # 注册关键字
79
- self._register_keywords(ast, file_path)
80
209
  except Exception as e:
210
+ # 如果处理失败,移除缓存标记
211
+ self.resource_cache.pop(absolute_path, None)
81
212
  print(f"资源文件 {file_path} 加载失败: {str(e)}")
82
213
  raise
83
214
 
215
+ def _process_resource_file_content(self, content: str,
216
+ file_path: str) -> None:
217
+ """处理资源文件内容
218
+
219
+ Args:
220
+ content: 文件内容
221
+ file_path: 文件路径
222
+ """
223
+ # 解析资源文件
224
+ lexer = get_lexer()
225
+ parser = get_parser()
226
+ ast = parser.parse(content, lexer=lexer)
227
+
228
+ # 处理导入指令
229
+ self._process_imports(ast, os.path.dirname(file_path))
230
+
231
+ # 注册关键字
232
+ self._register_keywords_from_ast(ast, file_path)
233
+
84
234
  def _process_imports(self, ast: Node, base_dir: str) -> None:
85
235
  """处理资源文件中的导入指令
86
236
 
@@ -108,12 +258,13 @@ class CustomKeywordManager:
108
258
  # 递归加载导入的资源文件
109
259
  self.load_resource_file(imported_file)
110
260
 
111
- def _register_keywords(self, ast: Node, file_path: str) -> None:
112
- """从AST中注册关键字
261
+ def _register_keywords_from_ast(self, ast: Node,
262
+ source_name: str) -> None:
263
+ """从AST中注册关键字(重构后的版本)
113
264
 
114
265
  Args:
115
266
  ast: 抽象语法树
116
- file_path: 文件路径
267
+ source_name: 来源名称
117
268
  """
118
269
  if ast.type != 'Start' or len(ast.children) < 2:
119
270
  return
@@ -125,7 +276,7 @@ class CustomKeywordManager:
125
276
 
126
277
  for node in statements_node.children:
127
278
  if node.type in ['CustomKeyword', 'Function']:
128
- self._register_custom_keyword(node, file_path)
279
+ self._register_custom_keyword(node, source_name)
129
280
 
130
281
  def _register_custom_keyword(self, node: Node, file_path: str) -> None:
131
282
  """注册自定义关键字
@@ -206,6 +357,91 @@ class CustomKeywordManager:
206
357
 
207
358
  print(f"已注册自定义关键字: {keyword_name} 来自文件: {file_path}")
208
359
 
360
+ def register_keyword_from_dsl_content(self, dsl_content: str,
361
+ source_name: str = "DSL内容") -> list:
362
+ """从DSL内容注册关键字(公共方法)
363
+
364
+ Args:
365
+ dsl_content: DSL文本内容
366
+ source_name: 来源名称,用于日志显示
367
+
368
+ Returns:
369
+ list: 注册成功的关键字名称列表
370
+
371
+ Raises:
372
+ Exception: 解析或注册失败时抛出异常
373
+ """
374
+ try:
375
+ # 解析DSL内容
376
+ lexer = get_lexer()
377
+ parser = get_parser()
378
+ ast = parser.parse(dsl_content, lexer=lexer)
379
+
380
+ # 收集注册前的关键字列表
381
+ existing_keywords = (
382
+ set(keyword_manager._keywords.keys())
383
+ if hasattr(keyword_manager, '_keywords')
384
+ else set()
385
+ )
386
+
387
+ # 使用统一的注册方法
388
+ self._register_keywords_from_ast(ast, source_name)
389
+
390
+ # 计算新注册的关键字
391
+ new_keywords = (
392
+ set(keyword_manager._keywords.keys())
393
+ if hasattr(keyword_manager, '_keywords')
394
+ else set()
395
+ )
396
+ registered_keywords = list(new_keywords - existing_keywords)
397
+
398
+ if not registered_keywords:
399
+ raise ValueError("在DSL内容中未找到任何关键字定义")
400
+
401
+ return registered_keywords
402
+
403
+ except Exception as e:
404
+ print(f"从DSL内容注册关键字失败(来源:{source_name}): {e}")
405
+ raise
406
+
407
+ def register_specific_keyword_from_dsl_content(
408
+ self, keyword_name: str, dsl_content: str,
409
+ source_name: str = "DSL内容") -> bool:
410
+ """从DSL内容注册指定的关键字(公共方法)
411
+
412
+ Args:
413
+ keyword_name: 要注册的关键字名称
414
+ dsl_content: DSL文本内容
415
+ source_name: 来源名称,用于日志显示
416
+
417
+ Returns:
418
+ bool: 是否注册成功
419
+
420
+ Raises:
421
+ Exception: 解析失败或未找到指定关键字时抛出异常
422
+ """
423
+ try:
424
+ # 解析DSL内容
425
+ lexer = get_lexer()
426
+ parser = get_parser()
427
+ ast = parser.parse(dsl_content, lexer=lexer)
428
+
429
+ # 查找指定的关键字定义
430
+ if ast.type == 'Start' and len(ast.children) >= 2:
431
+ statements_node = ast.children[1]
432
+ if statements_node.type == 'Statements':
433
+ for node in statements_node.children:
434
+ if (node.type in ['CustomKeyword', 'Function'] and
435
+ node.value == keyword_name):
436
+ self._register_custom_keyword(node, source_name)
437
+ return True
438
+
439
+ raise ValueError(f"在DSL内容中未找到关键字定义: {keyword_name}")
440
+
441
+ except Exception as e:
442
+ print(f"从DSL内容注册关键字失败 {keyword_name}(来源:{source_name}): {e}")
443
+ raise
444
+
209
445
 
210
446
  # 创建全局自定义关键字管理器实例
211
447
  custom_keyword_manager = CustomKeywordManager()