pytest-dsl 0.13.0__tar.gz → 0.14.0__tar.gz

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.
Files changed (102) hide show
  1. {pytest_dsl-0.13.0/pytest_dsl.egg-info → pytest_dsl-0.14.0}/PKG-INFO +1 -1
  2. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pyproject.toml +4 -4
  3. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/cli.py +34 -2
  4. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/custom_keyword_manager.py +114 -14
  5. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/dsl_executor.py +217 -28
  6. pytest_dsl-0.14.0/pytest_dsl/core/hook_manager.py +87 -0
  7. pytest_dsl-0.14.0/pytest_dsl/core/hookable_executor.py +134 -0
  8. pytest_dsl-0.14.0/pytest_dsl/core/hookable_keyword_manager.py +106 -0
  9. pytest_dsl-0.14.0/pytest_dsl/core/hookspecs.py +175 -0
  10. pytest_dsl-0.14.0/pytest_dsl/core/yaml_loader.py +239 -0
  11. pytest_dsl-0.14.0/pytest_dsl/core/yaml_vars.py +158 -0
  12. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0/pytest_dsl.egg-info}/PKG-INFO +1 -1
  13. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl.egg-info/SOURCES.txt +11 -0
  14. pytest_dsl-0.14.0/tests/auth_config.yaml +280 -0
  15. pytest_dsl-0.14.0/tests/simple_config.yaml +64 -0
  16. pytest_dsl-0.14.0/tests/test_auth_mock_server.py +627 -0
  17. pytest_dsl-0.14.0/tests/test_auth_runner.py +337 -0
  18. pytest_dsl-0.14.0/tests/test_platform_hook_integration.py +981 -0
  19. pytest_dsl-0.14.0/tests/test_platform_hook_pytest.py +59 -0
  20. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/test_retry_runner.py +0 -1
  21. pytest_dsl-0.14.0/tests/test_simple_hook_demo.py +357 -0
  22. pytest_dsl-0.13.0/pytest_dsl/core/yaml_loader.py +0 -139
  23. pytest_dsl-0.13.0/pytest_dsl/core/yaml_vars.py +0 -75
  24. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/LICENSE +0 -0
  25. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/MANIFEST.in +0 -0
  26. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/README.md +0 -0
  27. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/__init__.py +0 -0
  28. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/conftest_adapter.py +0 -0
  29. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/__init__.py +0 -0
  30. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/auth_provider.py +0 -0
  31. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/auto_decorator.py +0 -0
  32. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/auto_directory.py +0 -0
  33. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/context.py +0 -0
  34. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/dsl_executor_utils.py +0 -0
  35. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/global_context.py +0 -0
  36. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/http_client.py +0 -0
  37. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/http_request.py +0 -0
  38. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/keyword_manager.py +0 -0
  39. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/lexer.py +0 -0
  40. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/parser.py +0 -0
  41. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/parsetab.py +0 -0
  42. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/plugin_discovery.py +0 -0
  43. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/utils.py +0 -0
  44. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/core/variable_utils.py +0 -0
  45. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/docs/custom_keywords.md +0 -0
  46. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/__init__.py +0 -0
  47. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/assert/assertion_example.auto +0 -0
  48. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/assert/boolean_test.auto +0 -0
  49. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/assert/expression_test.auto +0 -0
  50. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/custom/test_advanced_keywords.auto +0 -0
  51. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/custom/test_custom_keywords.auto +0 -0
  52. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/custom/test_default_values.auto +0 -0
  53. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/__init__.py +0 -0
  54. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/builtin_auth_test.auto +0 -0
  55. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/file_reference_test.auto +0 -0
  56. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/http_advanced.auto +0 -0
  57. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/http_example.auto +0 -0
  58. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/http_length_test.auto +0 -0
  59. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/http_retry_assertions.auto +0 -0
  60. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +0 -0
  61. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/http_with_yaml.auto +0 -0
  62. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/new_retry_test.auto +0 -0
  63. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/retry_assertions_only.auto +0 -0
  64. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/retry_config_only.auto +0 -0
  65. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/retry_debug.auto +0 -0
  66. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/retry_with_fix.auto +0 -0
  67. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/simple_retry.auto +0 -0
  68. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/http/vars.yaml +0 -0
  69. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/quickstart/api_basics.auto +0 -0
  70. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/quickstart/assertions.auto +0 -0
  71. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/quickstart/loops.auto +0 -0
  72. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/test_assert.py +0 -0
  73. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/test_custom_keyword.py +0 -0
  74. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/test_http.py +0 -0
  75. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/examples/test_quickstart.py +0 -0
  76. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/keywords/__init__.py +0 -0
  77. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/keywords/assertion_keywords.py +0 -0
  78. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/keywords/global_keywords.py +0 -0
  79. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/keywords/http_keywords.py +0 -0
  80. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/keywords/system_keywords.py +0 -0
  81. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/main_adapter.py +0 -0
  82. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/plugin.py +0 -0
  83. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/remote/__init__.py +0 -0
  84. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/remote/hook_manager.py +0 -0
  85. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/remote/keyword_client.py +0 -0
  86. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/remote/keyword_server.py +0 -0
  87. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/remote/variable_bridge.py +0 -0
  88. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl/templates/keywords_report.html +0 -0
  89. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl.egg-info/dependency_links.txt +0 -0
  90. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl.egg-info/entry_points.txt +0 -0
  91. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl.egg-info/requires.txt +0 -0
  92. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/pytest_dsl.egg-info/top_level.txt +0 -0
  93. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/setup.cfg +0 -0
  94. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/setup.py +0 -0
  95. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/mock_config.yaml +0 -0
  96. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/test_end_to_end_seamless.py +0 -0
  97. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/test_enhanced_variable_access.py +0 -0
  98. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/test_http_assertions_extractors.py +0 -0
  99. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/test_mock_server.py +0 -0
  100. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/test_seamless_variable_sync.py +0 -0
  101. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/test_variable_sync.py +0 -0
  102. {pytest_dsl-0.13.0 → pytest_dsl-0.14.0}/tests/test_variable_sync_demo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-dsl
