pytest-dsl 0.15.3__py3-none-any.whl → 0.15.5__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.
@@ -34,8 +34,8 @@ class ReturnException(Exception):
34
34
  class DSLExecutionError(Exception):
35
35
  """DSL执行异常,包含行号信息"""
36
36
 
37
- def __init__(self, message: str, line_number: int = None, node_type: str = None,
38
- original_exception: Exception = None):
37
+ def __init__(self, message: str, line_number: int = None,
38
+ node_type: str = None, original_exception: Exception = None):
39
39
  self.line_number = line_number
40
40
  self.node_type = node_type
41
41
  self.original_exception = original_exception
@@ -48,7 +48,9 @@ class DSLExecutionError(Exception):
48
48
  error_parts.append(f"节点类型: {node_type}")
49
49
  if original_exception:
50
50
  error_parts.append(
51
- f"原始异常: {type(original_exception).__name__}: {str(original_exception)}")
51
+ f"原始异常: {type(original_exception).__name__}: "
52
+ f"{str(original_exception)}"
53
+ )
52
54
 
53
55
  super().__init__(" | ".join(error_parts))
54
56
 
@@ -72,6 +74,10 @@ class DSLExecutor:
72
74
  self.variables = {}
73
75
  self.test_context = TestContext()
74
76
  self.test_context.executor = self # 让 test_context 能够访问到 executor
77
+
78
+ # 设置变量提供者,实现YAML变量等外部变量源的注入
79
+ self._setup_variable_providers()
80
+
75
81
  self.variable_replacer = VariableReplacer(
76
82
  self.variables, self.test_context)
77
83
  self.imported_files = set() # 跟踪已导入的文件,避免循环导入
@@ -104,7 +110,8 @@ class DSLExecutor:
104
110
  target_node = node or self._current_node
105
111
 
106
112
  # 尝试从当前节点获取行号
107
- if target_node and hasattr(target_node, 'line_number') and target_node.line_number:
113
+ if (target_node and hasattr(target_node, 'line_number') and
114
+ target_node.line_number):
108
115
  return f"\n行号: {target_node.line_number}"
109
116
 
110
117
  # 如果当前节点没有行号,从节点栈中查找最近的有行号的节点
@@ -113,12 +120,16 @@ class DSLExecutor:
113
120
  return f"\n行号: {stack_node.line_number}"
114
121
 
115
122
  # 如果当前节点没有行号,尝试从当前执行的节点获取
116
- if self._current_node and hasattr(self._current_node, 'line_number') and self._current_node.line_number:
123
+ if (self._current_node and
124
+ hasattr(self._current_node, 'line_number') and
125
+ self._current_node.line_number):
117
126
  return f"\n行号: {self._current_node.line_number}"
118
127
 
119
128
  return ""
120
129
 
