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.
@@ -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")
@@ -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
- # 在_execute_test_iteration之前添加
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
- from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
350
- custom_keyword_manager.load_resource_file(file_path)
351
- 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)
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[capture_var] = capture_value
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[var_name] = actual_result
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 as e:
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[capture_var] = capture_value
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[var_name] = actual_result
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(f"远程关键字没有返回结果")
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': self._handle_assignment_remote_keyword_call,
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()