pytest-dsl 0.15.3__py3-none-any.whl → 0.15.4__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/__init__.py CHANGED
@@ -12,7 +12,7 @@ pytest-dsl - 基于pytest的DSL测试框架
12
12
  - 自定义关键字支持
13
13
  """
14
14
 
15
- __version__ = "0.1.0"
15
+ __version__ = "0.15.4"
16
16
 
17
17
  # 核心执行器
18
18
  from pytest_dsl.core.dsl_executor import DSLExecutor
@@ -47,6 +47,20 @@ from pytest_dsl.core.variable_utils import VariableReplacer
47
47
  # 自定义关键字管理器
48
48
  from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
49
49
 
50
+ # 远程服务器注册器
51
+ from pytest_dsl.core.remote_server_registry import (
52
+ remote_server_registry, RemoteServerRegistry,
53
+ register_remote_server_with_variables,
54
+ create_database_variable_provider,
55
+ create_config_file_variable_provider
56
+ )
57
+
58
+ # 远程服务器配置加载器
59
+ from pytest_dsl.core.yaml_loader import (
60
+ load_remote_servers_from_yaml,
61
+ register_remote_servers_from_config
62
+ )
63
+
50
64
  # 关键字加载器
51
65
  from pytest_dsl.core.keyword_loader import (
52
66
  keyword_loader, KeywordLoader,
@@ -61,6 +75,22 @@ from pytest_dsl.core.keyword_utils import (
61
75
  generate_html_report
62
76
  )
63
77
 
78
+ # 远程关键字功能
79
+ try:
80
+ from pytest_dsl.remote import (
81
+ remote_keyword_manager, RemoteKeywordManager, RemoteKeywordClient,
82
+ register_remote_server, register_multiple_servers
83
+ )
84
+ _REMOTE_AVAILABLE = True
85
+ except ImportError:
86
+ # 如果远程功能依赖不可用,设置为None
87
+ remote_keyword_manager = None
88
+ RemoteKeywordManager = None
89
+ RemoteKeywordClient = None
90
+ register_remote_server = None
91
+ register_multiple_servers = None
92
+ _REMOTE_AVAILABLE = False
93
+
64
94
  # 便捷导入的别名
65
95
  Executor = DSLExecutor
66
96
  Validator = DSLValidator
@@ -106,6 +136,17 @@ __all__ = [
106
136
  'Node', 'get_parser', 'get_lexer',
107
137
  'TestContext', 'global_context',
108
138
  'VariableReplacer',
139
+
140
+ # 远程关键字功能(如果可用)
141
+ 'remote_keyword_manager', 'RemoteKeywordManager', 'RemoteKeywordClient',
142
+ 'register_remote_server', 'register_multiple_servers',
143
+
144
+ # 远程服务器注册器
145
+ 'remote_server_registry', 'RemoteServerRegistry',
146
+ 'register_remote_server_with_variables',
147
+ 'create_database_variable_provider',
148
+ 'create_config_file_variable_provider',
149
+ 'load_remote_servers_from_yaml', 'register_remote_servers_from_config',
109
150
  ]
110
151
 
111
152
  # 快捷函数
@@ -137,7 +178,8 @@ def parse_dsl(content: str) -> Node:
137
178
  return parser.parse(content, lexer=lexer)
138
179
 
139
180
 
140
- def execute_dsl(content: str, context: dict = None, enable_hooks: bool = True) -> any:
181
+ def execute_dsl(content: str, context: dict = None,
182
+ enable_hooks: bool = True) -> any:
141
183
  """执行DSL内容的便捷函数
142
184
 
143
185
  Args:
@@ -158,7 +200,8 @@ def execute_dsl(content: str, context: dict = None, enable_hooks: bool = True) -
158
200
  return executor.execute(ast)
159
201
 
160
202
 