121
- def _handle_exception_with_line_info(self, e: Exception, node=None, context_info: str = "", skip_allure_logging: bool = False):
130
+ def _handle_exception_with_line_info(self, e: Exception, node=None,
131
+ context_info: str = "",
132
+ skip_allure_logging: bool = False):
122
133
  """统一处理异常并记录行号信息
123
134
 
124
135
  Args:
@@ -275,7 +286,8 @@ class DSLExecutor:
275
286
  elif expr_node.type == 'StringLiteral':
276
287
  # 字符串字面量,如果包含变量占位符则进行替换,否则直接返回
277
288
  if '${' in expr_node.value:
278
- return self.variable_replacer.replace_in_string(expr_node.value)
289
+ return self.variable_replacer.replace_in_string(
290
+ expr_node.value)
279
291
  else:
280
292
  return expr_node.value
281
293
  elif expr_node.type == 'NumberLiteral':
@@ -290,7 +302,8 @@ class DSLExecutor:
290
302
  raise KeyError(f"变量 '{var_name}' 不存在")
291
303
  elif expr_node.type == 'PlaceholderRef':
292
304
  # 变量占位符 ${var},进行变量替换
293
- return self.variable_replacer.replace_in_string(expr_node.value)
305
+ return self.variable_replacer.replace_in_string(
306
+ expr_node.value)
294
307
  elif expr_node.type == 'KeywordCall':
295
308
  return self.execute(expr_node)
296
309
  elif expr_node.type == 'ListExpr':
@@ -321,7 +334,8 @@ class DSLExecutor:
321
334
  else:
322
335
  raise Exception(f"无法求值的表达式类型: {expr_node.type}")
323
336
 
324
- return self._execute_with_error_handling(_eval_expression_impl, expr_node)
337
+ return self._execute_with_error_handling(
338
+ _eval_expression_impl, expr_node)
325
339
 
326
340
  def _eval_expression_value(self, value):
327
341
  """处理表达式值的具体逻辑"""
@@ -331,8 +345,10 @@ class DSLExecutor:
331
345
  elif isinstance(value, str):
332
346
  # 定义扩展的变量引用模式,支持数组索引和字典键访问
333
347
  pattern = (
334
- r'\$\{([a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*'
335
- r'(?:(?:\.[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)'
348
+ r'\$\{([a-zA-Z_\u4e00-\u9fa5]'
349
+ r'[a-zA-Z0-9_\u4e00-\u9fa5]*'
350
+ r'(?:(?:\.[a-zA-Z_\u4e00-\u9fa5]'
351
+ r'[a-zA-Z0-9_\u4e00-\u9fa5]*)'
336
352
  r'|(?:\[[^\]]+\]))*)\}'
337
353
  )
338
354
  # 检查整个字符串是否完全匹配单一变量引用模式
@@ -347,8 +363,10 @@ class DSLExecutor:
347
363
  else:
348
364
  # 对于不包含 ${} 的普通字符串,检查是否为单纯的变量名
349
365
  # 只有当字符串是有效的变量名格式且确实存在该变量时,才当作变量处理
350
- if (re.match(r'^[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*$', value) and
351
- value in self.variable_replacer.local_variables):
366
+ var_pattern = (r'^[a-zA-Z_\u4e00-\u9fa5]'
367
+ r'[a-zA-Z0-9_\u4e00-\u9fa5]*$')
368
+ if (re.match(var_pattern, value) and
369
+ value in self.variable_replacer.local_variables):
352
370
  return self.variable_replacer.local_variables[value]
353
371
  else:
354
372
  # 否则当作字符串字面量处理
@@ -357,7 +375,8 @@ class DSLExecutor:
357
375
  except Exception as e:
358
376
  # 为变量解析异常添加更多上下文信息
359
377
  context_info = f"解析表达式值 '{value}'"
360
- self._handle_exception_with_line_info(e, context_info=context_info)
378
+ self._handle_exception_with_line_info(
379
+ e, context_info=context_info)
361
380
 
362
381
  def _eval_comparison_expr(self, expr_node):
363
382
  """
@@ -366,6 +385,7 @@ class DSLExecutor:
366
385
  :param expr_node: 比较表达式节点
367
386
  :return: 比较结果(布尔值)
368
387
  """
388
+ operator = "未知" # 设置默认值,避免UnboundLocalError
369
389
  try:
370
390
  left_value = self.eval_expression(expr_node.children[0])
371
391
  right_value = self.eval_expression(expr_node.children[1])
@@ -403,6 +423,7 @@ class DSLExecutor:
403
423
  :param expr_node: 算术表达式节点
404
424
  :return: 计算结果
405
425
  """
426
+ operator = "未知" # 设置默认值,避免UnboundLocalError
406
427
  try:
407
428
  left_value = self.eval_expression(expr_node.children[0])
408
429
  right_value = self.eval_expression(expr_node.children[1])
@@ -746,9 +767,15 @@ class DSLExecutor:
746
767
  name="赋值详情",
747
768
  attachment_type=allure.attachment_type.TEXT
748
769
  )
770
+
771
+ # 通知远程服务器变量已更新
772
+ self._notify_remote_servers_variable_changed(
773
+ var_name, expr_value)
774
+
749
775
  except Exception as e:
750
776
  # 在步骤内部记录异常详情
751
- error_details = f"执行Assignment节点: {str(e)}{line_info}\n上下文: 执行Assignment节点"
777
+ error_details = (f"执行Assignment节点: {str(e)}{line_info}\n"
778
+ f"上下文: 执行Assignment节点")
752
779
  allure.attach(
753
780
  error_details,
754
781
  name="DSL执行异常",
@@ -789,9 +816,15 @@ class DSLExecutor:
789
816
  name="关键字赋值详情",
790
817
  attachment_type=allure.attachment_type.TEXT
791
818
  )
