pytest-dsl 0.9.0__py3-none-any.whl → 0.9.1__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 CHANGED
@@ -453,5 +453,33 @@ def main():
453
453
  sys.exit(1)
454
454
 
455
455
 
456
+ def main_list_keywords():
457
+ """关键字列表命令的专用入口点"""
458
+ parser = argparse.ArgumentParser(description='查看pytest-dsl可用关键字列表')
459
+ parser.add_argument(
460
+ '--format', choices=['text', 'json'],
461
+ default='text',
462
+ help='输出格式:text(默认) 或 json'
463
+ )
464
+ parser.add_argument(
465
+ '--filter', type=str, default=None,
466
+ help='过滤关键字名称(支持部分匹配)'
467
+ )
468
+ parser.add_argument(
469
+ '--category',
470
+ choices=['builtin', 'custom', 'remote', 'all'],
471
+ default='all',
472
+ help='关键字类别:builtin(内置)、custom(自定义)、remote(远程)、all(全部,默认)'
473
+ )
474
+
475
+ args = parser.parse_args()
476
+
477
+ list_keywords(
478
+ output_format=args.format,
479
+ name_filter=args.filter,
480
+ category_filter=args.category
481
+ )
482
+
483
+
456
484
  if __name__ == '__main__':
457
485
  main()