161
- def register_keyword(name: str, parameters: list = None, source_type: str = "external",
203
+ def register_keyword(name: str, parameters: list = None,
204
+ source_type: str = "external",
162
205
  source_name: str = "user_defined"):
163
206
  """注册关键字的装饰器
164
207
 
@@ -198,6 +241,12 @@ def check_version_compatibility():
198
241
  pass
199
242
 
200
243
 
244
+ # 远程功能检查
245
+ def is_remote_available() -> bool:
246
+ """检查远程功能是否可用"""
247
+ return _REMOTE_AVAILABLE
248
+
249
+
201
250
  # 初始化时进行版本检查
202
251
  check_version_compatibility()
203
252
 
pytest_dsl/cli.py CHANGED
@@ -165,11 +165,18 @@ def load_yaml_variables(args):
165
165
  environment = (os.environ.get('PYTEST_DSL_ENVIRONMENT') or
166
166
  os.environ.get('ENVIRONMENT'))
167
167
 
168
+ # 智能判断是否应该加载默认配置
169
+ # 如果用户指定了YAML文件或目录,则不自动加载默认配置
170
+ user_specified_files = bool(args.yaml_vars)
171
+ user_specified_dir = bool(args.yaml_vars_dir)
172
+ auto_load_default = not (user_specified_files or user_specified_dir)
173
+
168
174
  load_yaml_variables_from_args(
169
175
  yaml_files=args.yaml_vars,
170
176
  yaml_vars_dir=args.yaml_vars_dir,
171
177
  project_root=os.getcwd(), # CLI模式下使用当前工作目录作为项目根目录
172
- environment=environment
178
+ environment=environment,
179
+ auto_load_default=auto_load_default # 使用智能判断的结果
173
180
  )
174
181
  except Exception as e:
175
182
  print(f"加载YAML变量失败: {str(e)}")
@@ -1,18 +1,42 @@
1
1
  class TestContext:
2
2
  def __init__(self):
3
3
  self._data = {}
4
+ self._external_providers = [] # 外部变量提供者列表
4
5
 
5
6
  def set(self, key: str, value: any) -> None:
6
7
  """设置上下文变量"""
7
8
  self._data[key] = value
8
9
 
9
10
  def get(self, key: str, default=None) -> any:
10
- """获取上下文变量,如果不存在返回默认值"""
11
- return self._data.get(key, default)
11
+ """获取上下文变量,遵循变量优先级:本地变量 > 外部提供者变量"""
12
+ # 1. 首先检查本地变量
13
+ if key in self._data:
14
+ return self._data[key]
15
+
16
+ # 2. 检查外部提供者(按注册顺序)
17
+ for provider in self._external_providers:
18
+ if hasattr(provider, 'get_variable'):
19
+ value = provider.get_variable(key)
20
+ if value is not None:
21
+ return value
22
+
23
+ # 3. 返回默认值
24
+ return default
12
25
 
13
26
  def has(self, key: str) -> bool:
14
- """检查上下文变量是否存在"""
15
- return key in self._data
27
+ """检查上下文变量是否存在(包括外部提供者)"""
28
+ # 检查本地变量
29
+ if key in self._data:
30
+ return True
31
+
32
+ # 检查外部提供者
33
+ for provider in self._external_providers:
34
+ if hasattr(provider, 'get_variable'):
35
+ value = provider.get_variable(key)
36
+ if value is not None:
37
+ return True
38
+
39
+ return False
16
40
 
17
41
  def clear(self) -> None:
18
42
  """清空上下文"""
@@ -20,4 +44,34 @@ class TestContext:
20
44
 
21
45
  def get_local_variables(self) -> dict:
22
46
  """获取所有本地变量"""
23
- return self._data
47
+ return self._data
48
+
49
+ def register_external_variable_provider(self, provider) -> None:
50
+ """注册外部变量提供者
51
+
52
+ Args:
53
+ provider: 变量提供者,需要实现get_variable(key)方法
54
+ """
55
+ if provider not in self._external_providers:
56
+ self._external_providers.append(provider)
57
+
58
+ def sync_variables_from_external_sources(self) -> None:
59
+ """将外部变量提供者中的常用变量同步到本地缓存中,提高访问性能
60
+
61
+ 这个方法会调用所有外部提供者的get_all_variables方法,
62
+ 将常用变量缓存到本地_data字典中,以提高后续访问的性能。
63
+ 注意:本地变量的优先级仍然高于外部变量。
64
+ """
65
+ for provider in self._external_providers:
66
+ if hasattr(provider, 'get_all_variables'):
67
+ try:
68
+ # 获取提供者的所有变量
69
+ external_vars = provider.get_all_variables()
70
+ if isinstance(external_vars, dict):
71
+ # 只同步那些本地还没有的变量,保持本地变量的优先级
72
+ for key, value in external_vars.items():
73
+ if key not in self._data:
74
+ self._data[key] = value
75
+ except Exception as e:
76
+ # 如果某个提供者同步失败,记录警告但继续处理其他提供者
77
+ print(f"警告:同步外部变量提供者变量时发生错误: {e}")
@@ -212,7 +212,7 @@ class CustomKeywordManager:
212
212
  print(f"资源文件 {file_path} 加载失败: {str(e)}")
213
213
  raise
214
214
 
215
- def _process_resource_file_content(self, content: str,
215
+ def _process_resource_file_content(self, content: str,
216
216
  file_path: str) -> None:
217
217
  """处理资源文件内容
218
218
 
@@ -258,7 +258,7 @@ class CustomKeywordManager:
258
258
  # 递归加载导入的资源文件
259
259
  self.load_resource_file(imported_file)
260
260
 
261
- def _register_keywords_from_ast(self, ast: Node,
261
+ def _register_keywords_from_ast(self, ast: Node,
262
262
  source_name: str) -> None:
263
263
  """从AST中注册关键字(重构后的版本)
264
264
 
@@ -341,6 +341,12 @@ class CustomKeywordManager:
341
341
  executor.test_context.set(
342
342
  param_name, kwargs[param_mapping_name])
343
343
 
344
+ # 重要:创建变量替换器,使变量解析正常工作
345
+ from pytest_dsl.core.variable_utils import VariableReplacer
346
+ executor.variable_replacer = VariableReplacer(
347
+ executor.variables, executor.test_context
348
+ )
349
+
344
350
  # 执行关键字体中的语句
345
351
  result = None
346
352
  try:
@@ -357,7 +363,7 @@ class CustomKeywordManager:
357
363
 
358
364
  print(f"已注册自定义关键字: {keyword_name} 来自文件: {file_path}")
359
365
 
360
- def register_keyword_from_dsl_content(self, dsl_content: str,
366
+ def register_keyword_from_dsl_content(self, dsl_content: str,
361
367
  source_name: str = "DSL内容") -> list:
362
368
  """从DSL内容注册关键字(公共方法)
363
369
 
@@ -379,8 +385,8 @@ class CustomKeywordManager:
379
385
 
380
386
  # 收集注册前的关键字列表
381
387
  existing_keywords = (
382
- set(keyword_manager._keywords.keys())
383
- if hasattr(keyword_manager, '_keywords')
388
+ set(keyword_manager._keywords.keys())
389
+ if hasattr(keyword_manager, '_keywords')
384
390
  else set()
385
391
  )
386
392
 
@@ -389,8 +395,8 @@ class CustomKeywordManager:
389
395
 
390
396
  # 计算新注册的关键字
391
397
  new_keywords = (
392
- set(keyword_manager._keywords.keys())
393
- if hasattr(keyword_manager, '_keywords')
398
+ set(keyword_manager._keywords.keys())
399
+ if hasattr(keyword_manager, '_keywords')
394
400
  else set()
395
401
  )
396
402
  registered_keywords = list(new_keywords - existing_keywords)
@@ -405,7 +411,7 @@ class CustomKeywordManager:
405
411
  raise
406
412
 
407
413
  def register_specific_keyword_from_dsl_content(
408
- self, keyword_name: str, dsl_content: str,
414
+ self, keyword_name: str, dsl_content: str,
409
415
  source_name: str = "DSL内容") -> bool:
410
416
  """从DSL内容注册指定的关键字(公共方法)
411
417
 
@@ -72,6 +72,10 @@ class DSLExecutor:
72
72
  self.variables = {}
73
73
  self.test_context = TestContext()
74
74
  self.test_context.executor = self # 让 test_context 能够访问到 executor
75
+
76
+ # 设置变量提供者,实现YAML变量等外部变量源的注入
77
+ self._setup_variable_providers()
78
+
75
79
  self.variable_replacer = VariableReplacer(
76
80
  self.variables, self.test_context)
77
81
  self.imported_files = set() # 跟踪已导入的文件,避免循环导入
@@ -348,7 +352,7 @@ class DSLExecutor:
348
352
  # 对于不包含 ${} 的普通字符串,检查是否为单纯的变量名
349
353
  # 只有当字符串是有效的变量名格式且确实存在该变量时,才当作变量处理
350
354
  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):
355
+ value in self.variable_replacer.local_variables):
352
356
  return self.variable_replacer.local_variables[value]
353
357
  else:
354
358
  # 否则当作字符串字面量处理
@@ -366,6 +370,7 @@ class DSLExecutor:
366
370
  :param expr_node: 比较表达式节点
367
371
  :return: 比较结果(布尔值)
368
372
  """
373
+ operator = "未知" # 设置默认值,避免UnboundLocalError
369
374
  try:
370
375
  left_value = self.eval_expression(expr_node.children[0])
371
376
  right_value = self.eval_expression(expr_node.children[1])
@@ -403,6 +408,7 @@ class DSLExecutor:
403
408
  :param expr_node: 算术表达式节点
404
409
  :return: 计算结果
405
410
  """
411
+ operator = "未知" # 设置默认值,避免UnboundLocalError
406
412
  try:
407
413
  left_value = self.eval_expression(expr_node.children[0])
408
414
  right_value = self.eval_expression(expr_node.children[1])
@@ -903,7 +909,7 @@ class DSLExecutor:
903
909
  try:
904
910
  # 准备参数(这里可能抛出参数解析异常)
905
911
  kwargs = self._prepare_keyword_params(node, keyword_info)
906
-
912
+
907
913
  # 传递自定义步骤名称给KeywordManager,避免重复的allure步骤嵌套
908
914
  kwargs['step_name'] = keyword_name # 内层步骤只显示关键字名称
909
915
  # 避免KeywordManager重复记录,由DSL执行器统一记录
@@ -927,7 +933,8 @@ class DSLExecutor:
927
933
  if "参数解析异常" in core_error:
928
934
  # 提取参数名和具体错误
929
935
  import re
930
- match = re.search(r'参数解析异常 \(([^)]+)\): (.+)', core_error)
936
+ match = re.search(
937
+ r'参数解析异常 \(([^)]+)\): (.+)', core_error)
931
938
  if match:
932
939
  param_name, detailed_error = match.groups()
933
940
  error_details = f"参数解析失败 ({param_name}): {detailed_error}{line_info}\n上下文: 执行KeywordCall节点"
@@ -938,7 +945,7 @@ class DSLExecutor:
938
945
  else:
939
946
  # 其他异常
940
947
  error_details = f"执行KeywordCall节点: {str(e)}{line_info}\n上下文: 执行KeywordCall节点"
941
-
948
+
942
949
  allure.attach(
943
950
  error_details,
944
951
  name="DSL执行异常",
@@ -958,14 +965,14 @@ class DSLExecutor:
958
965
  for param in node.children[0]:
959
966
  param_name = param.value
960
967
  english_param_name = mapping.get(param_name, param_name)
961
-
968
+
962
969
  # 在子步骤中处理参数值解析,但不记录异常详情
963
970
  with allure.step(f"解析参数: {param_name}"):
964
971
  try:
965
972
  # 对参数值进行变量替换
966
973
  param_value = self.eval_expression(param.children[0])
967
974
  kwargs[english_param_name] = param_value
968
-
975
+
969
976
  # 只记录参数解析成功的简要信息
970
977
  allure.attach(
971
978
  f"参数名: {param_name}\n"
@@ -1144,7 +1151,8 @@ class DSLExecutor:
1144
1151
  else:
1145
1152
  self.variable_replacer.local_variables[
1146
1153
  capture_var] = capture_value
1147
- self.test_context.set(capture_var, capture_value)
1154
+ self.test_context.set(
1155
+ capture_var, capture_value)
1148
1156
 
1149
1157
  # 将主要结果赋值给指定变量
1150
1158
  actual_result = main_result
@@ -1265,7 +1273,7 @@ class DSLExecutor:
1265
1273
  # 其他异常使用统一处理机制
1266
1274
  # 对于这些节点类型,异常已经在步骤中记录过了,跳过重复记录
1267
1275
  step_handled_nodes = {
1268
- 'KeywordCall', 'Assignment', 'AssignmentKeywordCall',
1276
+ 'KeywordCall', 'Assignment', 'AssignmentKeywordCall',
1269
1277
  'ForLoop', 'RemoteKeywordCall', 'AssignmentRemoteKeywordCall'
1270
1278
  }
1271
1279
  skip_logging = node.type in step_handled_nodes
@@ -1312,6 +1320,18 @@ class DSLExecutor:
1312
1320
  f"hooks_enabled={self.enable_hooks}, "
1313
1321
  f"tracking_enabled={self.enable_tracking})")
1314
1322
 
1323
+ def _setup_variable_providers(self):
1324
+ """设置变量提供者,将外部变量源注入到TestContext中"""
1325
+ try:
1326
+ from .variable_providers import setup_context_with_default_providers
1327
+ setup_context_with_default_providers(self.test_context)
1328
+
1329
+ # 同步常用变量到context中,提高访问性能
1330
+ self.test_context.sync_variables_from_external_sources()
1331
+ except ImportError as e:
1332
+ # 如果导入失败,记录警告但不影响正常功能
1333
+ print(f"警告:无法设置变量提供者: {e}")
1334
+
1315
1335
  def _init_hooks(self):
1316
1336
  """初始化hook机制"""
1317
1337
  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