819
+
820
+ # 通知远程服务器变量已更新
821
+ self._notify_remote_servers_variable_changed(var_name, result)
822
+
792
823
  except Exception as e:
793
824
  # 在步骤内部记录异常详情
794
- error_details = f"执行AssignmentKeywordCall节点: {str(e)}{line_info}\n上下文: 执行AssignmentKeywordCall节点"
825
+ error_details = (f"执行AssignmentKeywordCall节点: {str(e)}"
826
+ f"{line_info}\n"
827
+ f"上下文: 执行AssignmentKeywordCall节点")
795
828
  allure.attach(
796
829
  error_details,
797
830
  name="DSL执行异常",
@@ -800,6 +833,51 @@ class DSLExecutor:
800
833
  # 重新抛出异常,让外层的统一异常处理机制处理
801
834
  raise
802
835
 
836
+ def _notify_remote_servers_variable_changed(self, var_name, var_value):
837
+ """通知远程服务器变量已发生变化
838
+
839
+ Args:
840
+ var_name: 变量名
841
+ var_value: 变量值
842
+ """
843
+ try:
844
+ # 使用统一的序列化工具进行变量过滤
845
+ from .serialization_utils import XMLRPCSerializer
846
+
847
+ variables_to_filter = {var_name: var_value}
848
+ filtered_variables = XMLRPCSerializer.filter_variables(
849
+ variables_to_filter)
850
+
851
+ if not filtered_variables:
852
+ # 变量被过滤掉了(敏感变量或不可序列化)
853
+ return
854
+
855
+ # 导入远程关键字管理器
856
+ from pytest_dsl.remote.keyword_client import remote_keyword_manager
857
+
858
+ # 获取所有已连接的远程服务器客户端
859
+ for alias, client in remote_keyword_manager.clients.items():
860
+ try:
861
+ # 调用远程服务器的变量同步接口
862
+ result = client.server.sync_variables_from_client(
863
+ filtered_variables, client.api_key)
864
+
865
+ if result.get('status') == 'success':
866
+ print(f"🔄 变量 {var_name} 已同步到远程服务器 {alias}")
867
+ else:
868
+ error_msg = result.get('error', '未知错误')
869
+ print(f"❌ 变量 {var_name} 同步到远程服务器 {alias} "
870
+ f"失败: {error_msg}")
871
+
872
+ except Exception as e:
873
+ print(f"❌ 通知远程服务器 {alias} 变量变化失败: {str(e)}")
874
+
875
+ except ImportError:
876
+ # 如果没有导入远程模块,跳过通知
877
+ pass
878
+ except Exception as e:
879
+ print(f"❌ 通知远程服务器变量变化时发生错误: {str(e)}")
880
+
803
881
  def _handle_for_loop(self, node):
804
882
  """处理for循环"""
805
883
  step_name = f"执行循环: {node.value}"
@@ -828,6 +906,9 @@ class DSLExecutor:
828
906
  self.variable_replacer.local_variables[var_name] = i
829
907
  self.test_context.set(var_name, i)
830
908
 
909
+ # 通知远程服务器循环变量已更新
910
+ self._notify_remote_servers_variable_changed(var_name, i)
911
+
831
912
  with allure.step(f"循环轮次: {var_name} = {i}"):
832
913
  try:
833
914
  self.execute(node.children[2])
@@ -857,7 +938,9 @@ class DSLExecutor:
857
938
  raise e
858
939
  except Exception as e:
859
940
  # 在循环轮次内部记录异常详情
860
- error_details = f"循环执行异常 ({var_name} = {i}): {str(e)}{line_info}\n上下文: 执行ForLoop节点"
941
+ error_details = (f"循环执行异常 ({var_name} = {i}): "
942
+ f"{str(e)}{line_info}\n"
943
+ f"上下文: 执行ForLoop节点")
861
944
  allure.attach(
862
945
  error_details,
863
946
  name="DSL执行异常",
@@ -870,7 +953,8 @@ class DSLExecutor:
870
953
  raise
871
954
  except Exception as e:
872
955
  # 在步骤内部记录异常详情
873
- error_details = f"执行ForLoop节点: {str(e)}{line_info}\n上下文: 执行ForLoop节点"
956
+ error_details = (f"执行ForLoop节点: {str(e)}{line_info}\n"
957
+ f"上下文: 执行ForLoop节点")
874
958
  allure.attach(
875
959
  error_details,
876
960
  name="DSL执行异常",
@@ -891,7 +975,8 @@ class DSLExecutor:
891
975
  # 在步骤内部记录异常
892
976
  with allure.step(f"调用关键字: {keyword_name}"):
893
977
  allure.attach(
894
- f"执行KeywordCall节点: 未注册的关键字: {keyword_name}{line_info}\n上下文: 执行KeywordCall节点",
978
+ f"执行KeywordCall节点: 未注册的关键字: {keyword_name}"
979
+ f"{line_info}\n上下文: 执行KeywordCall节点",
895
980
  name="DSL执行异常",
896
981
  attachment_type=allure.attachment_type.TEXT
897
982
  )
@@ -903,7 +988,7 @@ class DSLExecutor:
903
988
  try:
904
989
  # 准备参数(这里可能抛出参数解析异常)
905
990
  kwargs = self._prepare_keyword_params(node, keyword_info)
906
-
991
+
907
992
  # 传递自定义步骤名称给KeywordManager,避免重复的allure步骤嵌套
908
993
  kwargs['step_name'] = keyword_name # 内层步骤只显示关键字名称
909
994
  # 避免KeywordManager重复记录,由DSL执行器统一记录
@@ -927,18 +1012,26 @@ class DSLExecutor:
927
1012
  if "参数解析异常" in core_error:
928
1013
  # 提取参数名和具体错误
929
1014
  import re
930
- match = re.search(r'参数解析异常 \(([^)]+)\): (.+)', core_error)
1015
+ match = re.search(
1016
+ r'参数解析异常 \(([^)]+)\): (.+)', core_error)
931
1017
  if match:
932
1018
  param_name, detailed_error = match.groups()
933
- error_details = f"参数解析失败 ({param_name}): {detailed_error}{line_info}\n上下文: 执行KeywordCall节点"
1019
+ error_details = (f"参数解析失败 ({param_name}): "
1020
+ f"{detailed_error}{line_info}\n"
1021
+ f"上下文: 执行KeywordCall节点")
934
1022
  else:
935
- error_details = f"参数解析失败: {core_error}{line_info}\n上下文: 执行KeywordCall节点"
1023
+ error_details = (f"参数解析失败: {core_error}"
1024
+ f"{line_info}\n"
1025
+ f"上下文: 执行KeywordCall节点")
936
1026
  else:
937
- error_details = f"参数解析失败: {core_error}{line_info}\n上下文: 执行KeywordCall节点"
1027
+ error_details = (f"参数解析失败: {core_error}"
1028
+ f"{line_info}\n"
1029
+ f"上下文: 执行KeywordCall节点")
938
1030
  else:
939
1031
  # 其他异常
940
- error_details = f"执行KeywordCall节点: {str(e)}{line_info}\n上下文: 执行KeywordCall节点"
941
-
1032
+ error_details = (f"执行KeywordCall节点: {str(e)}{line_info}\n"
1033
+ f"上下文: 执行KeywordCall节点")
1034
+
942
1035
  allure.attach(
943
1036
  error_details,
944
1037
  name="DSL执行异常",
@@ -951,21 +1044,20 @@ class DSLExecutor:
951
1044
  """准备关键字调用参数"""
952
1045
  mapping = keyword_info.get('mapping', {})
953
1046
  kwargs = {'context': self.test_context} # 默认传入context参数
954
- line_info = self._get_line_info(node)
955
1047
 
956
1048
  # 检查是否有参数列表
957
1049
  if node.children[0]:
958
1050
  for param in node.children[0]:
959
1051
  param_name = param.value
960
1052
  english_param_name = mapping.get(param_name, param_name)
961
-
1053
+
962
1054
  # 在子步骤中处理参数值解析,但不记录异常详情
963
1055
  with allure.step(f"解析参数: {param_name}"):
964
1056
  try:
965
1057
  # 对参数值进行变量替换
966
1058
  param_value = self.eval_expression(param.children[0])
967
1059
  kwargs[english_param_name] = param_value
968
-
1060
+
969
1061
  # 只记录参数解析成功的简要信息
970
1062
  allure.attach(
971
1063
  f"参数名: {param_name}\n"
@@ -975,7 +1067,8 @@ class DSLExecutor:
975
1067
  )
976
1068
  except Exception as e:
977
1069
  # 将异常重新包装,添加参数名信息,但不在这里记录到allure
978
- raise Exception(f"参数解析异常 ({param_name}): {str(e)}")
1070
+ raise Exception(
1071
+ f"参数解析异常 ({param_name}): {str(e)}")
979
1072
 
980
1073
  return kwargs
981
1074
 
@@ -1104,7 +1197,8 @@ class DSLExecutor:
1104
1197
  return result
1105
1198
  except Exception as e:
1106
1199
  # 在步骤内部记录异常详情
1107
- error_details = f"执行RemoteKeywordCall节点: {str(e)}{line_info}\n上下文: 执行RemoteKeywordCall节点"
1200
+ error_details = (f"执行RemoteKeywordCall节点: {str(e)}"
1201
+ f"{line_info}\n上下文: 执行RemoteKeywordCall节点")
1108
1202
  allure.attach(
1109
1203
  error_details,
1110
1204
  name="DSL执行异常",
@@ -1144,7 +1238,8 @@ class DSLExecutor:
1144
1238
  else:
1145
1239
  self.variable_replacer.local_variables[
1146
1240
  capture_var] = capture_value
1147
- self.test_context.set(capture_var, capture_value)
1241
+ self.test_context.set(
1242
+ capture_var, capture_value)
1148
1243
 
1149
1244
  # 将主要结果赋值给指定变量
1150
1245
  actual_result = main_result
@@ -1170,12 +1265,26 @@ class DSLExecutor:
1170
1265
  name="远程关键字赋值",
1171
1266
  attachment_type=allure.attachment_type.TEXT
1172
1267
  )
1268
+
1269
+ # 通知远程服务器变量已更新
1270
+ self._notify_remote_servers_variable_changed(
1271
+ var_name, actual_result)
1272
+
1273
+ # 同时处理captures中的变量同步
1274
+ if isinstance(result, dict) and 'captures' in result:
1275
+ captures = result.get('captures', {})
1276
+ for capture_var, capture_value in captures.items():
1277
+ # 通知远程服务器捕获的变量也已更新
1278
+ self._notify_remote_servers_variable_changed(
1279
+ capture_var, capture_value)
1173
1280
  else:
1174
1281
  error_msg = "远程关键字没有返回结果"
1175
1282
  raise Exception(error_msg)
1176
1283
  except Exception as e:
1177
1284
  # 在步骤内部记录异常详情
1178
- error_details = f"执行AssignmentRemoteKeywordCall节点: {str(e)}{line_info}\n上下文: 执行AssignmentRemoteKeywordCall节点"
1285
+ error_details = (f"执行AssignmentRemoteKeywordCall节点: {str(e)}"
1286
+ f"{line_info}\n"
1287
+ f"上下文: 执行AssignmentRemoteKeywordCall节点")
1179
1288
  allure.attach(
1180
1289
  error_details,
1181
1290
  name="DSL执行异常",
@@ -1249,7 +1358,8 @@ class DSLExecutor:
1249
1358
  self.execution_tracker.finish_current_step(error=error_msg)
1250
1359
 
1251
1360
  # 如果是控制流异常或已经是DSLExecutionError,直接重抛
1252
- if isinstance(e, (BreakException, ContinueException, ReturnException, DSLExecutionError)):
1361
+ if isinstance(e, (BreakException, ContinueException,
1362
+ ReturnException, DSLExecutionError)):
1253
1363
  raise
1254
1364
 
1255
1365
  # 如果是断言异常,保持原样但可能添加行号信息
@@ -1265,7 +1375,7 @@ class DSLExecutor:
1265
1375
  # 其他异常使用统一处理机制
1266
1376
  # 对于这些节点类型,异常已经在步骤中记录过了,跳过重复记录
1267
1377
  step_handled_nodes = {
1268
- 'KeywordCall', 'Assignment', 'AssignmentKeywordCall',
1378
+ 'KeywordCall', 'Assignment', 'AssignmentKeywordCall',
1269
1379
  'ForLoop', 'RemoteKeywordCall', 'AssignmentRemoteKeywordCall'
1270
1380
  }
1271
1381
  skip_logging = node.type in step_handled_nodes
@@ -1312,6 +1422,20 @@ class DSLExecutor:
1312
1422
  f"hooks_enabled={self.enable_hooks}, "
1313
1423
  f"tracking_enabled={self.enable_tracking})")
1314
1424
 
1425
+ def _setup_variable_providers(self):
1426
+ """设置变量提供者,将外部变量源注入到TestContext中"""
1427
+ try:
1428
+ from .variable_providers import (
1429
+ setup_context_with_default_providers
1430
+ )
1431
+ setup_context_with_default_providers(self.test_context)
1432
+
1433
+ # 同步常用变量到context中,提高访问性能
1434
+ self.test_context.sync_variables_from_external_sources()
1435
+ except ImportError as e:
1436
+ # 如果导入失败,记录警告但不影响正常功能
1437
+ print(f"警告:无法设置变量提供者: {e}")
1438
+
1315
1439
  def _init_hooks(self):
1316
1440
  """初始化hook机制"""
1317
1441
  try:
@@ -4,7 +4,6 @@ import tempfile
4
4
  import allure
5
5
  from typing import Dict, Any, Optional
6
6
  from filelock import FileLock
7
- from .yaml_vars import yaml_vars
8
7
 
9
8
 
10
9
  class GlobalContext:
@@ -19,6 +18,20 @@ class GlobalContext:
19
18
  self._storage_dir, "global_vars.json")
20
19
  self._lock_file = os.path.join(self._storage_dir, "global_vars.lock")
21
20
 
21
+ # 初始化变量提供者(延迟加载,避免循环导入)
22
+ self._yaml_provider = None
23
+
24
+ def _get_yaml_provider(self):
25
+ """延迟获取YAML变量提供者,避免循环导入"""
26
+ if self._yaml_provider is None:
27
+ try:
28
+ from .variable_providers import YAMLVariableProvider
29
+ self._yaml_provider = YAMLVariableProvider()
30
+ except ImportError:
31
+ # 如果变量提供者不可用,创建一个空的提供者
32
+ self._yaml_provider = _EmptyProvider()
33
+ return self._yaml_provider
34
+
22
35
  def set_variable(self, name: str, value: Any) -> None:
23
36
  """设置全局变量"""
24
37
  with FileLock(self._lock_file):
@@ -34,8 +47,9 @@ class GlobalContext:
34
47
 
35
48
  def get_variable(self, name: str) -> Any:
36
49
  """获取全局变量,优先从YAML变量中获取"""
37
- # 首先尝试从YAML变量中获取
38
- yaml_value = yaml_vars.get_variable(name)
50
+ # 首先尝试从YAML变量中获取(通过变量提供者)
51
+ yaml_provider = self._get_yaml_provider()
52
+ yaml_value = yaml_provider.get_variable(name)
39
53
  if yaml_value is not None:
40
54
  return yaml_value
41
55
 
@@ -46,8 +60,9 @@ class GlobalContext:
46
60
 
47
61
  def has_variable(self, name: str) -> bool:
48
62
  """检查全局变量是否存在(包括YAML变量)"""
49
- # 首先检查YAML变量
50
- if yaml_vars.get_variable(name) is not None:
63
+ # 首先检查YAML变量(通过变量提供者)
64
+ yaml_provider = self._get_yaml_provider()
65
+ if yaml_provider.has_variable(name):
51
66
  return True
52
67
 
53
68
  # 然后检查全局变量存储
@@ -73,9 +88,11 @@ class GlobalContext:
73
88
  """清除所有全局变量(包括YAML变量)"""
74
89
  with FileLock(self._lock_file):
75
90
  self._save_variables({})
76
-
77
- # 清除YAML变量
78
- yaml_vars.clear()
91
+
92
+ # 清除YAML变量(通过变量提供者)
93
+ yaml_provider = self._get_yaml_provider()
94
+ if hasattr(yaml_provider, 'clear'):
95
+ yaml_provider.clear()
79
96
 
80
97
  allure.attach(
81
98
  "清除所有全局变量",
@@ -99,5 +116,18 @@ class GlobalContext:
99
116
  json.dump(variables, f, ensure_ascii=False, indent=2)
100
117
 
101
118
 
119
+ class _EmptyProvider:
120
+ """空的变量提供者,用作后备方案"""
121
+
122
+ def get_variable(self, key: str) -> Optional[Any]:
123
+ return None
124
+
125
+ def has_variable(self, key: str) -> bool:
126
+ return False
127
+
128
+ def clear(self):
129
+ pass
130
+
131
+
102
132
  # 创建全局上下文管理器实例
103
133
  global_context = GlobalContext()
@@ -3,7 +3,6 @@ import logging
3
3
  from typing import Dict, Any
4
4
  import requests
5
5
  from urllib.parse import urljoin
6
- from pytest_dsl.core.yaml_vars import yaml_vars
7
6
  from pytest_dsl.core.auth_provider import create_auth_provider
8
7
 
9
8
  logger = logging.getLogger(__name__)
@@ -289,6 +288,31 @@ class HTTPClientManager:
289
288
  """初始化客户端管理器"""
290
289
  self._clients: Dict[str, HTTPClient] = {}
291
290
  self._sessions: Dict[str, HTTPClient] = {}
291
+ self._context = None # 添加context引用
292
+
293
+ def set_context(self, context):
294
+ """设置测试上下文,用于获取HTTP客户端配置
295
+
296
+ Args:
297
+ context: TestContext实例
298
+ """
299
+ self._context = context
300
+
301
+ def _get_http_clients_config(self) -> Dict[str, Any]:
302
+ """从context获取HTTP客户端配置
303
+
304
+ Returns:
305
+ HTTP客户端配置字典
306
+ """
307
+ if self._context:
308
+ return self._context.get("http_clients") or {}
309
+
310
+ # 如果没有context,尝试从yaml_vars获取(兼容性)
311
+ try:
312
+ from pytest_dsl.core.yaml_vars import yaml_vars
313
+ return yaml_vars.get_variable("http_clients") or {}
314
+ except ImportError:
315
+ return {}
292
316
 
293
317
  def create_client(self, config: Dict[str, Any]) -> HTTPClient:
294
318
  """从配置创建客户端
@@ -326,8 +350,8 @@ class HTTPClientManager:
326
350
  if name in self._clients:
327
351
  return self._clients[name]
328
352
 
329
- # 从YAML变量中读取客户端配置
330
- http_clients = yaml_vars.get_variable("http_clients") or {}
353
+ # 从context获取HTTP客户端配置(统一的变量获取方式)
354
+ http_clients = self._get_http_clients_config()
331
355
  client_config = http_clients.get(name)
332
356
 
333
357
  if not client_config:
@@ -377,7 +401,7 @@ class HTTPClientManager:
377
401
  return session
378
402
 
379
403
  def _get_client_config(self, name: str) -> Dict[str, Any]:
380
- """从YAML变量获取客户端配置
404
+ """从context获取客户端配置
381
405
 
382
406
  Args:
383
407
  name: 客户端名称
@@ -385,7 +409,7 @@ class HTTPClientManager:
385
409
  Returns:
386
410
  客户端配置
387
411
  """
388
- http_clients = yaml_vars.get_variable("http_clients") or {}
412
+ http_clients = self._get_http_clients_config()
389
413
  client_config = http_clients.get(name)
390
414
 
391
415
  if not client_config and name == "default":
@@ -210,6 +210,13 @@ class KeywordFormatter:
210
210
  'parameters': keyword_info.parameters
211
211
  }
212
212
 
213
+ # 添加来源字段,优先显示项目自定义关键字的文件位置
214
+ if keyword_info.file_location:
215
+ keyword_data['source'] = keyword_info.file_location
216
+ else:
217
+ keyword_data['source'] = keyword_info.source_info.get(
218
+ 'display_name', keyword_info.source_info.get('name', '未知'))
219
+
213
220
  # 远程关键字特殊信息
214
221
  if keyword_info.remote_info:
215
222
  keyword_data['remote'] = keyword_info.remote_info