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 +59 -4
- pytest_dsl/core/custom_keyword_manager.py +250 -14
- pytest_dsl/core/dsl_executor.py +251 -25
- pytest_dsl/core/hook_manager.py +87 -0
- pytest_dsl/core/hookable_executor.py +134 -0
- pytest_dsl/core/hookable_keyword_manager.py +106 -0
- pytest_dsl/core/hookspecs.py +175 -0
- pytest_dsl/core/parser.py +7 -2
- pytest_dsl/core/parsetab.py +90 -89
- pytest_dsl/core/yaml_loader.py +142 -42
- pytest_dsl/core/yaml_vars.py +90 -7
- pytest_dsl/plugin.py +19 -2
- {pytest_dsl-0.12.1.dist-info → pytest_dsl-0.14.0.dist-info}/METADATA +1 -1
- {pytest_dsl-0.12.1.dist-info → pytest_dsl-0.14.0.dist-info}/RECORD +18 -14
- {pytest_dsl-0.12.1.dist-info → pytest_dsl-0.14.0.dist-info}/WHEEL +0 -0
- {pytest_dsl-0.12.1.dist-info → pytest_dsl-0.14.0.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.12.1.dist-info → pytest_dsl-0.14.0.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.12.1.dist-info → pytest_dsl-0.14.0.dist-info}/top_level.txt +0 -0
pytest_dsl/core/dsl_executor.py
CHANGED
@@ -2,14 +2,11 @@ import re
|
|
2
2
|
import allure
|
3
3
|
import csv
|
4
4
|
import os
|
5
|
-
import
|
6
|
-
from pytest_dsl.core.
|
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 =
|
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
|
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
|
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
|
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
|
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
|
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")
|
@@ -300,6 +317,9 @@ class DSLExecutor:
|
|
300
317
|
metadata = {}
|
301
318
|
teardown_node = None
|
302
319
|
|
320
|
+
# 自动导入项目中的resources目录
|
321
|
+
self._auto_import_resources()
|
322
|
+
|
303
323
|
# 先处理元数据和找到teardown节点
|
304
324
|
for child in node.children:
|
305
325
|
if child.type == 'Metadata':
|
@@ -314,7 +334,7 @@ class DSLExecutor:
|
|
314
334
|
elif child.type == 'Teardown':
|
315
335
|
teardown_node = child
|
316
336
|
|
317
|
-
|
337
|
+
# 在_execute_test_iteration之前添加
|
318
338
|
self._handle_custom_keywords_in_file(node)
|
319
339
|
# 执行测试
|
320
340
|
self._execute_test_iteration(metadata, node, teardown_node)
|
@@ -334,6 +354,67 @@ class DSLExecutor:
|
|
334
354
|
# 测试用例执行完成后清空上下文
|
335
355
|
self.test_context.clear()
|
336
356
|
|
357
|
+
def _auto_import_resources(self):
|
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
|
+
# 然后进行传统的文件系统自动导入
|
383
|
+
try:
|
384
|
+
from pytest_dsl.core.custom_keyword_manager import (
|
385
|
+
custom_keyword_manager
|
386
|
+
)
|
387
|
+
|
388
|
+
# 尝试从多个可能的项目根目录位置导入resources
|
389
|
+
possible_roots = [
|
390
|
+
os.getcwd(), # 当前工作目录
|
391
|
+
os.path.dirname(os.getcwd()), # 上级目录
|
392
|
+
]
|
393
|
+
|
394
|
+
# 如果在pytest环境中,尝试获取pytest的根目录
|
395
|
+
try:
|
396
|
+
import pytest
|
397
|
+
if hasattr(pytest, 'config') and pytest.config:
|
398
|
+
pytest_root = pytest.config.rootdir
|
399
|
+
if pytest_root:
|
400
|
+
possible_roots.insert(0, str(pytest_root))
|
401
|
+
except Exception:
|
402
|
+
pass
|
403
|
+
|
404
|
+
# 尝试每个可能的根目录
|
405
|
+
for project_root in possible_roots:
|
406
|
+
if project_root and os.path.exists(project_root):
|
407
|
+
resources_dir = os.path.join(project_root, "resources")
|
408
|
+
if (os.path.exists(resources_dir) and
|
409
|
+
os.path.isdir(resources_dir)):
|
410
|
+
custom_keyword_manager.auto_import_resources_directory(
|
411
|
+
project_root)
|
412
|
+
break
|
413
|
+
|
414
|
+
except Exception as e:
|
415
|
+
# 自动导入失败不应该影响测试执行,只记录警告
|
416
|
+
print(f"自动导入resources目录时出现警告: {str(e)}")
|
417
|
+
|
337
418
|
def _handle_import(self, file_path):
|
338
419
|
"""处理导入指令
|
339
420
|
|
@@ -345,10 +426,34 @@ class DSLExecutor:
|
|
345
426
|
return
|
346
427
|
|
347
428
|
try:
|
348
|
-
#
|
349
|
-
|
350
|
-
|
351
|
-
|
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)
|
352
457
|
except Exception as e:
|
353
458
|
print(f"导入资源文件失败: {file_path}, 错误: {str(e)}")
|
354
459
|
raise
|
@@ -448,7 +553,8 @@ class DSLExecutor:
|
|
448
553
|
if capture_var.startswith('g_'):
|
449
554
|
global_context.set_variable(capture_var, capture_value)
|
450
555
|
else:
|
451
|
-
self.variable_replacer.local_variables[
|
556
|
+
self.variable_replacer.local_variables[
|
557
|
+
capture_var] = capture_value
|
452
558
|
self.test_context.set(capture_var, capture_value)
|
453
559
|
|
454
560
|
# 将主要结果赋值给指定变量
|
@@ -467,7 +573,8 @@ class DSLExecutor:
|
|
467
573
|
)
|
468
574
|
else:
|
469
575
|
# 存储在本地变量字典和测试上下文中
|
470
|
-
self.variable_replacer.local_variables[
|
576
|
+
self.variable_replacer.local_variables[
|
577
|
+
var_name] = actual_result
|
471
578
|
self.test_context.set(var_name, actual_result) # 同时添加到测试上下文
|
472
579
|
allure.attach(
|
473
580
|
f"变量: {var_name}\n值: {actual_result}",
|
@@ -529,7 +636,7 @@ class DSLExecutor:
|
|
529
636
|
# 由于KeywordManager中的wrapper已经添加了allure.step和日志,这里不再重复添加
|
530
637
|
result = keyword_manager.execute(keyword_name, **kwargs)
|
531
638
|
return result
|
532
|
-
except Exception
|
639
|
+
except Exception:
|
533
640
|
# 异常会在KeywordManager的wrapper中记录,这里只需要向上抛出
|
534
641
|
raise
|
535
642
|
|
@@ -704,7 +811,8 @@ class DSLExecutor:
|
|
704
811
|
if capture_var.startswith('g_'):
|
705
812
|
global_context.set_variable(capture_var, capture_value)
|
706
813
|
else:
|
707
|
-
self.variable_replacer.local_variables[
|
814
|
+
self.variable_replacer.local_variables[
|
815
|
+
capture_var] = capture_value
|
708
816
|
self.test_context.set(capture_var, capture_value)
|
709
817
|
|
710
818
|
# 将主要结果赋值给指定变量
|
@@ -723,7 +831,8 @@ class DSLExecutor:
|
|
723
831
|
)
|
724
832
|
else:
|
725
833
|
# 存储在本地变量字典和测试上下文中
|
726
|
-
self.variable_replacer.local_variables[
|
834
|
+
self.variable_replacer.local_variables[
|
835
|
+
var_name] = actual_result
|
727
836
|
self.test_context.set(var_name, actual_result) # 同时添加到测试上下文
|
728
837
|
allure.attach(
|
729
838
|
f"变量: {var_name}\n值: {actual_result}",
|
@@ -731,7 +840,7 @@ class DSLExecutor:
|
|
731
840
|
attachment_type=allure.attachment_type.TEXT
|
732
841
|
)
|
733
842
|
else:
|
734
|
-
raise Exception(
|
843
|
+
raise Exception("远程关键字没有返回结果")
|
735
844
|
|
736
845
|
def execute(self, node):
|
737
846
|
"""执行AST节点"""
|
@@ -749,7 +858,8 @@ class DSLExecutor:
|
|
749
858
|
'CustomKeyword': lambda _: None, # 添加对CustomKeyword节点的处理,只需注册不需执行
|
750
859
|
'RemoteImport': self._handle_remote_import,
|
751
860
|
'RemoteKeywordCall': self._execute_remote_keyword_call,
|
752
|
-
'AssignmentRemoteKeywordCall':
|
861
|
+
'AssignmentRemoteKeywordCall': (
|
862
|
+
self._handle_assignment_remote_keyword_call),
|
753
863
|
'Break': self._handle_break,
|
754
864
|
'Continue': self._handle_continue
|
755
865
|
}
|
@@ -759,6 +869,122 @@ class DSLExecutor:
|
|
759
869
|
return handler(node)
|
760
870
|
raise Exception(f"未知的节点类型: {node.type}")
|
761
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
|
+
|
762
988
|
|
763
989
|
def read_file(filename):
|
764
990
|
"""读取 DSL 文件内容"""
|
@@ -0,0 +1,87 @@
|
|
1
|
+
"""
|
2
|
+
pytest-dsl hook管理器
|
3
|
+
|
4
|
+
管理插件的注册、发现和调用
|
5
|
+
"""
|
6
|
+
import pluggy
|
7
|
+
from typing import Optional, List, Any
|
8
|
+
from .hookspecs import DSLHookSpecs
|
9
|
+
|
10
|
+
|
11
|
+
class DSLHookManager:
|
12
|
+
"""DSL Hook管理器"""
|
13
|
+
|
14
|
+
_instance: Optional['DSLHookManager'] = None
|
15
|
+
|
16
|
+
def __init__(self):
|
17
|
+
self.pm: pluggy.PluginManager = pluggy.PluginManager("pytest_dsl")
|
18
|
+
self.pm.add_hookspecs(DSLHookSpecs)
|
19
|
+
self._initialized = False
|
20
|
+
|
21
|
+
@classmethod
|
22
|
+
def get_instance(cls) -> 'DSLHookManager':
|
23
|
+
"""获取单例实例"""
|
24
|
+
if cls._instance is None:
|
25
|
+
cls._instance = cls()
|
26
|
+
return cls._instance
|
27
|
+
|
28
|
+
def register_plugin(self, plugin: Any, name: Optional[str] = None) -> None:
|
29
|
+
"""注册插件
|
30
|
+
|
31
|
+
Args:
|
32
|
+
plugin: 插件实例或模块
|
33
|
+
name: 插件名称(可选)
|
34
|
+
"""
|
35
|
+
self.pm.register(plugin, name=name)
|
36
|
+
|
37
|
+
def unregister_plugin(self, plugin: Any = None,
|
38
|
+
name: Optional[str] = None) -> None:
|
39
|
+
"""注销插件
|
40
|
+
|
41
|
+
Args:
|
42
|
+
plugin: 插件实例或模块
|
43
|
+
name: 插件名称
|
44
|
+
"""
|
45
|
+
self.pm.unregister(plugin=plugin, name=name)
|
46
|
+
|
47
|
+
def is_registered(self, plugin: Any) -> bool:
|
48
|
+
"""检查插件是否已注册"""
|
49
|
+
return self.pm.is_registered(plugin)
|
50
|
+
|
51
|
+
def load_setuptools_entrypoints(self, group: str = "pytest_dsl") -> int:
|
52
|
+
"""加载setuptools入口点插件
|
53
|
+
|
54
|
+
Args:
|
55
|
+
group: 入口点组名
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
加载的插件数量
|
59
|
+
"""
|
60
|
+
return self.pm.load_setuptools_entrypoints(group)
|
61
|
+
|
62
|
+
def get_plugins(self) -> List[Any]:
|
63
|
+
"""获取所有已注册的插件"""
|
64
|
+
return self.pm.get_plugins()
|
65
|
+
|
66
|
+
def hook(self) -> Any:
|
67
|
+
"""获取hook调用器"""
|
68
|
+
return self.pm.hook
|
69
|
+
|
70
|
+
def initialize(self) -> None:
|
71
|
+
"""初始化hook管理器"""
|
72
|
+
if self._initialized:
|
73
|
+
return
|
74
|
+
|
75
|
+
# 尝试加载setuptools入口点插件
|
76
|
+
try:
|
77
|
+
loaded = self.load_setuptools_entrypoints()
|
78
|
+
if loaded > 0:
|
79
|
+
print(f"加载了 {loaded} 个插件")
|
80
|
+
except Exception as e:
|
81
|
+
print(f"加载插件时出现错误: {e}")
|
82
|
+
|
83
|
+
self._initialized = True
|
84
|
+
|
85
|
+
|
86
|
+
# 全局hook管理器实例
|
87
|
+
hook_manager = DSLHookManager.get_instance()
|
@@ -0,0 +1,134 @@
|
|
1
|
+
"""
|
2
|
+
可扩展的DSL执行器
|
3
|
+
|
4
|
+
支持hook机制的DSL执行器,提供统一的执行接口
|
5
|
+
"""
|
6
|
+
from typing import Dict, List, Optional, Any
|
7
|
+
from .dsl_executor import DSLExecutor
|
8
|
+
from .hook_manager import hook_manager
|
9
|
+
|
10
|
+
|
11
|
+
class HookableExecutor:
|
12
|
+
"""支持Hook机制的DSL执行器"""
|
13
|
+
|
14
|
+
def __init__(self):
|
15
|
+
self.executor = None
|
16
|
+
self._ensure_initialized()
|
17
|
+
|
18
|
+
def _ensure_initialized(self):
|
19
|
+
"""确保执行器已初始化"""
|
20
|
+
if self.executor is None:
|
21
|
+
self.executor = DSLExecutor(enable_hooks=True)
|
22
|
+
|
23
|
+
def execute_dsl(self, dsl_id: str, context: Optional[Dict[str, Any]] = None) -> Any:
|
24
|
+
"""执行DSL用例
|
25
|
+
|
26
|
+
Args:
|
27
|
+
dsl_id: DSL标识符
|
28
|
+
context: 执行上下文(可选)
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
执行结果
|
32
|
+
"""
|
33
|
+
self._ensure_initialized()
|
34
|
+
|
35
|
+
# 通过hook获取执行上下文扩展
|
36
|
+
if self.executor.enable_hooks and self.executor.hook_manager:
|
37
|
+
context_results = self.executor.hook_manager.pm.hook.dsl_get_execution_context(
|
38
|
+
dsl_id=dsl_id, base_context=context or {}
|
39
|
+
)
|
40
|
+
for extended_context in context_results:
|
41
|
+
if extended_context:
|
42
|
+
context = extended_context
|
43
|
+
break
|
44
|
+
|
45
|
+
# 执行DSL(内容为空,通过hook加载)
|
46
|
+
return self.executor.execute_from_content(
|
47
|
+
content="",
|
48
|
+
dsl_id=dsl_id,
|
49
|
+
context=context
|
50
|
+
)
|
51
|
+
|
52
|
+
def list_dsl_cases(self, project_id: Optional[int] = None,
|
53
|
+
filters: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
|
54
|
+
"""列出DSL用例
|
55
|
+
|
56
|
+
Args:
|
57
|
+
project_id: 项目ID(可选)
|
58
|
+
filters: 过滤条件(可选)
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
用例列表
|
62
|
+
"""
|
63
|
+
self._ensure_initialized()
|
64
|
+
|
65
|
+
if not (self.executor.enable_hooks and self.executor.hook_manager):
|
66
|
+
return []
|
67
|
+
|
68
|
+
all_cases = []
|
69
|
+
case_results = self.executor.hook_manager.pm.hook.dsl_list_cases(
|
70
|
+
project_id=project_id, filters=filters
|
71
|
+
)
|
72
|
+
|
73
|
+
for result in case_results:
|
74
|
+
if result:
|
75
|
+
all_cases.extend(result)
|
76
|
+
|
77
|
+
return all_cases
|
78
|
+
|
79
|
+
def validate_dsl_content(self, dsl_id: str, content: str) -> List[str]:
|
80
|
+
"""验证DSL内容
|
81
|
+
|
82
|
+
Args:
|
83
|
+
dsl_id: DSL标识符
|
84
|
+
content: DSL内容
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
验证错误列表,空列表表示验证通过
|
88
|
+
"""
|
89
|
+
self._ensure_initialized()
|
90
|
+
|
91
|
+
if not (self.executor.enable_hooks and self.executor.hook_manager):
|
92
|
+
return []
|
93
|
+
|
94
|
+
all_errors = []
|
95
|
+
validation_results = self.executor.hook_manager.pm.hook.dsl_validate_content(
|
96
|
+
dsl_id=dsl_id, content=content
|
97
|
+
)
|
98
|
+
|
99
|
+
for errors in validation_results:
|
100
|
+
if errors:
|
101
|
+
all_errors.extend(errors)
|
102
|
+
|
103
|
+
return all_errors
|
104
|
+
|
105
|
+
def transform_dsl_content(self, dsl_id: str, content: str) -> str:
|
106
|
+
"""转换DSL内容
|
107
|
+
|
108
|
+
Args:
|
109
|
+
dsl_id: DSL标识符
|
110
|
+
content: 原始DSL内容
|
111
|
+
|
112
|
+
Returns:
|
113
|
+
转换后的DSL内容
|
114
|
+
"""
|
115
|
+
self._ensure_initialized()
|
116
|
+
|
117
|
+
if not (self.executor.enable_hooks and self.executor.hook_manager):
|
118
|
+
return content
|
119
|
+
|
120
|
+
transformed_content = content
|
121
|
+
transform_results = self.executor.hook_manager.pm.hook.dsl_transform_content(
|
122
|
+
dsl_id=dsl_id, content=content
|
123
|
+
)
|
124
|
+
|
125
|
+
for result in transform_results:
|
126
|
+
if result:
|
127
|
+
transformed_content = result
|
128
|
+
break
|
129
|
+
|
130
|
+
return transformed_content
|
131
|
+
|
132
|
+
|
133
|
+
# 全局实例
|
134
|
+
hookable_executor = HookableExecutor()
|