pytest-dsl 0.10.0__py3-none-any.whl → 0.11.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.
@@ -1,11 +1,8 @@
1
- from typing import Dict, Any, List, Optional
2
1
  import os
3
- import pathlib
4
2
  from pytest_dsl.core.lexer import get_lexer
5
3
  from pytest_dsl.core.parser import get_parser, Node
6
4
  from pytest_dsl.core.dsl_executor import DSLExecutor
7
5
  from pytest_dsl.core.keyword_manager import keyword_manager
8
- from pytest_dsl.core.context import TestContext
9
6
 
10
7
 
11
8
  class CustomKeywordManager:
@@ -127,7 +124,7 @@ class CustomKeywordManager:
127
124
  return
128
125
 
129
126
  for node in statements_node.children:
130
- if node.type == 'CustomKeyword':
127
+ if node.type in ['CustomKeyword', 'Function']:
131
128
  self._register_custom_keyword(node, file_path)
132
129
 
133
130
  def _register_custom_keyword(self, node: Node, file_path: str) -> None:
@@ -132,7 +132,7 @@ class HTTPClient:
132
132
  request_kwargs = self.auth_provider.pre_request_hook(method, url, request_kwargs)
133
133
 
134
134
  # 记录请求详情
135
- logger.debug(f"=== HTTP请求详情 ===")
135
+ logger.debug("=== HTTP请求详情 ===")
136
136
  logger.debug(f"方法: {method}")
137
137
  logger.debug(f"URL: {url}")
138
138
  if 'headers' in request_kwargs:
@@ -171,7 +171,7 @@ class HTTPClient:
171
171
  response = requests.request(method, url, **request_kwargs)
172
172
 
173
173
  # 记录响应详情
174
- logger.debug(f"\n=== HTTP响应详情 ===")
174
+ logger.debug("\n=== HTTP响应详情 ===")
175
175
  logger.debug(f"状态码: {response.status_code}")
176
176
  logger.debug(f"响应头: {json.dumps(dict(response.headers), indent=2, ensure_ascii=False)}")
177
177
  try:
@@ -16,8 +16,14 @@ class KeywordManager:
16
16
  self._keywords: Dict[str, Dict] = {}
17
17
  self.current_context = None
18
18
 
19
- def register(self, name: str, parameters: List[Dict]):
20
- """关键字注册装饰器"""
19
+ def register(self, name: str, parameters: List[Dict], source_info: Optional[Dict] = None):
20
+ """关键字注册装饰器
21
+
22
+ Args:
23
+ name: 关键字名称
24
+ parameters: 参数列表
25
+ source_info: 来源信息,包含 source_type, source_name, module_name 等
26
+ """
21
27
  def decorator(func: Callable) -> Callable:
22
28
  @functools.wraps(func)
23
29
  def wrapper(**kwargs):
@@ -40,15 +46,71 @@ class KeywordManager:
40
46
  # 自动添加 step_name 到 mapping 中
41
47
  mapping["步骤名称"] = "step_name"
42
48
 
