pytest-dsl 0.15.2__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.
@@ -0,0 +1,333 @@
1
+ """
2
+ 远程服务器注册模块
3
+
4
+ 提供独立的远程服务器注册功能,方便其他系统集成pytest-dsl时使用自己的变量系统。
5
+ 这个模块不依赖于YAML配置,完全通过编程方式进行服务器注册。
6
+ """
7
+
8
+ from typing import Dict, List, Optional, Any, Callable
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class RemoteServerRegistry:
15
+ """远程服务器注册器
16
+
17
+ 提供灵活的API用于注册和管理远程关键字服务器,
18
+ 支持自定义变量获取方式,方便第三方系统集成。
19
+ """
20
+
21
+ def __init__(self):
22
+ self._variable_providers = [] # 变量提供者列表
23
+ self._server_configs = [] # 服务器配置列表
24
+ self._connection_callbacks = [] # 连接成功后的回调
25
+
26
+ def add_variable_provider(self, provider: Callable[[], Dict[str, Any]]):
27
+ """添加变量提供者
28
+
29
+ 变量提供者是一个无参数的可调用对象,返回字典形式的变量。
30
+ 这允许第三方系统提供自己的变量获取逻辑。
31
+
32
+ Args:
33
+ provider: 返回变量字典的可调用对象
34
+
35
+ Examples:
36
+ >>> def my_vars():
37
+ ... return {'api_key': 'secret', 'env': 'prod'}
38
+ >>> registry.add_variable_provider(my_vars)
39
+ """
40
+ if callable(provider):
41
+ self._variable_providers.append(provider)
42
+ else:
43
+ raise ValueError("变量提供者必须是可调用对象")
44
+
45
+ def add_connection_callback(self, callback: Callable[[str, bool], None]):
46
+ """添加连接回调
47
+
48
+ 连接回调会在每次连接远程服务器后被调用,
49
+ 无论连接成功还是失败。
50
+
51
+ Args:
52
+ callback: 接受(alias, success)参数的回调函数
53
+ """
54
+ if callable(callback):
55
+ self._connection_callbacks.append(callback)
56
+ else:
57
+ raise ValueError("连接回调必须是可调用对象")
58
+
59
+ def register_server(self,
60
+ url: str,
61
+ alias: str,
62
+ api_key: Optional[str] = None,
63
+ sync_global_vars: bool = True,
64
+ sync_custom_vars: bool = True,
65
+ exclude_patterns: Optional[List[str]] = None) -> bool:
66
+ """注册单个远程服务器
67
+
68
+ Args:
69
+ url: 服务器URL
70
+ alias: 服务器别名
71
+ api_key: API密钥
72
+ sync_global_vars: 是否同步全局变量
73
+ sync_custom_vars: 是否同步自定义变量(通过变量提供者)
74
+ exclude_patterns: 要排除的变量名模式列表
75
+
76
+ Returns:
77
+ bool: 是否连接成功
78
+ """
79
+ # 构建同步配置
80
+ sync_config = {
81
+ 'sync_global_vars': sync_global_vars,
82
+ 'sync_yaml_vars': False, # 不使用YAML变量
83
+ 'sync_custom_vars': sync_custom_vars,
84
+ 'exclude_patterns': exclude_patterns or ['password', 'secret', 'token']
85
+ }
86
+
87
+ # 收集要同步的变量
88
+ variables_to_sync = {}
89
+
90
+ if sync_custom_vars:
91
+ variables_to_sync.update(self._collect_custom_variables())
92
+
93
+ # 尝试连接
94
+ success = self._connect_to_server(
95
+ url, alias, api_key, sync_config, variables_to_sync)
96
+
97
+ # 调用连接回调
98
+ for callback in self._connection_callbacks:
99
+ try:
100
+ callback(alias, success)
101
+ except Exception as e:
102
+ logger.warning(f"连接回调执行失败: {e}")
103
+
104
+ if success:
105
+ # 保存配置以便后续使用
106
+ self._server_configs.append({
107
+ 'url': url,
108
+ 'alias': alias,
109
+ 'api_key': api_key,
110
+ 'sync_config': sync_config
111
+ })
112
+
113
+ return success
114
+
115
+ def register_servers_from_config(self, servers: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
116
+ """从配置列表批量注册服务器
117
+
118
+ Args:
119
+ servers: 服务器配置列表,每个配置包含url、alias等字段
120
+
121
+ Returns:
122
+ dict: 注册结果,键为alias,值为结果详情字典
123
+
124
+ Examples:
125
+ >>> servers = [
126
+ ... {'url': 'http://server1:8270', 'alias': 'server1'},
127
+ ... {'url': 'http://server2:8270', 'alias': 'server2', 'api_key': 'secret'}
128
+ ... ]
129
+ >>> results = registry.register_servers_from_config(servers)
130
+ """
131
+ results = {}
132
+
133
+ for server_config in servers:
134
+ if not isinstance(server_config, dict):
135
+ continue
136
+
137
+ url = server_config.get('url')
138
+ alias = server_config.get('alias')
139
+
140
+ if not url or not alias:
141
+ logger.warning(f"服务器配置缺少必要字段: {server_config}")
142
+ results[alias or 'unknown'] = {
143
+ 'success': False,
144
+ 'url': url or 'unknown',
145
+ 'alias': alias or 'unknown',
146
+ 'error': '缺少必要字段'
147
+ }
148
+ continue
149
+
150
+ api_key = server_config.get('api_key')
151
+ sync_global_vars = server_config.get('sync_global_vars', True)
152
+ sync_custom_vars = server_config.get('sync_custom_vars', True)
153
+ exclude_patterns = server_config.get('exclude_patterns')
154
+
155
+ success = self.register_server(
156
+ url=url,
157
+ alias=alias,
158
+ api_key=api_key,
159
+ sync_global_vars=sync_global_vars,
160
+ sync_custom_vars=sync_custom_vars,
161
+ exclude_patterns=exclude_patterns
162
+ )
163
+
164
+ results[alias] = {
165
+ 'success': success,
166
+ 'url': url,
167
+ 'alias': alias
168
+ }
169
+
170
+ return results
171
+
172
+ def _collect_custom_variables(self) -> Dict[str, Any]:
173
+ """收集自定义变量"""
174
+ variables = {}
175
+
176
+ for provider in self._variable_providers:
177
+ try:
178
+ provider_vars = provider()
179
+ if isinstance(provider_vars, dict):
180
+ variables.update(provider_vars)
181
+ else:
182
+ logger.warning(f"变量提供者返回了非字典类型: {type(provider_vars)}")
183
+ except Exception as e:
184
+ logger.warning(f"变量提供者执行失败: {e}")
185
+
186
+ return variables
187
+
188
+ def _connect_to_server(self,
189
+ url: str,
190
+ alias: str,
191
+ api_key: Optional[str],
192
+ sync_config: Dict[str, Any],
193
+ variables: Dict[str, Any]) -> bool:
194
+ """连接到远程服务器"""
195
+ try:
196
+ # 导入远程关键字管理器
197
+ from pytest_dsl.remote import remote_keyword_manager
198
+
199
+ # 创建扩展的同步配置
200
+ extended_sync_config = sync_config.copy()
201
+ extended_sync_config['custom_variables'] = variables
202
+
203
+ # 注册服务器
204
+ success = remote_keyword_manager.register_remote_server(
205
+ url=url,
206
+ alias=alias,
207
+ api_key=api_key,
208
+ sync_config=extended_sync_config
209
+ )
210
+
211
+ if success:
212
+ logger.info(f"成功连接到远程服务器: {alias} ({url})")
213
+ else:
214
+ logger.error(f"连接远程服务器失败: {alias} ({url})")
215
+
216
+ return success
217
+
218
+ except ImportError:
219
+ logger.error("远程功能不可用,请检查依赖安装")
220
+ return False
221
+ except Exception as e:
222
+ logger.error(f"连接远程服务器时发生错误: {e}")
223
+ return False
224
+
225
+ def get_registered_servers(self) -> List[Dict[str, Any]]:
226
+ """获取已注册的服务器列表"""
227
+ return self._server_configs.copy()
228
+
229
+ def clear_variable_providers(self):
230
+ """清空所有变量提供者"""
231
+ self._variable_providers.clear()
232
+
233
+ def clear_connection_callbacks(self):
234
+ """清空所有连接回调"""
235
+ self._connection_callbacks.clear()
236
+
237
+
238
+ # 创建全局注册器实例
239
+ remote_server_registry = RemoteServerRegistry()
240
+
241
+
242
+ # 便捷函数
243
+ def register_remote_server_with_variables(url: str,
244
+ alias: str,
245
+ variables: Dict[str, Any],
246
+ api_key: Optional[str] = None) -> bool:
247
+ """使用指定变量注册远程服务器的便捷函数
248
+
249
+ Args:
250
+ url: 服务器URL
251
+ alias: 服务器别名
252
+ variables: 要同步的变量字典
253
+ api_key: API密钥
254
+
255
+ Returns:
256
+ bool: 是否连接成功
257
+ """
258
+ # 创建临时变量提供者
259
+ def temp_provider():
260
+ return variables
261
+
262
+ # 临时添加变量提供者
263
+ original_providers = remote_server_registry._variable_providers.copy()
264
+ remote_server_registry._variable_providers = [temp_provider]
265
+
266
+ try:
267
+ return remote_server_registry.register_server(url, alias, api_key)
268
+ finally:
269
+ # 恢复原来的变量提供者
270
+ remote_server_registry._variable_providers = original_providers
271
+
272
+
273
+ def create_database_variable_provider(connection_string: str):
274
+ """创建数据库变量提供者示例
275
+
276
+ 这是一个示例函数,展示如何创建从数据库获取变量的提供者。
277
+ 实际使用时需要根据具体的数据库类型进行调整。
278
+
279
+ Args:
280
+ connection_string: 数据库连接字符串
281
+
282
+ Returns:
283
+ callable: 变量提供者函数
284
+ """
285
+ def database_provider():
286
+ # 这里是示例代码,实际需要根据数据库类型实现
287
+ # import sqlite3
288
+ # conn = sqlite3.connect(connection_string)
289
+ # cursor = conn.cursor()
290
+ # cursor.execute("SELECT key, value FROM variables")
291
+ # variables = dict(cursor.fetchall())
292
+ # conn.close()
293
+ # return variables
294
+
295
+ return {
296
+ 'db_host': 'localhost',
297
+ 'db_port': '5432',
298
+ 'db_name': 'test_db'
299
+ }
300
+
301
+ return database_provider
302
+
303
+
304
+ def create_config_file_variable_provider(config_file_path: str):
305
+ """创建配置文件变量提供者
306
+
307
+ 从JSON或其他配置文件读取变量。
308
+
309
+ Args:
310
+ config_file_path: 配置文件路径
311
+
312
+ Returns:
313
+ callable: 变量提供者函数
314
+ """
315
+ import json
316
+ import os
317
+
318
+ def config_file_provider():
319
+ if not os.path.exists(config_file_path):
320
+ return {}
321
+
322
+ try:
323
+ with open(config_file_path, 'r', encoding='utf-8') as f:
324
+ if config_file_path.endswith('.json'):
325
+ return json.load(f)
326
+ else:
327
+ # 可以扩展支持其他格式
328
+ return {}
329
+ except Exception as e:
330
+ logger.warning(f"读取配置文件失败: {e}")
331
+ return {}
332
+
333
+ return config_file_provider
pytest_dsl/core/utils.py CHANGED
@@ -69,7 +69,8 @@ def _legacy_replace_variables_in_string(value: str) -> str:
69
69
  break
70
70
 
71
71
  # 替换变量引用
72
- value = value[:match.start()] + str(var_value) + value[match.end():]
72
+ value = value[:match.start()] + str(var_value) + \
73
+ value[match.end():]
73
74
 
74
75
  # 再处理基本引用
75
76
  matches = list(re.finditer(basic_pattern, value))
@@ -77,7 +78,8 @@ def _legacy_replace_variables_in_string(value: str) -> str:
77
78
  var_name = match.group(1)
78
79
  if context_has_variable(var_name):
79
80
  var_value = get_variable(var_name)
80
- value = value[:match.start()] + str(var_value) + value[match.end():]
81
+ value = value[:match.start()] + str(var_value) + \
82
+ value[match.end():]
81
83
 
82
84
  return value
83
85
 
@@ -102,37 +104,42 @@ def replace_variables_in_dict(data: Union[Dict, List, str]) -> Union[Dict, List,
102
104
 
103
105
 
104
106
  def context_has_variable(var_name: str) -> bool:
105
- """检查变量是否存在于全局上下文"""
106
- # 先检查YAML变量
107
- from pytest_dsl.core.yaml_vars import yaml_vars
108
- if yaml_vars.get_variable(var_name) is not None:
109
- return True
107
+ """检查变量是否存在于上下文中
110
108
 
109
+ 检查顺序:
110
+ 1. 测试上下文
111
+ 2. 全局上下文(包含YAML变量)
112
+ """
111
113
  # 检查测试上下文
112
- from pytest_dsl.core.keyword_manager import keyword_manager
113
- current_context = getattr(keyword_manager, 'current_context', None)
114
- if current_context and current_context.has(var_name):
115
- return True
116
-
117
- # 再检查全局上下文
114
+ try:
115
+ from pytest_dsl.core.keyword_manager import keyword_manager
116
+ current_context = getattr(keyword_manager, 'current_context', None)
117
+ if current_context and current_context.has(var_name):
118
+ return True
119
+ except ImportError:
120
+ pass
121
+
122
+ # 检查全局上下文(包含YAML变量的统一访问)
118
123
  return global_context.has_variable(var_name)
119
124
 
120
125
 
121
126
  def get_variable(var_name: str) -> Any:
122
- """获取变量值,先从YAML变量中获取,再从全局上下文获取"""
123
- # 先从YAML变量中获取
124
- from pytest_dsl.core.yaml_vars import yaml_vars
125
- yaml_value = yaml_vars.get_variable(var_name)
126
- if yaml_value is not None:
127
- return yaml_value
127
+ """获取变量值
128
128
 
129
- # 检查测试上下文
130
- from pytest_dsl.core.keyword_manager import keyword_manager
131
- current_context = getattr(keyword_manager, 'current_context', None)
132
- if current_context and current_context.has(var_name):
133
- return current_context.get(var_name)
134
-
135
- # 再从全局上下文获取
129
+ 获取顺序:
130
+ 1. 测试上下文
131
+ 2. 全局上下文(包含YAML变量)
132
+ """
133
+ # 先从测试上下文获取
134
+ try:
135
+ from pytest_dsl.core.keyword_manager import keyword_manager
136
+ current_context = getattr(keyword_manager, 'current_context', None)
137
+ if current_context and current_context.has(var_name):
138
+ return current_context.get(var_name)
139
+ except ImportError:
140
+ pass
141
+
142
+ # 再从全局上下文获取(包含对YAML变量的统一访问)
136
143
  if global_context.has_variable(var_name):
137
144
  return global_context.get_variable(var_name)
138
145
 
@@ -163,4 +170,4 @@ def deep_merge(base: Dict, override: Dict) -> Dict:
163
170
  # 覆盖或添加值
164
171
  result[key] = value
165
172
 
166
- return result
173
+ return result
@@ -47,6 +47,7 @@ class DSLValidator:
47
47
  def __init__(self):
48
48
  self.errors: List[DSLValidationError] = []
49
49
  self.warnings: List[DSLValidationError] = []
50
+ self._temp_registered_keywords = [] # 记录临时注册的关键字,用于清理
50
51
 
51
52
  def validate(self, content: str, dsl_id: Optional[str] = None
52
53
  ) -> Tuple[bool, List[DSLValidationError]]:
@@ -61,6 +62,7 @@ class DSLValidator:
61
62
  """
62
63
  self.errors = []
63
64
  self.warnings = []
65
+ self._temp_registered_keywords = []
64
66
 
65
67
  # 基础验证
66
68
  self._validate_basic_format(content)
@@ -68,8 +70,12 @@ class DSLValidator:
68
70
  # 语法验证
69
71
  ast = self._validate_syntax(content)
70
72
 
71
- # 如果语法验证通过,进行语义验证
73
+ # 如果语法验证通过,进行预处理和语义验证
72
74
  if ast and not self.errors:
75
+ # 预注册自定义关键字
76
+ self._preregister_custom_keywords(ast)
77
+
78
+ # 语义验证
73
79
  self._validate_semantics(ast)
74
80
 
75
81
  # 元数据验证
@@ -80,8 +86,72 @@ class DSLValidator:
80
86
  if ast and not self.errors:
81
87
  self._validate_keywords(ast)
82
88
 
89
+ # 清理临时注册的关键字
90
+ self._cleanup_temp_keywords()
91
+
83
92
  return len(self.errors) == 0, self.errors + self.warnings
84
93
 
94
+ def _preregister_custom_keywords(self, ast: Node) -> None:
95
+ """预注册AST中定义的自定义关键字
96
+
97
+ Args:
98
+ ast: 解析后的AST
99
+ """
100
+ try:
101
+ from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
102
+
103
+ # 查找并注册自定义关键字
104
+ self._find_and_register_custom_keywords(ast)
105
+
106
+ except Exception as e:
107
+ # 如果预注册失败,记录警告但不影响验证流程
108
+ self.warnings.append(DSLValidationError(
109
+ "关键字预处理警告",
110
+ f"预注册自定义关键字时出现警告: {str(e)}"
111
+ ))
112
+
113
+ def _find_and_register_custom_keywords(self, node: Node) -> None:
114
+ """递归查找并注册自定义关键字
115
+
116
+ Args:
117
+ node: 当前节点
118
+ """
119
+ # 检查当前节点是否是自定义关键字定义
120
+ if node.type in ['CustomKeyword', 'Function']:
121
+ try:
122
+ from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
123
+
124
+ # 注册自定义关键字
125
+ custom_keyword_manager._register_custom_keyword(
126
+ node, "validation_temp")
127
+
128
+ # 记录已注册的关键字名称,用于后续清理
129
+ keyword_name = node.value
130
+ self._temp_registered_keywords.append(keyword_name)
131
+
132
+ except Exception as e:
133
+ self.warnings.append(DSLValidationError(
134
+ "关键字注册警告",
135
+ f"注册自定义关键字 '{node.value}' 时出现警告: {str(e)}"
136
+ ))
137
+
138
+ # 递归处理子节点
139
+ if hasattr(node, 'children') and node.children:
140
+ for child in node.children:
141
+ if isinstance(child, Node):
142
+ self._find_and_register_custom_keywords(child)
143
+
144
+ def _cleanup_temp_keywords(self) -> None:
145
+ """清理临时注册的关键字"""
146
+ try:
147
+ for keyword_name in self._temp_registered_keywords:
148
+ # 从关键字管理器中移除临时注册的关键字
149
+ if keyword_name in keyword_manager._keywords:
150
+ del keyword_manager._keywords[keyword_name]
151
+ except Exception as e:
152
+ # 清理失败不影响验证结果,只记录警告
153
+ pass
154
+
85
155
  def _validate_basic_format(self, content: str) -> None:
86
156
  """基础格式验证"""
87
157
  if not content or not content.strip():