3
- Version: 0.13.0
3
+ Version: 0.14.0
4
4
  Summary: A DSL testing framework based on pytest
5
5
  Author: Chen Shuanglin
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pytest-dsl"
7
- version = "0.13.0"
7
+ version = "0.14.0"
8
8
  description = "A DSL testing framework based on pytest"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -49,6 +49,6 @@ pytest-dsl-list = "pytest_dsl.cli:main_list_keywords"
49
49
  "Bug Tracker" = "https://github.com/felix-1991/pytest-dsl/issues"
50
50
 
51
51
 
52
- [[tool.uv.index]]
53
- name = "tuna"
54
- url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
52
+ # [[tool.uv.index]]
53
+ # name = "tuna"
54
+ # url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
@@ -724,12 +724,17 @@ def list_keywords(output_format='json', name_filter=None,
724
724
 
725
725
  def load_yaml_variables(args):
726
726
  """从命令行参数加载YAML变量"""
727
- # 使用统一的加载函数,包含远程服务器自动连接功能
727
+ # 使用统一的加载函数,包含远程服务器自动连接功能和hook支持
728
728
  try:
729
+ # 尝试从环境变量获取环境名称
730
+ environment = (os.environ.get('PYTEST_DSL_ENVIRONMENT') or
731
+ os.environ.get('ENVIRONMENT'))
732
+
729
733
  load_yaml_variables_from_args(
730
734
  yaml_files=args.yaml_vars,
731
735
  yaml_vars_dir=args.yaml_vars_dir,
732
- project_root=os.getcwd() # CLI模式下使用当前工作目录作为项目根目录
736
+ project_root=os.getcwd(), # CLI模式下使用当前工作目录作为项目根目录
737
+ environment=environment
733
738
  )
734
739
  except Exception as e:
735
740
  print(f"加载YAML变量失败: {str(e)}")
@@ -774,6 +779,33 @@ def run_dsl_tests(args):
774
779
  # 加载YAML变量(包括远程服务器自动连接)
775
780
  load_yaml_variables(args)
776
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用例,使用传统的文件执行方式
777
809
  lexer = get_lexer()
778
810
  parser = get_parser()
779
811
  executor = DSLExecutor()
@@ -200,23 +200,37 @@ class CustomKeywordManager:
200
200
  with open(file_path, 'r', encoding='utf-8') as f:
201
201
  content = f.read()
202
202
 
203
- # 解析资源文件
204
- lexer = get_lexer()
205
- parser = get_parser()
206
- ast = parser.parse(content, lexer=lexer)
207
-
208
- # 标记为已加载
203
+ # 标记为已加载(在解析前标记,避免循环导入)
209
204
  self.resource_cache[absolute_path] = True
210
205
 
211
- # 处理导入指令
212
- self._process_imports(ast, os.path.dirname(file_path))
206
+ # 使用公共方法解析和处理资源文件内容
207
+ self._process_resource_file_content(content, file_path)
213
208
 
214
- # 注册关键字
215
- self._register_keywords(ast, file_path)
216
209
  except Exception as e:
210
+ # 如果处理失败,移除缓存标记
211
+ self.resource_cache.pop(absolute_path, None)
217
212
  print(f"资源文件 {file_path} 加载失败: {str(e)}")
218
213
  raise
219
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
+
220
234
  def _process_imports(self, ast: Node, base_dir: str) -> None:
221
235
  """处理资源文件中的导入指令
222
236
 
@@ -244,12 +258,13 @@ class CustomKeywordManager:
244
258
  # 递归加载导入的资源文件
245
259
  self.load_resource_file(imported_file)
246
260
 
247
- def _register_keywords(self, ast: Node, file_path: str) -> None:
248
- """从AST中注册关键字
261
+ def _register_keywords_from_ast(self, ast: Node,
262
+ source_name: str) -> None:
263
+ """从AST中注册关键字(重构后的版本)
249
264
 
250
265
  Args:
251
266
  ast: 抽象语法树
252
- file_path: 文件路径
267
+ source_name: 来源名称
253
268
  """
254
269
  if ast.type != 'Start' or len(ast.children) < 2:
255
270
  return
@@ -261,7 +276,7 @@ class CustomKeywordManager:
261
276
 
262
277
  for node in statements_node.children:
263
278
  if node.type in ['CustomKeyword', 'Function']:
264
- self._register_custom_keyword(node, file_path)
279
+ self._register_custom_keyword(node, source_name)
265
280
 
266
281
  def _register_custom_keyword(self, node: Node, file_path: str) -> None:
267
282
  """注册自定义关键字
@@ -342,6 +357,91 @@ class CustomKeywordManager:
342
357
 
343
358
  print(f"已注册自定义关键字: {keyword_name} 来自文件: {file_path}")
344
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
+
345
445
 
346
446
  # 创建全局自定义关键字管理器实例
347
447
  custom_keyword_manager = CustomKeywordManager()
@@ -2,14 +2,11 @@ import re
2
2
  import allure
3
3
  import csv
4
4
  import os
5
- import pytest
6
- from pytest_dsl.core.lexer import get_lexer
7
- from pytest_dsl.core.parser import get_parser, Node
5
+ from typing import Dict, Any
6
+ from pytest_dsl.core.parser import Node
8
7
  from pytest_dsl.core.keyword_manager import keyword_manager
9
8
  from pytest_dsl.core.global_context import global_context
10
9
  from pytest_dsl.core.context import TestContext
11
- import pytest_dsl.keywords
12
- from pytest_dsl.core.yaml_vars import yaml_vars
13
10
  from pytest_dsl.core.variable_utils import VariableReplacer
14
11
 
15
12
 
@@ -39,14 +36,25 @@ class DSLExecutor:
39
36
  - PYTEST_DSL_KEEP_VARIABLES=0: (默认) 执行完成后清空变量,用于正常DSL执行
40
37
  """
41
38
 
42
- def __init__(self):
43
- """初始化DSL执行器"""
39
+ def __init__(self, enable_hooks: bool = True):
40
+ """初始化DSL执行器
41
+
42
+ Args:
43
+ enable_hooks: 是否启用hook机制,默认True
44
+ """
44
45
  self.variables = {}
45
46
  self.test_context = TestContext()
46
47
  self.test_context.executor = self # 让 test_context 能够访问到 executor
47
48
  self.variable_replacer = VariableReplacer(
48
49
  self.variables, self.test_context)
49
50
  self.imported_files = set() # 跟踪已导入的文件,避免循环导入
51
+
52
+ # Hook相关配置
53
+ self.enable_hooks = enable_hooks
54
+ self.current_dsl_id = None # 当前执行的DSL标识符
55
+
56
+ if self.enable_hooks:
57
+ self._init_hooks()
50
58
 
51
59
  def set_current_data(self, data):
52
60
  """设置当前测试数据集"""
@@ -141,7 +149,11 @@ class DSLExecutor:
141
149
  return self.variable_replacer.local_variables[value]
142
150
 
143
151
  # 定义扩展的变量引用模式,支持数组索引和字典键访问
144
- pattern = r'\$\{([a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*(?:(?:\.[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)|(?:\[[^\]]+\]))*)\}'
152
+ pattern = (
153
+ r'\$\{([a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*'
154
+ r'(?:(?:\.[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)'
155
+ r'|(?:\[[^\]]+\]))*)\}'
156
+ )
145
157
  # 检查整个字符串是否完全匹配单一变量引用模式
146
158
  match = re.fullmatch(pattern, value)
147
159
  if match:
@@ -198,13 +210,15 @@ class DSLExecutor:
198
210
  operator = expr_node.value # 操作符: +, -, *, /, %
199
211
 
200
212
  # 尝试类型转换 - 如果是字符串数字则转为数字
201
- if isinstance(left_value, str) and str(left_value).replace('.', '', 1).isdigit():
213
+ if (isinstance(left_value, str) and
214
+ str(left_value).replace('.', '', 1).isdigit()):
202
215
  left_value = float(left_value)
203
216
  # 如果是整数则转为整数
204
217
  if left_value.is_integer():
205
218
  left_value = int(left_value)
206
219
 
207
- if isinstance(right_value, str) and str(right_value).replace('.', '', 1).isdigit():
220
+ if (isinstance(right_value, str) and
221
+ str(right_value).replace('.', '', 1).isdigit()):
208
222
  right_value = float(right_value)
209
223
  # 如果是整数则转为整数
210
224
  if right_value.is_integer():
@@ -220,9 +234,11 @@ class DSLExecutor:
220
234
  return left_value - right_value
221
235
  elif operator == '*':
222
236
  # 如果其中一个是字符串,另一个是数字,则进行字符串重复
223
- if isinstance(left_value, str) and isinstance(right_value, (int, float)):
237
+ if (isinstance(left_value, str) and
238
+ isinstance(right_value, (int, float))):
224
239
  return left_value * int(right_value)
225
- elif isinstance(right_value, str) and isinstance(left_value, (int, float)):
240
+ elif (isinstance(right_value, str) and
241
+ isinstance(left_value, (int, float))):
226
242
  return right_value * int(left_value)
227
243
  return left_value * right_value
228
244
  elif operator == '/':
@@ -287,7 +303,8 @@ class DSLExecutor:
287
303
  for stmt in statements_node.children:
288
304
  if stmt.type == 'CustomKeyword':
289
305
  # 导入自定义关键字管理器
290
- from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
306
+ from pytest_dsl.core.custom_keyword_manager import (
307
+ custom_keyword_manager)
291
308
  # 注册自定义关键字
292
309
  custom_keyword_manager._register_custom_keyword(
293
310
  stmt, "current_file")
@@ -317,7 +334,7 @@ class DSLExecutor:
317
334
  elif child.type == 'Teardown':
318
335
  teardown_node = child
319
336
 
320
- # 在_execute_test_iteration之前添加
337
+ # 在_execute_test_iteration之前添加
321
338
  self._handle_custom_keywords_in_file(node)
322
339
  # 执行测试
323
340
  self._execute_test_iteration(metadata, node, teardown_node)
@@ -339,8 +356,34 @@ class DSLExecutor:
339
356
 
340
357
  def _auto_import_resources(self):
341
358
  """自动导入项目中的resources目录"""
359
+ # 首先尝试通过hook获取资源列表
360
+ if (self.enable_hooks and hasattr(self, 'hook_manager') and
361
+ self.hook_manager):
362
+ try:
363
+ cases = []
364
+ case_results = self.hook_manager.pm.hook.dsl_list_cases()
365
+ for result in case_results:
366
+ if result:
367
+ cases.extend(result)
368
+
369
+ # 如果hook返回了资源,导入它们
370
+ for case in cases:
371
+ case_id = case.get('id') or case.get('file_path', '')
372
+ if case_id and case_id not in self.imported_files:
373
+ try:
374
+ print(f"通过hook自动导入资源: {case_id}")
375
+ self._handle_import(case_id)
376
+ except Exception as e:
377
+ print(f"通过hook自动导入资源失败: {case_id}, 错误: {str(e)}")
378
+ continue
379
+ except Exception as e:
380
+ print(f"通过hook自动导入资源时出现警告: {str(e)}")
381
+
382
+ # 然后进行传统的文件系统自动导入
342
383
  try:
343
- from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
384
+ from pytest_dsl.core.custom_keyword_manager import (
385
+ custom_keyword_manager
386
+ )
344
387
 
345
388
  # 尝试从多个可能的项目根目录位置导入resources
346
389
  possible_roots = [
@@ -355,14 +398,15 @@ class DSLExecutor:
355
398
  pytest_root = pytest.config.rootdir
356
399
  if pytest_root:
357
400
  possible_roots.insert(0, str(pytest_root))
358
- except:
401
+ except Exception:
359
402
  pass
360
403
 
361
404
  # 尝试每个可能的根目录
362
405
  for project_root in possible_roots:
363
406
  if project_root and os.path.exists(project_root):
364
407
  resources_dir = os.path.join(project_root, "resources")
365
- if os.path.exists(resources_dir) and os.path.isdir(resources_dir):
408
+ if (os.path.exists(resources_dir) and
409
+ os.path.isdir(resources_dir)):
366
410
  custom_keyword_manager.auto_import_resources_directory(
367
411
  project_root)
368
412
  break
@@ -382,10 +426,34 @@ class DSLExecutor:
382
426
  return
383
427
 
384
428
  try:
385
- # 导入自定义关键字文件
386
- from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
387
- custom_keyword_manager.load_resource_file(file_path)
388
- self.imported_files.add(file_path)
429
+ # 尝试通过hook加载内容
430
+ content = None
431
+ if (self.enable_hooks and hasattr(self, 'hook_manager') and
432
+ self.hook_manager):
433
+ content_results = (
434
+ self.hook_manager.pm.hook.dsl_load_content(
435
+ dsl_id=file_path
436
+ )
437
+ )
438
+ for result in content_results:
439
+ if result is not None:
440
+ content = result
441
+ break
442
+
443
+ # 如果hook返回了内容,直接使用DSL解析方式处理
444
+ if content is not None:
445
+ ast = self._parse_dsl_content(content)
446
+
447
+ # 只处理自定义关键字,不执行测试流程
448
+ self._handle_custom_keywords_in_file(ast)
449
+ self.imported_files.add(file_path)
450
+ else:
451
+ # 使用传统方式导入文件
452
+ from pytest_dsl.core.custom_keyword_manager import (
453
+ custom_keyword_manager
454
+ )
455
+ custom_keyword_manager.load_resource_file(file_path)
456
+ self.imported_files.add(file_path)
389
457
  except Exception as e:
390
458
  print(f"导入资源文件失败: {file_path}, 错误: {str(e)}")
391
459
  raise
@@ -485,7 +553,8 @@ class DSLExecutor:
485
553
  if capture_var.startswith('g_'):
486
554
  global_context.set_variable(capture_var, capture_value)
487
555
  else:
488
- self.variable_replacer.local_variables[capture_var] = capture_value
556
+ self.variable_replacer.local_variables[
557
+ capture_var] = capture_value
489
558
  self.test_context.set(capture_var, capture_value)
490
559
 
491
560
  # 将主要结果赋值给指定变量
@@ -504,7 +573,8 @@ class DSLExecutor:
504
573
  )
505
574
  else:
506
575
  # 存储在本地变量字典和测试上下文中
507
- self.variable_replacer.local_variables[var_name] = actual_result
576
+ self.variable_replacer.local_variables[
577
+ var_name] = actual_result
508
578
  self.test_context.set(var_name, actual_result) # 同时添加到测试上下文
509
579
  allure.attach(
510
580
  f"变量: {var_name}\n值: {actual_result}",
@@ -566,7 +636,7 @@ class DSLExecutor:
566
636
  # 由于KeywordManager中的wrapper已经添加了allure.step和日志,这里不再重复添加
567
637
  result = keyword_manager.execute(keyword_name, **kwargs)
568
638
  return result
569
- except Exception as e:
639
+ except Exception:
570
640
  # 异常会在KeywordManager的wrapper中记录,这里只需要向上抛出
571
641
  raise
572
642
 
@@ -741,7 +811,8 @@ class DSLExecutor:
741
811
  if capture_var.startswith('g_'):
742
812
  global_context.set_variable(capture_var, capture_value)
743
813
  else:
744
- self.variable_replacer.local_variables[capture_var] = capture_value
814
+ self.variable_replacer.local_variables[
815
+ capture_var] = capture_value
745
816
  self.test_context.set(capture_var, capture_value)
746
817
 
747
818
  # 将主要结果赋值给指定变量
@@ -760,7 +831,8 @@ class DSLExecutor:
760
831
  )
761
832
  else:
762
833
  # 存储在本地变量字典和测试上下文中
763
- self.variable_replacer.local_variables[var_name] = actual_result
834
+ self.variable_replacer.local_variables[
835
+ var_name] = actual_result
764
836
  self.test_context.set(var_name, actual_result) # 同时添加到测试上下文
765
837
  allure.attach(
766
838
  f"变量: {var_name}\n值: {actual_result}",
@@ -768,7 +840,7 @@ class DSLExecutor:
768
840
  attachment_type=allure.attachment_type.TEXT
769
841
  )
770
842
  else:
771
- raise Exception(f"远程关键字没有返回结果")
843
+ raise Exception("远程关键字没有返回结果")
772
844
 
773
845
  def execute(self, node):
774
846
  """执行AST节点"""
@@ -786,7 +858,8 @@ class DSLExecutor:
786
858
  'CustomKeyword': lambda _: None, # 添加对CustomKeyword节点的处理,只需注册不需执行
787
859
  'RemoteImport': self._handle_remote_import,
788
860
  'RemoteKeywordCall': self._execute_remote_keyword_call,
789
- 'AssignmentRemoteKeywordCall': self._handle_assignment_remote_keyword_call,
861
+ 'AssignmentRemoteKeywordCall': (
862
+ self._handle_assignment_remote_keyword_call),
790
863
  'Break': self._handle_break,
791
864
  'Continue': self._handle_continue
792
865
  }