43
- self._keywords[name] = {
49
+ # 构建关键字信息,包含来源信息
50
+ keyword_info = {
44
51
  'func': wrapper,
45
52
  'mapping': mapping,
46
53
  'parameters': param_list,
47
54
  'defaults': defaults # 存储默认值
48
55
  }
56
+
57
+ # 添加来源信息
58
+ if source_info:
59
+ keyword_info.update(source_info)
60
+ else:
61
+ # 尝试从函数模块推断来源信息
62
+ keyword_info.update(self._infer_source_info(func))
63
+
64
+ self._keywords[name] = keyword_info
49
65
  return wrapper
50
66
  return decorator
51
67
 
68
+ def _infer_source_info(self, func: Callable) -> Dict:
69
+ """从函数推断来源信息"""
70
+ source_info = {}
71
+
72
+ if hasattr(func, '__module__'):
73
+ module_name = func.__module__
74
+ source_info['module_name'] = module_name
75
+
76
+ if module_name.startswith('pytest_dsl.keywords'):
77
+ # 内置关键字
78
+ source_info['source_type'] = 'builtin'
79
+ source_info['source_name'] = 'pytest-dsl内置'
80
+ elif 'pytest_dsl' in module_name:
81
+ # pytest-dsl相关但不是内置的
82
+ source_info['source_type'] = 'internal'
83
+ source_info['source_name'] = 'pytest-dsl'
84
+ else:
85
+ # 第三方插件或用户自定义
86
+ source_info['source_type'] = 'external'
87
+ # 提取可能的包名
88
+ parts = module_name.split('.')
89
+ if len(parts) > 1:
90
+ source_info['source_name'] = parts[0]
91
+ else:
92
+ source_info['source_name'] = module_name
93
+
94
+ return source_info
95
+
96
+ def register_with_source(self, name: str, parameters: List[Dict],
97
+ source_type: str, source_name: str, **kwargs):
98
+ """带来源信息的关键字注册装饰器
99
+
100
+ Args:
101
+ name: 关键字名称
102
+ parameters: 参数列表
103
+ source_type: 来源类型 (builtin, plugin, local, remote, project_custom)
104
+ source_name: 来源名称 (插件名、文件路径等)
105
+ **kwargs: 其他来源相关信息
106
+ """
107
+ source_info = {
108
+ 'source_type': source_type,
109
+ 'source_name': source_name,
110
+ **kwargs
111
+ }
112
+ return self.register(name, parameters, source_info)
113
+
52
114
  def execute(self, keyword_name: str, **params: Any) -> Any:
53
115
  """执行关键字"""
54
116
  keyword_info = self._keywords.get(keyword_name)
@@ -84,6 +146,18 @@ class KeywordManager:
84
146
 
85
147
  return keyword_info
86
148
 
149
+ def get_keywords_by_source(self) -> Dict[str, List[str]]:
150
+ """按来源分组获取关键字"""
151
+ by_source = {}
152
+
153
+ for name, info in self._keywords.items():
154
+ source_name = info.get('source_name', '未知来源')
155
+ if source_name not in by_source:
156
+ by_source[source_name] = []
157
+ by_source[source_name].append(name)
158
+
159
+ return by_source
160
+
87
161
  def _log_execution(self, keyword_name: str, params: Dict, result: Any) -> None:
88
162
  """记录关键字执行结果"""
89
163
  allure.attach(
@@ -62,7 +62,28 @@ def load_plugin_keywords(plugin_name: str) -> None:
62
62
 
63
63
  # 如果插件有register_keywords函数,调用它
64
64
  if hasattr(plugin, 'register_keywords') and callable(plugin.register_keywords):
65
- plugin.register_keywords(keyword_manager)
65
+ # 创建一个包装的关键字管理器,自动添加来源信息
66
+ class PluginKeywordManager:
67
+ def __init__(self, original_manager, plugin_name):
68
+ self.original_manager = original_manager
69
+ self.plugin_name = plugin_name
70
+
71
+ def register(self, name: str, parameters):
72
+ """带插件来源信息的注册方法"""
73
+ return self.original_manager.register_with_source(
74
+ name, parameters,
75
+ source_type='plugin',
76
+ source_name=plugin_name,
77
+ module_name=plugin_name
78
+ )
79
+
80
+ def __getattr__(self, name):
81
+ # 代理其他方法到原始管理器
82
+ return getattr(self.original_manager, name)
83
+
84
+ plugin_manager = PluginKeywordManager(keyword_manager, plugin_name)
85
+ plugin.register_keywords(plugin_manager)
86
+ print(f"通过register_keywords加载插件: {plugin_name}")
66
87
  return
67
88
 
68
89
  # 否则,遍历包中的所有模块
@@ -71,13 +92,29 @@ def load_plugin_keywords(plugin_name: str) -> None:
71
92
  if not is_pkg:
72
93
  try:
73
94
  module = importlib.import_module(name)
95
+ print(f"加载插件模块: {name}")
74
96
  # 模块已导入,关键字装饰器会自动注册
97
+ # 但我们需要在导入后更新来源信息
98
+ _update_keywords_source_info(plugin_name, name)
75
99
  except ImportError as e:
76
100
  print(f"无法导入模块 {name}: {e}")
77
101
  except ImportError as e:
78
102
  print(f"无法导入插件 {plugin_name}: {e}")
79
103
 
80
104
 
105
+ def _update_keywords_source_info(plugin_name: str, module_name: str):
106
+ """更新模块中关键字的来源信息"""
107
+ # 找到可能是新注册的关键字
108
+ for keyword_name, keyword_info in keyword_manager._keywords.items():
109
+ if keyword_info.get('module_name') == module_name:
110
+ # 更新来源信息
111
+ keyword_info.update({
112
+ 'source_type': 'plugin',
113
+ 'source_name': plugin_name,
114
+ 'plugin_module': module_name
115
+ })
116
+
117
+
81
118
  def load_all_plugins() -> None:
82
119
  """
83
120
  发现并加载所有已安装的关键字插件
@@ -20,7 +20,10 @@ from pytest_dsl.core.context import TestContext
20
20
  # 配置日志
21
21
  logger = logging.getLogger(__name__)
22
22
 
23
- def _process_file_reference(reference: Union[str, Dict[str, Any]], allow_vars: bool = True, test_context: TestContext = None) -> Any:
23
+
24
+ def _process_file_reference(reference: Union[str, Dict[str, Any]],
25
+ allow_vars: bool = True,
26
+ test_context: TestContext = None) -> Any:
24
27
  """处理文件引用,加载外部文件内容
25
28
 
26
29
  支持两种语法:
@@ -43,7 +46,8 @@ def _process_file_reference(reference: Union[str, Dict[str, Any]], allow_vars: b
43
46
  if match:
44
47
  file_path = match.group(1).strip()
45
48
  is_template = '_template' in reference[:15] # 检查是否为模板
46
- return _load_file_content(file_path, is_template, 'auto', 'utf-8', test_context)
49
+ return _load_file_content(file_path, is_template, 'auto', 'utf-8',
50
+ test_context)
47
51
 
48
52
  # 处理详细语法
49
53
  elif isinstance(reference, dict) and 'file_ref' in reference:
@@ -51,7 +55,8 @@ def _process_file_reference(reference: Union[str, Dict[str, Any]], allow_vars: b
51
55
 
52
56
  if isinstance(file_ref, str):
53
57
  # 如果file_ref是字符串,使用默认配置
54
- return _load_file_content(file_ref, allow_vars, 'auto', 'utf-8', test_context)
58
+ return _load_file_content(file_ref, allow_vars, 'auto', 'utf-8',
59
+ test_context)
55
60
  elif isinstance(file_ref, dict):
56
61
  # 如果file_ref是字典,使用自定义配置
57
62
  file_path = file_ref.get('path')
@@ -62,14 +67,16 @@ def _process_file_reference(reference: Union[str, Dict[str, Any]], allow_vars: b
62
67
  file_type = file_ref.get('type', 'auto')
63
68
  encoding = file_ref.get('encoding', 'utf-8')
64
69
 
65
- return _load_file_content(file_path, template, file_type, encoding, test_context)
70
+ return _load_file_content(file_path, template, file_type, encoding,
71
+ test_context)
66
72
 
67
73
  # 如果不是文件引用,返回原始值
68
74
  return reference
69
75
 
70
76
 
71
77
  def _load_file_content(file_path: str, is_template: bool = False,
72
- file_type: str = 'auto', encoding: str = 'utf-8', test_context: TestContext = None) -> Any:
78
+ file_type: str = 'auto', encoding: str = 'utf-8',
79
+ test_context: TestContext = None) -> Any:
73
80
  """加载文件内容
74
81
 
75
82
  Args:
@@ -122,7 +129,8 @@ def _load_file_content(file_path: str, is_template: bool = False,
122
129
  return content
123
130
 
124
131
 
125
- def _process_request_config(config: Dict[str, Any], test_context: TestContext = None) -> Dict[str, Any]:
132
+ def _process_request_config(config: Dict[str, Any],
133
+ test_context: TestContext = None) -> Dict[str, Any]:
126
134
  """处理请求配置,检查并处理文件引用
127
135
 
128
136
  Args:
@@ -140,20 +148,24 @@ def _process_request_config(config: Dict[str, Any], test_context: TestContext =
140
148
 
141
149
  # 处理json字段
142
150
  if 'json' in request:
143
- request['json'] = _process_file_reference(request['json'], test_context=test_context)
151
+ request['json'] = _process_file_reference(
152
+ request['json'], test_context=test_context)
144
153
 
145
154
  # 处理data字段
146
155
  if 'data' in request:
147
- request['data'] = _process_file_reference(request['data'], test_context=test_context)
156
+ request['data'] = _process_file_reference(
157
+ request['data'], test_context=test_context)
148
158
 
149
159
  # 处理headers字段
150
160
  if 'headers' in request:
151
- request['headers'] = _process_file_reference(request['headers'], test_context=test_context)
161
+ request['headers'] = _process_file_reference(
162
+ request['headers'], test_context=test_context)
152
163
 
153
164
  return config
154
165
 
155
166
 
156
- def _normalize_retry_config(config, assert_retry_count=None, assert_retry_interval=None):
167
+ def _normalize_retry_config(config, assert_retry_count=None,
168
+ assert_retry_interval=None):
157
169
  """标准化断言重试配置
158
170
 
159
171
  将不同来源的重试配置(命令行参数、retry配置、retry_assertions配置)
@@ -223,14 +235,23 @@ def _normalize_retry_config(config, assert_retry_count=None, assert_retry_interv
223
235
 
224
236
 
225
237
  @keyword_manager.register('HTTP请求', [
226
- {'name': '客户端', 'mapping': 'client', 'description': '客户端名称,对应YAML变量文件中的客户端配置', 'default': 'default'},
227
- {'name': '配置', 'mapping': 'config', 'description': '包含请求、捕获和断言的YAML配置'},
228
- {'name': '会话', 'mapping': 'session', 'description': '会话名称,用于在多个请求间保持会话状态'},
229
- {'name': '保存响应', 'mapping': 'save_response', 'description': '将完整响应保存到指定变量名中'},
230
- {'name': '禁用授权', 'mapping': 'disable_auth', 'description': '禁用客户端配置中的授权机制', 'default': False},
231
- {'name': '模板', 'mapping': 'template', 'description': '使用YAML变量文件中定义的请求模板'},
232
- {'name': '断言重试次数', 'mapping': 'assert_retry_count', 'description': '断言失败时的重试次数', 'default': 0},
233
- {'name': '断言重试间隔', 'mapping': 'assert_retry_interval', 'description': '断言重试间隔时间(秒)', 'default': 1}
238
+ {'name': '客户端', 'mapping': 'client',
239
+ 'description': '客户端名称,对应YAML变量文件中的客户端配置',
240
+ 'default': 'default'},
241
+ {'name': '配置', 'mapping': 'config',
242
+ 'description': '包含请求、捕获和断言的YAML配置'},
243
+ {'name': '会话', 'mapping': 'session',
244
+ 'description': '会话名称,用于在多个请求间保持会话状态'},
245
+ {'name': '保存响应', 'mapping': 'save_response',
246
+ 'description': '将完整响应保存到指定变量名中'},
247
+ {'name': '禁用授权', 'mapping': 'disable_auth',
248
+ 'description': '禁用客户端配置中的授权机制', 'default': False},
249
+ {'name': '模板', 'mapping': 'template',
250
+ 'description': '使用YAML变量文件中定义的请求模板'},
251
+ {'name': '断言重试次数', 'mapping': 'assert_retry_count',
252
+ 'description': '断言失败时的重试次数', 'default': 0},
253
+ {'name': '断言重试间隔', 'mapping': 'assert_retry_interval',
254
+ 'description': '断言重试间隔时间(秒)', 'default': 1}
234
255
  ])
235
256
  def http_request(context, **kwargs):
236
257
  """执行HTTP请求
@@ -260,7 +281,26 @@ def http_request(context, **kwargs):
260
281
  assert_retry_count = kwargs.get('assert_retry_count')
261
282
  assert_retry_interval = kwargs.get('assert_retry_interval')
262
283
 
263
- with allure.step(f"发送HTTP请求 (客户端: {client_name}{', 会话: ' + session_name if session_name else ''})"):
284
+ # 添加调试信息,检查客户端配置是否可用
285
+ print(f"🌐 HTTP请求 - 客户端: {client_name}")
286
+
287
+ # 检查YAML变量中的http_clients配置(现在包含同步的变量)
288
+ http_clients_config = yaml_vars.get_variable("http_clients")
289
+ if http_clients_config:
290
+ print(f"✓ 找到http_clients配置,包含 {len(http_clients_config)} 个客户端")
291
+ if client_name in http_clients_config:
292
+ print(f"✓ 找到客户端 '{client_name}' 的配置")
293
+ client_config = http_clients_config[client_name]
294
+ print(f" - base_url: {client_config.get('base_url', 'N/A')}")
295
+ print(f" - timeout: {client_config.get('timeout', 'N/A')}")
296
+ else:
297
+ print(f"⚠️ 未找到客户端 '{client_name}' 的配置")
298
+ print(f" 可用客户端: {list(http_clients_config.keys())}")
299
+ else:
300
+ print("⚠️ 未找到http_clients配置")
301
+
302
+ with allure.step(f"发送HTTP请求 (客户端: {client_name}"
303
+ f"{', 会话: ' + session_name if session_name else ''})"):
264
304
  # 处理模板
265
305
  if template_name:
266
306
  # 从YAML变量中获取模板
@@ -299,7 +339,8 @@ def http_request(context, **kwargs):
299
339
  raise ValueError(f"无效的YAML配置: {str(e)}")
300
340
 
301
341
  # 统一处理重试配置
302
- retry_config = _normalize_retry_config(config, assert_retry_count, assert_retry_interval)
342
+ retry_config = _normalize_retry_config(config, assert_retry_count,
343
+ assert_retry_interval)
303
344
 
304
345
  # 为了兼容性,将标准化后的重试配置写回到配置中
305
346
  if retry_config['enabled']:
@@ -344,7 +385,8 @@ def http_request(context, **kwargs):
344
385
  if session_name:
345
386
  try:
346
387
  from pytest_dsl.core.http_client import http_client_manager
347
- session_client = http_client_manager.get_session(session_name, client_name)
388
+ session_client = http_client_manager.get_session(
389
+ session_name, client_name)
348
390
  if session_client and session_client._session:
349
391
  session_state = {
350
392
  "cookies": dict(session_client._session.cookies),
@@ -375,7 +417,8 @@ def http_request(context, **kwargs):
375
417
  return {
376
418
  "result": captured_values, # 主要返回值保持兼容
377
419
  "captures": captured_values, # 明确的捕获变量
378
- "session_state": {session_name: session_state} if session_state else {},
420
+ "session_state": ({session_name: session_state}
421
+ if session_state else {}),
379
422
  "response": response_data, # 完整响应(如果需要)
380
423
  "metadata": {
381
424
  "response_time": getattr(response, 'elapsed', None),
@@ -396,7 +439,8 @@ def _deep_merge(dict1, dict2):
396
439
  合并后的字典
397
440
  """
398
441
  for key in dict2:
399
- if key in dict1 and isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
442
+ if (key in dict1 and isinstance(dict1[key], dict) and
443
+ isinstance(dict2[key], dict)):
400
444
  _deep_merge(dict1[key], dict2[key])
401
445
  else:
402
446
  dict1[key] = dict2[key]
@@ -424,7 +468,8 @@ def _process_assertions_with_unified_retry(http_req, retry_config):
424
468
  )
425
469
 
426
470
  # 添加一个特殊的标记到配置中,表示我们只想收集失败的断言而不抛出异常
427
- original_config = http_req.config.copy() if isinstance(http_req.config, dict) else {}
471
+ original_config = (http_req.config.copy()
472
+ if isinstance(http_req.config, dict) else {})
428
473
 
429
474
  # 创建一个临时副本
430
475
  temp_config = original_config.copy()
@@ -442,7 +487,8 @@ def _process_assertions_with_unified_retry(http_req, retry_config):
442
487
  except Exception as collect_err:
443
488
  # 出现意外错误时记录
444
489
  allure.attach(
445
- f"收集失败断言时出错: {type(collect_err).__name__}: {str(collect_err)}",
490
+ f"收集失败断言时出错: {type(collect_err).__name__}: "
491
+ f"{str(collect_err)}",
446
492
  name="断言收集错误",
447
493
  attachment_type=allure.attachment_type.TEXT
448
494
  )
@@ -513,16 +559,18 @@ def _process_assertions_with_unified_retry(http_req, retry_config):
513
559
 
514
560
  # 找出所有断言中最大的重试次数
515
561
  for retryable_assertion in retryable_assertions:
516
- max_retry_count = max(max_retry_count, retryable_assertion.get('retry_count', 3))
562
+ max_retry_count = max(max_retry_count,
563
+ retryable_assertion.get('retry_count', 3))
517
564
 
518
565
  # 进行断言重试
519
- for attempt in range(1, max_retry_count + 1): # 从1开始,因为第0次已经尝试过了
566
+ for attempt in range(1, max_retry_count + 1):
520
567
  # 等待重试间隔
521
568
  with allure.step(f"断言重试 (尝试 {attempt}/{max_retry_count})"):
522
569
  # 确定本次重试的间隔时间(使用每个断言中最长的间隔时间)
523
570
  retry_interval = retry_config['interval']
524
571
  for assertion in retryable_assertions:
525
- retry_interval = max(retry_interval, assertion.get('retry_interval', 1.0))
572
+ retry_interval = max(retry_interval,
573
+ assertion.get('retry_interval', 1.0))
526
574
 
527
575
  allure.attach(
528
576
  f"重试 {len(retryable_assertions)} 个断言\n"
@@ -552,14 +600,21 @@ def _process_assertions_with_unified_retry(http_req, retry_config):
552
600
  # 只重试那些仍在重试范围内的断言
553
601
  try:
554
602
  # 从原始断言配置中提取出需要重试的断言
555
- retry_assertion_indexes = [a['index'] for a in still_retryable_assertions]
556
- retry_assertions = [http_req.config.get('asserts', [])[idx] for idx in retry_assertion_indexes]
603
+ retry_assertion_indexes = [
604
+ a['index'] for a in still_retryable_assertions]
605
+ retry_assertions = [
606
+ http_req.config.get('asserts', [])[idx]
607
+ for idx in retry_assertion_indexes]
557
608
 
558
609
  # 创建索引映射:新索引 -> 原始索引
559
- index_mapping = {new_idx: orig_idx for new_idx, orig_idx in enumerate(retry_assertion_indexes)}
610
+ index_mapping = {
611
+ new_idx: orig_idx for new_idx, orig_idx in
612
+ enumerate(retry_assertion_indexes)}
560
613
 
561
614
  # 只处理需要重试的断言,传递索引映射
562
- results, new_failed_assertions = http_req.process_asserts(specific_asserts=retry_assertions, index_mapping=index_mapping)
615
+ results, new_failed_assertions = http_req.process_asserts(
616
+ specific_asserts=retry_assertions,
617
+ index_mapping=index_mapping)
563
618
 
564
619
  # 如果所有断言都通过了,检查全部断言
565
620
  if not new_failed_assertions:
@@ -607,7 +662,8 @@ def _process_assertions_with_unified_retry(http_req, retry_config):
607
662
  except AssertionError as final_err:
608
663
  # 重新格式化错误消息,添加重试信息
609
664
  enhanced_error = (
610
- f"断言验证失败 (已重试 {max_retry_count} 次):\n\n{str(final_err)}"
665
+ f"断言验证失败 (已重试 {max_retry_count} 次):\n\n"
666
+ f"{str(final_err)}"
611
667
  )
612
668
  allure.attach(
613
669
  enhanced_error,
@@ -617,4 +673,4 @@ def _process_assertions_with_unified_retry(http_req, retry_config):
617
673
  raise AssertionError(enhanced_error) from final_err
618
674
 
619
675
 
620
- # 注意:旧的重试函数已被移除,现在使用统一的重试机制
676
+ # 注意:旧的重试函数已被移除,现在使用统一的重试机制
@@ -7,16 +7,19 @@ from pytest_dsl.core.keyword_manager import keyword_manager, Parameter
7
7
  # 配置日志
8
8
  logger = logging.getLogger(__name__)
9
9
 
10
+
10
11
  class RemoteKeywordClient:
11
12
  """远程关键字客户端,用于连接远程关键字服务器并执行关键字"""
12
13
 
13
- def __init__(self, url='http://localhost:8270/', api_key=None, alias=None, sync_config=None):
14
+ def __init__(self, url='http://localhost:8270/', api_key=None, alias=None,
15
+ sync_config=None):
14
16
  self.url = url
15
17
  self.server = xmlrpc.client.ServerProxy(url, allow_none=True)
16
18
  self.keyword_cache = {}
17
19
  self.param_mappings = {} # 存储每个关键字的参数映射
18
20
  self.api_key = api_key
19
- self.alias = alias or url.replace('http://', '').replace('https://', '').split(':')[0]
21
+ self.alias = alias or url.replace('http://', '').replace(
22
+ 'https://', '').split(':')[0]
20
23
 
21
24
  # 变量传递配置(简化版)
22
25
  self.sync_config = sync_config or {
@@ -24,7 +27,6 @@ class RemoteKeywordClient:
24
27
  'sync_yaml_vars': True, # 连接时传递YAML配置变量
25
28
  'yaml_sync_keys': None, # 指定要同步的YAML键列表,None表示同步所有(除了排除的)
26
29
  'yaml_exclude_patterns': [ # 排除包含这些模式的YAML变量
27
- 'password', 'secret', 'key', 'token', 'credential', 'auth',
28
30
  'private', 'remote_servers' # 排除远程服务器配置避免循环
29
31
  ]
30
32
  }
@@ -42,7 +44,8 @@ class RemoteKeywordClient:
42
44
  self._send_initial_variables()
43
45
 
44
46
  logger.info(f"已连接到远程关键字服务器: {self.url}, 别名: {self.alias}")
45
- print(f"RemoteKeywordClient: 成功连接到远程服务器 {self.url}, 别名: {self.alias}")
47
+ print(f"RemoteKeywordClient: 成功连接到远程服务器 {self.url}, "
48
+ f"别名: {self.alias}")
46
49
  return True
47
50
  except Exception as e:
48
51
  error_msg = f"连接远程关键字服务器失败: {str(e)}"
@@ -81,7 +84,8 @@ class RemoteKeywordClient:
81
84
  for param_detail in param_details:
82
85
  param_name = param_detail['name']
83
86
  param_mapping_name = param_detail.get('mapping', param_name)
84
- param_desc = param_detail.get('description', f'远程关键字参数: {param_name}')
87
+ param_desc = param_detail.get('description',
88
+ f'远程关键字参数: {param_name}')
85
89
  param_default = param_detail.get('default')
86
90
 
87
91
  # 确保参数名称正确映射
@@ -114,7 +118,8 @@ class RemoteKeywordClient:
114
118
  'func': remote_func,
115
119
  'mapping': {p['name']: p['mapping'] for p in parameters},
116
120
  'parameters': [Parameter(**p) for p in parameters],
117
- 'defaults': {p['mapping']: p['default'] for p in parameters if p['default'] is not None}, # 添加默认值支持
121
+ 'defaults': {p['mapping']: p['default'] for p in parameters
122
+ if p['default'] is not None}, # 添加默认值支持
118
123
  'remote': True, # 标记为远程关键字
119
124
  'alias': self.alias,
120
125
  'original_name': name
@@ -161,7 +166,7 @@ class RemoteKeywordClient:
161
166
  else:
162
167
  # 如果没有任何映射,使用原始参数名
163
168
  param_mapping = None
164
- print(f"没有找到参数映射,使用原始参数名")
169
+ print("没有找到参数映射,使用原始参数名")
165
170
 
166
171
  # 映射参数名称
167
172
  mapped_kwargs = {}
@@ -209,10 +214,11 @@ class RemoteKeywordClient:
209
214
 
210
215
  # 处理响应数据
211
216
  if 'response' in return_data and return_data['response']:
212
- print(f"远程关键字响应数据: 已接收")
217
+ print("远程关键字响应数据: 已接收")
213
218
 
214
219
  # 检查是否为新的统一返回格式(包含captures等字段)
215
- if 'captures' in return_data or 'session_state' in return_data or 'metadata' in return_data:
220
+ if ('captures' in return_data or 'session_state' in return_data or
221
+ 'metadata' in return_data):
216
222
  # 返回完整的新格式,让DSL执行器处理变量捕获
217
223
  return return_data
218
224
  elif 'result' in return_data:
@@ -243,11 +249,13 @@ class RemoteKeywordClient:
243
249
  if variables_to_send:
244
250
  try:
245
251
  # 调用远程服务器的变量接收接口
246
- result = self.server.sync_variables_from_client(variables_to_send, self.api_key)
252
+ result = self.server.sync_variables_from_client(
253
+ variables_to_send, self.api_key)
247
254
  if result.get('status') == 'success':
248
255
  print(f"成功传递 {len(variables_to_send)} 个变量到远程服务器")
249
256
  else:
250
- print(f"传递变量到远程服务器失败: {result.get('error', '未知错误')}")
257
+ print(f"传递变量到远程服务器失败: "
258
+ f"{result.get('error', '未知错误')}")
251
259
  except Exception as e:
252
260
  print(f"调用远程变量接口失败: {str(e)}")
253
261
  else:
@@ -295,10 +303,12 @@ class RemoteKeywordClient:
295
303
  # 获取所有YAML变量
296
304
  yaml_data = yaml_vars._variables
297
305
  if yaml_data:
306
+ print(f"客户端YAML变量总数: {len(yaml_data)}")
307
+
298
308
  # 检查同步配置中是否指定了特定的键
299
309
  sync_keys = self.sync_config.get('yaml_sync_keys', None)
300
310
  exclude_patterns = self.sync_config.get('yaml_exclude_patterns', [
301
- 'password', 'secret', 'key', 'token', 'credential', 'auth',
311
+ 'password', 'secret', 'token', 'credential', 'auth',
302
312
  'private', 'remote_servers' # 排除远程服务器配置避免循环
303
313
  ])
304
314
 
@@ -307,6 +317,7 @@ class RemoteKeywordClient:
307
317
  for key in sync_keys:
308
318
  if key in yaml_data:
309
319
  variables[key] = yaml_data[key]
320
+ print(f"传递指定YAML变量: {key}")
310
321
  else:
311
322
  # 传递所有YAML变量,但排除敏感信息
312
323
  for key, value in yaml_data.items():
@@ -323,7 +334,8 @@ class RemoteKeywordClient:
323
334
  if not should_exclude and isinstance(value, str):
324
335
  value_lower = value.lower()
325
336
  for pattern in exclude_patterns:
326
- if pattern.lower() in value_lower and len(value) < 100: # 只检查短字符串
337
+ if (pattern.lower() in value_lower and
338
+ len(value) < 100): # 只检查短字符串
327
339
  should_exclude = True
328
340
  break
329
341
 
@@ -336,11 +348,11 @@ class RemoteKeywordClient:
336
348
 
337
349
  except Exception as e:
338
350
  logger.warning(f"收集YAML变量失败: {str(e)}")
351
+ print(f"收集YAML变量失败: {str(e)}")
339
352
 
340
353
  return variables
341
354
 
342
355
 
343
-
344
356
  # 远程关键字客户端管理器
345
357
  class RemoteKeywordManager:
346
358
  """远程关键字客户端管理器,管理多个远程服务器连接"""
@@ -348,7 +360,8 @@ class RemoteKeywordManager:
348
360
  def __init__(self):
349
361
  self.clients = {} # 别名 -> 客户端实例
350
362
 
351
- def register_remote_server(self, url, alias, api_key=None, sync_config=None):
363
+ def register_remote_server(self, url, alias, api_key=None,
364
+ sync_config=None):
352
365
  """注册远程关键字服务器
353
366
 
354
367
  Args:
@@ -361,7 +374,8 @@ class RemoteKeywordManager:
361
374
  bool: 是否成功连接
362
375
  """
363
376
  print(f"RemoteKeywordManager: 正在注册远程服务器 {url} 别名 {alias}")
364
- client = RemoteKeywordClient(url=url, api_key=api_key, alias=alias, sync_config=sync_config)
377
+ client = RemoteKeywordClient(url=url, api_key=api_key, alias=alias,
378
+ sync_config=sync_config)
365
379
  success = client.connect()
366
380
 
367
381
  if success:
@@ -394,6 +408,5 @@ class RemoteKeywordManager:
394
408
  return client._execute_remote_keyword(name=keyword_name, **kwargs)
395
409
 
396
410
 
397
-
398
411
  # 创建全局远程关键字管理器实例
399
412
  remote_keyword_manager = RemoteKeywordManager()