@@ -0,0 +1,7 @@
1
+ """
2
+ Remote module for pytest-dsl.
3
+
4
+ This module provides remote keyword server functionality.
5
+ """
6
+
7
+ __version__ = "0.9.0"
@@ -0,0 +1,155 @@
1
+ """远程服务器Hook管理器
2
+
3
+ 该模块提供了远程服务器的hook机制,支持在服务器生命周期的关键点执行自定义逻辑。
4
+ """
5
+
6
+ import logging
7
+ from typing import Dict, List, Callable, Any
8
+ from enum import Enum
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class HookType(Enum):
14
+ """Hook类型枚举"""
15
+ SERVER_STARTUP = "server_startup"
16
+ SERVER_SHUTDOWN = "server_shutdown"
17
+ BEFORE_KEYWORD_EXECUTION = "before_keyword_execution"
18
+ AFTER_KEYWORD_EXECUTION = "after_keyword_execution"
19
+
20
+
21
+ class HookContext:
22
+ """Hook执行上下文"""
23
+
24
+ def __init__(self, hook_type: HookType, **kwargs):
25
+ self.hook_type = hook_type
26
+ self.data = kwargs
27
+ self.shared_variables = kwargs.get('shared_variables', {})
28
+
29
+ def get(self, key: str, default=None):
30
+ """获取上下文数据"""
31
+ return self.data.get(key, default)
32
+
33
+ def set(self, key: str, value: Any):
34
+ """设置上下文数据"""
35
+ self.data[key] = value
36
+
37
+ def get_shared_variable(self, name: str, default=None):
38
+ """获取共享变量"""
39
+ return self.shared_variables.get(name, default)
40
+
41
+
42
+ class HookManager:
43
+ """Hook管理器"""
44
+
45
+ def __init__(self):
46
+ self._hooks: Dict[HookType, List[Callable]] = {
47
+ HookType.SERVER_STARTUP: [],
48
+ HookType.SERVER_SHUTDOWN: [],
49
+ HookType.BEFORE_KEYWORD_EXECUTION: [],
50
+ HookType.AFTER_KEYWORD_EXECUTION: []
51
+ }
52
+
53
+ def register_hook(self, hook_type: HookType, hook_func: Callable):
54
+ """注册hook函数
55
+
56
+ Args:
57
+ hook_type: Hook类型
58
+ hook_func: Hook函数,接收HookContext参数
59
+ """
60
+ if hook_type not in self._hooks:
61
+ raise ValueError(f"不支持的hook类型: {hook_type}")
62
+
63
+ self._hooks[hook_type].append(hook_func)
64
+ logger.info(f"注册hook: {hook_type.value} -> {hook_func.__name__}")
65
+
66
+ def execute_hooks(self, hook_type: HookType, **context_data) -> HookContext:
67
+ """执行指定类型的所有hook
68
+
69
+ Args:
70
+ hook_type: Hook类型
71
+ **context_data: 传递给hook的上下文数据
72
+
73
+ Returns:
74
+ HookContext: 执行后的上下文
75
+ """
76
+ context = HookContext(hook_type, **context_data)
77
+
78
+ hooks = self._hooks.get(hook_type, [])
79
+ if not hooks:
80
+ logger.debug(f"没有注册的hook: {hook_type.value}")
81
+ return context
82
+
83
+ logger.debug(f"执行{len(hooks)}个hook: {hook_type.value}")
84
+
85
+ for hook_func in hooks:
86
+ try:
87
+ logger.debug(f"执行hook: {hook_func.__name__}")
88
+ hook_func(context)
89
+ except Exception as e:
90
+ logger.error(f"Hook执行失败 {hook_func.__name__}: {str(e)}")
91
+ # 继续执行其他hook,不因为一个hook失败而中断
92
+
93
+ return context
94
+
95
+ def get_registered_hooks(self, hook_type: HookType = None) -> Dict[HookType, List[str]]:
96
+ """获取已注册的hook信息
97
+
98
+ Args:
99
+ hook_type: 指定hook类型,None表示获取所有
100
+
101
+ Returns:
102
+ Dict: hook类型到函数名列表的映射
103
+ """
104
+ result = {}
105
+
106
+ if hook_type:
107
+ hooks = self._hooks.get(hook_type, [])
108
+ result[hook_type] = [hook.__name__ for hook in hooks]
109
+ else:
110
+ for ht, hooks in self._hooks.items():
111
+ result[ht] = [hook.__name__ for hook in hooks]
112
+
113
+ return result
114
+
115
+ def clear_hooks(self, hook_type: HookType = None):
116
+ """清除hook
117
+
118
+ Args:
119
+ hook_type: 指定hook类型,None表示清除所有
120
+ """
121
+ if hook_type:
122
+ self._hooks[hook_type] = []
123
+ logger.info(f"清除hook: {hook_type.value}")
124
+ else:
125
+ for ht in self._hooks:
126
+ self._hooks[ht] = []
127
+ logger.info("清除所有hook")
128
+
129
+
130
+ # 全局hook管理器实例
131
+ hook_manager = HookManager()
132
+
133
+
134
+ def register_startup_hook(func: Callable):
135
+ """装饰器:注册服务器启动hook"""
136
+ hook_manager.register_hook(HookType.SERVER_STARTUP, func)
137
+ return func
138
+
139
+
140
+ def register_shutdown_hook(func: Callable):
141
+ """装饰器:注册服务器关闭hook"""
142
+ hook_manager.register_hook(HookType.SERVER_SHUTDOWN, func)
143
+ return func
144
+
145
+
146
+ def register_before_keyword_hook(func: Callable):
147
+ """装饰器:注册关键字执行前hook"""
148
+ hook_manager.register_hook(HookType.BEFORE_KEYWORD_EXECUTION, func)
149
+ return func
150
+
151
+
152
+ def register_after_keyword_hook(func: Callable):
153
+ """装饰器:注册关键字执行后hook"""
154
+ hook_manager.register_hook(HookType.AFTER_KEYWORD_EXECUTION, func)
155
+ return func
@@ -0,0 +1,376 @@
1
+ import xmlrpc.client
2
+ from functools import partial
3
+ import logging
4
+
5
+ from pytest_dsl.core.keyword_manager import keyword_manager, Parameter
6
+
7
+ # 配置日志
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class RemoteKeywordClient:
11
+ """远程关键字客户端,用于连接远程关键字服务器并执行关键字"""
12
+
13
+ def __init__(self, url='http://localhost:8270/', api_key=None, alias=None, sync_config=None):
14
+ self.url = url
15
+ self.server = xmlrpc.client.ServerProxy(url, allow_none=True)
16
+ self.keyword_cache = {}
17
+ self.param_mappings = {} # 存储每个关键字的参数映射
18
+ self.api_key = api_key
19
+ self.alias = alias or url.replace('http://', '').replace('https://', '').split(':')[0]
20
+
21
+ # 变量传递配置(简化版)
22
+ self.sync_config = sync_config or {
23
+ 'sync_global_vars': True, # 连接时传递全局变量(g_开头)
24
+ 'sync_yaml_vars': True, # 连接时传递YAML配置变量
25
+ 'yaml_sync_keys': None, # 指定要同步的YAML键列表,None表示同步所有(除了排除的)
26
+ 'yaml_exclude_patterns': [ # 排除包含这些模式的YAML变量
27
+ 'password', 'secret', 'key', 'token', 'credential', 'auth',
28
+ 'private', 'remote_servers' # 排除远程服务器配置避免循环
29
+ ]
30
+ }
31
+
32
+ def connect(self):
33
+ """连接到远程服务器并获取可用关键字"""
34
+ try:
35
+ print(f"RemoteKeywordClient: 正在连接到远程服务器 {self.url}")
36
+ keyword_names = self.server.get_keyword_names()
37
+ print(f"RemoteKeywordClient: 获取到 {len(keyword_names)} 个关键字")
38
+ for name in keyword_names:
39
+ self._register_remote_keyword(name)
40
+
41
+ # 连接时传递变量到远程服务器
42
+ self._send_initial_variables()
43
+
44
+ logger.info(f"已连接到远程关键字服务器: {self.url}, 别名: {self.alias}")
45
+ print(f"RemoteKeywordClient: 成功连接到远程服务器 {self.url}, 别名: {self.alias}")
46
+ return True
47
+ except Exception as e:
48
+ error_msg = f"连接远程关键字服务器失败: {str(e)}"
49
+ logger.error(error_msg)
50
+ print(f"RemoteKeywordClient: {error_msg}")
51
+ return False
52
+
53
+ def _register_remote_keyword(self, name):
54
+ """注册远程关键字到本地关键字管理器"""
55
+ # 获取关键字参数信息
56
+ try:
57
+ param_names = self.server.get_keyword_arguments(name)
58
+ doc = self.server.get_keyword_documentation(name)
59
+
60
+ print(f"注册远程关键字: {name}, 参数: {param_names}")
61
+
62
+ # 创建参数列表
63
+ parameters = []
64
+ param_mapping = {} # 为每个关键字创建参数映射
65
+
66
+ for param_name in param_names:
67
+ # 确保参数名称正确映射
68
+ # 这里我们保持原始参数名称,但在执行时会进行正确映射
69
+ parameters.append({
70
+ 'name': param_name,
71
+ 'mapping': param_name, # 保持原始参数名称
72
+ 'description': f'远程关键字参数: {param_name}'
73
+ })
74
+ # 添加到参数映射
75
+ param_mapping[param_name] = param_name
76
+
77
+ # 添加步骤名称参数,这是所有关键字都应该有的
78
+ if not any(p['name'] == '步骤名称' for p in parameters):
79
+ parameters.append({
80
+ 'name': '步骤名称',
81
+ 'mapping': 'step_name',
82
+ 'description': '自定义的步骤名称,用于在报告中显示'
83
+ })
84
+ param_mapping['步骤名称'] = 'step_name'
85
+
86
+ # 创建远程关键字执行函数
87
+ remote_func = partial(self._execute_remote_keyword, name=name)
88
+ remote_func.__doc__ = doc
89
+
90
+ # 注册到关键字管理器,使用别名前缀
91
+ remote_keyword_name = f"{self.alias}|{name}"
92
+ keyword_manager._keywords[remote_keyword_name] = {
93
+ 'func': remote_func,
94
+ 'mapping': {p['name']: p['mapping'] for p in parameters},
95
+ 'parameters': [Parameter(**p) for p in parameters],
96
+ 'remote': True, # 标记为远程关键字
97
+ 'alias': self.alias,
98
+ 'original_name': name
99
+ }
100
+
101
+ # 缓存关键字信息
102
+ self.keyword_cache[name] = {
103
+ 'parameters': param_names, # 注意这里只缓存原始参数,不包括步骤名称
104
+ 'doc': doc
105
+ }
106
+
107
+ # 保存参数映射
108
+ self.param_mappings[name] = param_mapping
109
+
110
+ logger.debug(f"已注册远程关键字: {remote_keyword_name}")
111
+ except Exception as e:
112
+ logger.error(f"注册远程关键字 {name} 失败: {str(e)}")
113
+
114
+ def _execute_remote_keyword(self, **kwargs):
115
+ """执行远程关键字"""
116
+ name = kwargs.pop('name')
117
+
118
+ # 移除context参数,因为它不能被序列化
119
+ if 'context' in kwargs:
120
+ kwargs.pop('context', None)
121
+
122
+ # 移除step_name参数,这是自动添加的,不需要传递给远程服务器
123
+ if 'step_name' in kwargs:
124
+ kwargs.pop('step_name', None)
125
+
126
+ # 打印调试信息
127
+ print(f"远程关键字调用: {name}, 参数: {kwargs}")
128
+
129
+ # 创建反向映射字典,用于检查参数是否已经映射
130
+ reverse_mapping = {}
131
+
132
+ # 使用动态注册的参数映射
133
+ if name in self.param_mappings:
134
+ param_mapping = self.param_mappings[name]
135
+ print(f"使用动态参数映射: {param_mapping}")
136
+ for cn_name, en_name in param_mapping.items():
137
+ reverse_mapping[en_name] = cn_name
138
+ else:
139
+ # 如果没有任何映射,使用原始参数名
140
+ param_mapping = None
141
+ print(f"没有找到参数映射,使用原始参数名")
142
+
143
+ # 映射参数名称
144
+ mapped_kwargs = {}
145
+ if param_mapping:
146
+ for k, v in kwargs.items():
147
+ if k in param_mapping:
148
+ mapped_key = param_mapping[k]
149
+ mapped_kwargs[mapped_key] = v
150
+ print(f"参数映射: {k} -> {mapped_key} = {v}")
151
+ else:
152
+ mapped_kwargs[k] = v
153
+ else:
154
+ mapped_kwargs = kwargs
155
+
156
+ # 确保参数名称正确映射
157
+ # 获取关键字的参数信息
158
+ if name in self.keyword_cache:
159
+ param_names = self.keyword_cache[name]['parameters']
160
+ print(f"远程关键字 {name} 的参数列表: {param_names}")
161
+
162
+ # 不再显示警告信息,因为参数已经在服务器端正确处理
163
+ # 服务器端会使用默认值或者报错,客户端不需要重复警告
164
+
165
+ # 执行远程调用
166
+ # 检查是否需要传递API密钥
167
+ if self.api_key:
168
+ result = self.server.run_keyword(name, mapped_kwargs, self.api_key)
169
+ else:
170
+ result = self.server.run_keyword(name, mapped_kwargs)
171
+
172
+ print(f"远程关键字执行结果: {result}")
173
+
174
+ if result['status'] == 'PASS':
175
+ return_data = result['return']
176
+
177
+ # 处理新的返回格式
178
+ if isinstance(return_data, dict):
179
+ # 处理捕获的变量 - 这里需要访问本地上下文
180
+ if 'captures' in return_data and return_data['captures']:
181
+ print(f"远程关键字捕获的变量: {return_data['captures']}")
182
+
183
+ # 处理会话状态
184
+ if 'session_state' in return_data and return_data['session_state']:
185
+ print(f"远程关键字会话状态: {return_data['session_state']}")
186
+
187
+ # 处理响应数据
188
+ if 'response' in return_data and return_data['response']:
189
+ print(f"远程关键字响应数据: 已接收")
190
+
191
+ # 检查是否为新的统一返回格式(包含captures等字段)
192
+ if 'captures' in return_data or 'session_state' in return_data or 'metadata' in return_data:
193
+ # 返回完整的新格式,让DSL执行器处理变量捕获
194
+ return return_data
195
+ elif 'result' in return_data:
196
+ # 返回主要结果,保持向后兼容
197
+ return return_data['result']
198
+ else:
199
+ return return_data
200
+
201
+ return return_data
202
+ else:
203
+ error_msg = result.get('error', '未知错误')
204
+ traceback = '\n'.join(result.get('traceback', []))
205
+ raise Exception(f"远程关键字执行失败: {error_msg}\n{traceback}")
206
+
207
+ def _send_initial_variables(self):
208
+ """连接时发送初始变量到远程服务器"""
209
+ try:
210
+ variables_to_send = {}
211
+
212
+ # 收集全局变量
213
+ if self.sync_config.get('sync_global_vars', True):
214
+ variables_to_send.update(self._collect_global_variables())
215
+
216
+ # 收集YAML变量
217
+ if self.sync_config.get('sync_yaml_vars', True):
218
+ variables_to_send.update(self._collect_yaml_variables())
219
+
220
+ if variables_to_send:
221
+ try:
222
+ # 调用远程服务器的变量接收接口
223
+ result = self.server.sync_variables_from_client(variables_to_send, self.api_key)
224
+ if result.get('status') == 'success':
225
+ print(f"成功传递 {len(variables_to_send)} 个变量到远程服务器")
226
+ else:
227
+ print(f"传递变量到远程服务器失败: {result.get('error', '未知错误')}")
228
+ except Exception as e:
229
+ print(f"调用远程变量接口失败: {str(e)}")
230
+ else:
231
+ print("没有需要传递的变量")
232
+
233
+ except Exception as e:
234
+ logger.warning(f"初始变量传递失败: {str(e)}")
235
+ print(f"初始变量传递失败: {str(e)}")
236
+
237
+ def _collect_global_variables(self):
238
+ """收集全局变量"""
239
+ from pytest_dsl.core.global_context import global_context
240
+ variables = {}
241
+
242
+ # 获取所有全局变量(包括g_开头的变量)
243
+ try:
244
+ # 这里需要访问全局上下文的内部存储
245
+ # 由于GlobalContext使用文件存储,我们需要直接读取
246
+ import json
247
+ import os
248
+ from filelock import FileLock
249
+
250
+ storage_file = global_context._storage_file
251
+ lock_file = global_context._lock_file
252
+
253
+ if os.path.exists(storage_file):
254
+ with FileLock(lock_file):
255
+ with open(storage_file, 'r', encoding='utf-8') as f:
256
+ stored_vars = json.load(f)
257
+ # 只同步g_开头的全局变量
258
+ for name, value in stored_vars.items():
259
+ if name.startswith('g_'):
260
+ variables[name] = value
261
+ except Exception as e:
262
+ logger.warning(f"收集全局变量失败: {str(e)}")
263
+
264
+ return variables
265
+
266
+ def _collect_yaml_variables(self):
267
+ """收集YAML配置变量"""
268
+ from pytest_dsl.core.yaml_vars import yaml_vars
269
+ variables = {}
270
+
271
+ try:
272
+ # 获取所有YAML变量
273
+ yaml_data = yaml_vars._variables
274
+ if yaml_data:
275
+ # 检查同步配置中是否指定了特定的键
276
+ sync_keys = self.sync_config.get('yaml_sync_keys', None)
277
+ exclude_patterns = self.sync_config.get('yaml_exclude_patterns', [
278
+ 'password', 'secret', 'key', 'token', 'credential', 'auth',
279
+ 'private', 'remote_servers' # 排除远程服务器配置避免循环
280
+ ])
281
+
282
+ if sync_keys:
283
+ # 如果指定了特定键,只传递这些键,直接使用原始变量名
284
+ for key in sync_keys:
285
+ if key in yaml_data:
286
+ variables[key] = yaml_data[key]
287
+ else:
288
+ # 传递所有YAML变量,但排除敏感信息
289
+ for key, value in yaml_data.items():
290
+ # 检查是否包含敏感信息
291
+ key_lower = key.lower()
292
+ should_exclude = False
293
+
294
+ for pattern in exclude_patterns:
295
+ if pattern.lower() in key_lower:
296
+ should_exclude = True
297
+ break
298
+
299
+ # 如果值是字符串,也检查是否包含敏感信息
300
+ if not should_exclude and isinstance(value, str):
301
+ value_lower = value.lower()
302
+ for pattern in exclude_patterns:
303
+ if pattern.lower() in value_lower and len(value) < 100: # 只检查短字符串
304
+ should_exclude = True
305
+ break
306
+
307
+ if not should_exclude:
308
+ # 直接使用原始变量名,不添加yaml_前缀,实现无缝传递
309
+ variables[key] = value
310
+ print(f"传递YAML变量: {key}")
311
+ else:
312
+ print(f"跳过敏感YAML变量: {key}")
313
+
314
+ except Exception as e:
315
+ logger.warning(f"收集YAML变量失败: {str(e)}")
316
+
317
+ return variables
318
+
319
+
320
+
321
+ # 远程关键字客户端管理器
322
+ class RemoteKeywordManager:
323
+ """远程关键字客户端管理器,管理多个远程服务器连接"""
324
+
325
+ def __init__(self):
326
+ self.clients = {} # 别名 -> 客户端实例
327
+
328
+ def register_remote_server(self, url, alias, api_key=None, sync_config=None):
329
+ """注册远程关键字服务器
330
+
331
+ Args:
332
+ url: 服务器URL
333
+ alias: 服务器别名
334
+ api_key: API密钥(可选)
335
+ sync_config: 变量同步配置(可选)
336
+
337
+ Returns:
338
+ bool: 是否成功连接
339
+ """
340
+ print(f"RemoteKeywordManager: 正在注册远程服务器 {url} 别名 {alias}")
341
+ client = RemoteKeywordClient(url=url, api_key=api_key, alias=alias, sync_config=sync_config)
342
+ success = client.connect()
343
+
344
+ if success:
345
+ print(f"RemoteKeywordManager: 成功连接到远程服务器 {url}")
346
+ self.clients[alias] = client
347
+ else:
348
+ print(f"RemoteKeywordManager: 连接远程服务器 {url} 失败")
349
+
350
+ return success
351
+
352
+ def get_client(self, alias):
353
+ """获取指定别名的客户端实例"""
354
+ return self.clients.get(alias)
355
+
356
+ def execute_remote_keyword(self, alias, keyword_name, **kwargs):
357
+ """执行远程关键字
358
+
359
+ Args:
360
+ alias: 服务器别名
361
+ keyword_name: 关键字名称
362
+ **kwargs: 关键字参数
363
+
364
+ Returns:
365
+ 执行结果
366
+ """
367
+ client = self.get_client(alias)
368
+ if not client:
369
+ raise Exception(f"未找到别名为 {alias} 的远程服务器")
370
+
371
+ return client._execute_remote_keyword(name=keyword_name, **kwargs)
372
+
373
+
374
+
375
+ # 创建全局远程关键字管理器实例
376
+ remote_keyword_manager = RemoteKeywordManager()