@@ -796,6 +869,122 @@ class DSLExecutor:
796
869
  return handler(node)
797
870
  raise Exception(f"未知的节点类型: {node.type}")
798
871
 
872
+ def __repr__(self):
873
+ """返回DSL执行器的字符串表示"""
874
+ return (f"DSLExecutor(variables={len(self.variables)}, "
875
+ f"hooks_enabled={self.enable_hooks})")
876
+
877
+ def _init_hooks(self):
878
+ """初始化hook机制"""
879
+ try:
880
+ from .hook_manager import hook_manager
881
+ hook_manager.initialize()
882
+ # 调用hook注册自定义关键字
883
+ hook_manager.pm.hook.dsl_register_custom_keywords()
884
+ self.hook_manager = hook_manager
885
+ except ImportError:
886
+ # 如果没有安装pluggy,禁用hook
887
+ self.enable_hooks = False
888
+ self.hook_manager = None
889
+
890
+ def execute_from_content(self, content: str, dsl_id: str = None,
891
+ context: Dict[str, Any] = None) -> Any:
892
+ """从内容执行DSL,支持hook扩展
893
+
894
+ Args:
895
+ content: DSL内容,如果为空字符串将尝试通过hook加载
896
+ dsl_id: DSL标识符(可选)
897
+ context: 执行上下文(可选)
898
+
899
+ Returns:
900
+ 执行结果
901
+ """
902
+ self.current_dsl_id = dsl_id
903
+
904
+ # 如果content为空且有dsl_id,尝试通过hook加载内容
905
+ if (not content and dsl_id and self.enable_hooks and
906
+ hasattr(self, 'hook_manager') and self.hook_manager):
907
+ content_results = self.hook_manager.pm.hook.dsl_load_content(
908
+ dsl_id=dsl_id)
909
+ for result in content_results:
910
+ if result is not None:
911
+ content = result
912
+ break
913
+
914
+ if not content:
915
+ raise ValueError(f"无法获取DSL内容: {dsl_id}")
916
+
917
+ # 应用执行上下文
918
+ if context:
919
+ self.variables.update(context)
920
+ for key, value in context.items():
921
+ self.test_context.set(key, value)
922
+ self.variable_replacer = VariableReplacer(
923
+ self.variables, self.test_context
924
+ )
925
+
926
+ # 执行前hook
927
+ if self.enable_hooks and self.hook_manager:
928
+ self.hook_manager.pm.hook.dsl_before_execution(
929
+ dsl_id=dsl_id, context=context or {}
930
+ )
931
+
932
+ result = None
933
+ exception = None
934
+
935
+ try:
936
+ # 解析并执行
937
+ ast = self._parse_dsl_content(content)
938
+ result = self.execute(ast)
939
+
940
+ except Exception as e:
941
+ exception = e
942
+ # 执行后hook(在异常情况下)
943
+ if self.enable_hooks and self.hook_manager:
944
+ try:
945
+ self.hook_manager.pm.hook.dsl_after_execution(
946
+ dsl_id=dsl_id,
947
+ context=context or {},
948
+ result=result,
949
+ exception=exception
950
+ )
951
+ except Exception as hook_error:
952
+ print(f"Hook执行失败: {hook_error}")
953
+ raise
954
+ else:
955
+ # 执行后hook(在成功情况下)
956
+ if self.enable_hooks and self.hook_manager:
957
+ try:
958
+ self.hook_manager.pm.hook.dsl_after_execution(
959
+ dsl_id=dsl_id,
960
+ context=context or {},
961
+ result=result,
962
+ exception=None
963
+ )
964
+ except Exception as hook_error:
965
+ print(f"Hook执行失败: {hook_error}")
966
+
967
+ return result
968
+
969
+ def _parse_dsl_content(self, content: str) -> Node:
970
+ """解析DSL内容为AST(公共方法)
971
+
972
+ Args:
973
+ content: DSL文本内容
974
+
975
+ Returns:
976
+ Node: 解析后的AST根节点
977
+
978
+ Raises:
979
+ Exception: 解析失败时抛出异常
980
+ """
981
+ from pytest_dsl.core.lexer import get_lexer
982
+ from pytest_dsl.core.parser import get_parser
983
+
984
+ lexer = get_lexer()
985
+ parser = get_parser()
986
+ return parser.parse(content, lexer=lexer)
987
+
799
988
 
800
989
  def read_file(filename):
801
990
  """读取 DSL 文件内容"""