pytest-dsl 0.9.1__py3-none-any.whl → 0.10.0__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
@@ -64,8 +64,12 @@ def parse_args():
64
64
  )
65
65
  list_parser.add_argument(
66
66
  '--format', choices=['text', 'json'],
67
- default='text',
68
- help='输出格式:text(默认) 或 json'
67
+ default='json',
68
+ help='输出格式:json(默认) 或 text'
69
+ )
70
+ list_parser.add_argument(
71
+ '--output', '-o', type=str, default=None,
72
+ help='输出文件路径(仅对 json 格式有效,默认为 keywords.json)'
69
73
  )
70
74
  list_parser.add_argument(
71
75
  '--filter', type=str, default=None,
@@ -88,7 +92,10 @@ def parse_args():
88
92
  if '--list-keywords' in argv:
89
93
  parser.add_argument('--list-keywords', action='store_true')
90
94
  parser.add_argument(
91
- '--format', choices=['text', 'json'], default='text'
95
+ '--format', choices=['text', 'json'], default='json'
96
+ )
97
+ parser.add_argument(
98
+ '--output', '-o', type=str, default=None
92
99
  )
93
100
  parser.add_argument('--filter', type=str, default=None)
94
101
  parser.add_argument(
@@ -178,13 +185,22 @@ def format_keyword_info_text(keyword_name, keyword_info, show_category=True):
178
185
  param_name = getattr(param, 'name', str(param))
179
186
  param_mapping = getattr(param, 'mapping', '')
180
187
  param_desc = getattr(param, 'description', '')
188
+ param_default = getattr(param, 'default', None)
181
189
 
190
+ # 构建参数描述
191
+ param_info = []
182
192
  if param_mapping and param_mapping != param_name:
183
- lines.append(
184
- f" {param_name} ({param_mapping}): {param_desc}"
185
- )
193
+ param_info.append(f"{param_name} ({param_mapping})")
186
194
  else:
187
- lines.append(f" {param_name}: {param_desc}")
195
+ param_info.append(param_name)
196
+
197
+ param_info.append(f": {param_desc}")
198
+
199
+ # 添加默认值信息
200
+ if param_default is not None:
201
+ param_info.append(f" (默认值: {param_default})")
202
+
203
+ lines.append(f" {''.join(param_info)}")
188
204
  else:
189
205
  lines.append(" 参数: 无")
190
206
 
@@ -221,6 +237,12 @@ def format_keyword_info_json(keyword_name, keyword_info):
221
237
  'mapping': getattr(param, 'mapping', ''),
222
238
  'description': getattr(param, 'description', '')
223
239
  }
240
+
241
+ # 添加默认值信息
242
+ param_default = getattr(param, 'default', None)
243
+ if param_default is not None:
244
+ param_data['default'] = param_default
245
+
224
246
  keyword_data['parameters'].append(param_data)
225
247
 
226
248
  # 函数文档
@@ -231,8 +253,8 @@ def format_keyword_info_json(keyword_name, keyword_info):
231
253
  return keyword_data
232
254
 
233
255
 
234
- def list_keywords(output_format='text', name_filter=None,
235
- category_filter='all'):
256
+ def list_keywords(output_format='json', name_filter=None,
257
+ category_filter='all', output_file=None):
236
258
  """罗列所有关键字信息"""
237
259
  import json
238
260
 
@@ -319,7 +341,25 @@ def list_keywords(output_format='text', name_filter=None,
319
341
  keyword_data = format_keyword_info_json(name, info)
320
342
  keywords_data['keywords'].append(keyword_data)
321
343
 
322
- print(json.dumps(keywords_data, ensure_ascii=False, indent=2))
344
+ json_output = json.dumps(keywords_data, ensure_ascii=False, indent=2)
345
+
346
+ # 确定输出文件名
347
+ if output_file is None:
348
+ output_file = 'keywords.json'
349
+
350
+ # 写入到文件
351
+ try:
352
+ with open(output_file, 'w', encoding='utf-8') as f:
353
+ f.write(json_output)
354
+ print(f"关键字信息已保存到文件: {output_file}")
355
+ print(f"共 {total_count} 个关键字")
356
+ for cat, count in category_counts.items():
357
+ cat_names = {'builtin': '内置', 'custom': '自定义', 'remote': '远程'}
358
+ print(f" {cat_names.get(cat, cat)}: {count} 个")
359
+ except Exception as e:
360
+ print(f"保存文件失败: {e}")
361
+ # 如果写入文件失败,则回退到打印
362
+ print(json_output)
323
363
 
324
364
 
325
365
  def load_yaml_variables(args):
@@ -433,16 +473,19 @@ def main():
433
473
  list_keywords(
434
474
  output_format=args.format,
435
475
  name_filter=args.filter,
436
- category_filter=args.category
476
+ category_filter=args.category,
477
+ output_file=args.output
437
478
  )
438
479
  elif args.command == 'run':
439
480
  run_dsl_tests(args)
440
481
  elif args.command == 'list-keywords-compat':
441
482
  # 向后兼容:旧的--list-keywords格式
483
+ output_file = getattr(args, 'output', None)
442
484
  list_keywords(
443
485
  output_format=args.format,
444
486
  name_filter=args.filter,
445
- category_filter=args.category
487
+ category_filter=args.category,
488
+ output_file=output_file
446
489
  )
447
490
  elif args.command == 'run-compat':
448
491
  # 向后兼容:默认执行DSL测试
@@ -458,8 +501,12 @@ def main_list_keywords():
458
501
  parser = argparse.ArgumentParser(description='查看pytest-dsl可用关键字列表')
459
502
  parser.add_argument(
460
503
  '--format', choices=['text', 'json'],
461
- default='text',
462
- help='输出格式:text(默认) 或 json'
504
+ default='json',
505
+ help='输出格式:json(默认) 或 text'
506
+ )
507
+ parser.add_argument(
508
+ '--output', '-o', type=str, default=None,
509
+ help='输出文件路径(仅对 json 格式有效,默认为 keywords.json)'
463
510
  )
464
511
  parser.add_argument(
465
512
  '--filter', type=str, default=None,
@@ -477,7 +524,8 @@ def main_list_keywords():
477
524
  list_keywords(
478
525
  output_format=args.format,
479
526
  name_filter=args.filter,
480
- category_filter=args.category
527
+ category_filter=args.category,
528
+ output_file=args.output
481
529
  )
482
530
 
483
531
 
@@ -1,13 +1,14 @@
1
- from typing import Dict, Any, Callable, List
1
+ from typing import Dict, Any, Callable, List, Optional
2
2
  import functools
3
3
  import allure
4
4
 
5
5
 
6
6
  class Parameter:
7
- def __init__(self, name: str, mapping: str, description: str):
7
+ def __init__(self, name: str, mapping: str, description: str, default: Any = None):
8
8
  self.name = name
9
9
  self.mapping = mapping
10
10
  self.description = description
11
+ self.default = default
11
12
 
12
13
 
13
14
  class KeywordManager:
@@ -34,6 +35,7 @@ class KeywordManager:
34
35
 
35
36
  param_list = [Parameter(**p) for p in parameters]
36
37
  mapping = {p.name: p.mapping for p in param_list}
38
+ defaults = {p.mapping: p.default for p in param_list if p.default is not None}
37
39
 
38
40
  # 自动添加 step_name 到 mapping 中
39
41
  mapping["步骤名称"] = "step_name"
@@ -41,7 +43,8 @@ class KeywordManager:
41
43
  self._keywords[name] = {
42
44
  'func': wrapper,
43
45
  'mapping': mapping,
44
- 'parameters': param_list
46
+ 'parameters': param_list,
47
+ 'defaults': defaults # 存储默认值
45
48
  }
46
49
  return wrapper
47
50
  return decorator
@@ -51,7 +54,19 @@ class KeywordManager:
51
54
  keyword_info = self._keywords.get(keyword_name)
52
55
  if not keyword_info:
53
56
  raise KeyError(f"未注册的关键字: {keyword_name}")
54
- return keyword_info['func'](**params)
57
+
58
+ # 应用默认值
59
+ final_params = {}
60
+ defaults = keyword_info.get('defaults', {})
61
+
62
+ # 首先设置所有默认值
63
+ for param_key, default_value in defaults.items():
64
+ final_params[param_key] = default_value
65
+
66
+ # 然后用传入的参数覆盖默认值
67
+ final_params.update(params)
68
+
69
+ return keyword_info['func'](**final_params)
55
70
 
56
71
  def get_keyword_info(self, keyword_name: str) -> Dict:
57
72
  """获取关键字信息"""
@@ -99,8 +114,9 @@ class KeywordManager:
99
114
  description="自定义的步骤名称,用于在报告中显示"
100
115
  ))
101
116
  for param in info['parameters']:
117
+ default_info = f" (默认值: {param.default})" if param.default is not None else ""
102
118
  docs.append(
103
- f" {param.name} ({param.mapping}): {param.description}")
119
+ f" {param.name} ({param.mapping}): {param.description}{default_info}")
104
120
  docs.append("")
105
121
  return "\n".join(docs)
106
122
 
@@ -48,7 +48,8 @@ def _compare_values(actual: Any, expected: Any, operator: str = "==") -> bool:
48
48
  Args:
49
49
  actual: 实际值
50
50
  expected: 预期值
51
- operator: 比较运算符 (==, !=, >, <, >=, <=, contains, not_contains, matches, and, or, not)
51
+ operator: 比较运算符 (==, !=, >, <, >=, <=, contains, not_contains,
52
+ matches, and, or, not)
52
53
 
53
54
  Returns:
54
55
  比较结果 (True/False)
@@ -96,8 +97,9 @@ def _compare_values(actual: Any, expected: Any, operator: str = "==") -> bool:
96
97
 
97
98
 
98
99
  @keyword_manager.register('断言', [
99
- {'name': '条件', 'mapping': 'condition', 'description': '断言条件表达式,例如: "${value} == 100" 或 "1 + 1 == 2"'},
100
- {'name': '消息', 'mapping': 'message', 'description': '断言失败时的错误消息'},
100
+ {'name': '条件', 'mapping': 'condition',
101
+ 'description': '断言条件表达式,例如: "${value} == 100" 或 "1 + 1 == 2"'},
102
+ {'name': '消息', 'mapping': 'message', 'description': '断言失败时的错误消息', 'default': '断言失败'},
101
103
  ])
102
104
  def assert_condition(**kwargs):
103
105
  """执行表达式断言
@@ -116,9 +118,11 @@ def assert_condition(**kwargs):
116
118
  message = kwargs.get('message', '断言失败')
117
119
  context = kwargs.get('context')
118
120
 
119
- # 简单解析表达式,支持 ==, !=, >, <, >=, <=, contains, not_contains, matches, in, and, or, not
121
+ # 简单解析表达式,支持 ==, !=, >, <, >=, <=, contains, not_contains,
122
+ # matches, in, and, or, not
120
123
  # 格式: "left_value operator right_value" 或 "boolean_expression"
121
- operators = ["==", "!=", ">", "<", ">=", "<=", "contains", "not_contains", "matches", "in", "and", "or", "not"]
124
+ operators = ["==", "!=", ">", "<", ">=", "<=", "contains", "not_contains",
125
+ "matches", "in", "and", "or", "not"]
122
126
 
123
127
  # 先检查是否包含这些操作符
124
128
  operator_used = None
@@ -132,7 +136,8 @@ def assert_condition(**kwargs):
132
136
  try:
133
137
  # 对条件进行变量替换
134
138
  if '${' in condition:
135
- condition = context.executor.variable_replacer.replace_in_string(condition)
139
+ condition = context.executor.variable_replacer.replace_in_string(
140
+ condition)
136
141
  # 尝试直接求值
137
142
  result = eval(condition)
138
143
  if not isinstance(result, bool):
@@ -141,7 +146,8 @@ def assert_condition(**kwargs):
141
146
  raise AssertionError(f"{message}. 布尔表达式求值为假: {condition}")
142
147
  return True
143
148
  except Exception as e:
144
- raise AssertionError(f"{message}. 无法解析条件表达式: {condition}. 错误: {str(e)}")
149
+ raise AssertionError(
150
+ f"{message}. 无法解析条件表达式: {condition}. 错误: {str(e)}")
145
151
 
146
152
  # 解析左值和右值
147
153
  left_value, right_value = condition.split(f" {operator_used} ", 1)
@@ -351,8 +357,8 @@ def assert_condition(**kwargs):
351
357
  {'name': 'JSON数据', 'mapping': 'json_data', 'description': 'JSON数据(字符串或对象)'},
352
358
  {'name': 'JSONPath', 'mapping': 'jsonpath', 'description': 'JSONPath表达式'},
353
359
  {'name': '预期值', 'mapping': 'expected_value', 'description': '预期的值'},
354
- {'name': '操作符', 'mapping': 'operator', 'description': '比较操作符,默认为"=="'},
355
- {'name': '消息', 'mapping': 'message', 'description': '断言失败时的错误消息'},
360
+ {'name': '操作符', 'mapping': 'operator', 'description': '比较操作符', 'default': '=='},
361
+ {'name': '消息', 'mapping': 'message', 'description': '断言失败时的错误消息', 'default': 'JSON断言失败'},
356
362
  ])
357
363
  def assert_json(**kwargs):
358
364
  """执行JSON断言
@@ -476,7 +482,7 @@ def extract_json(**kwargs):
476
482
  @keyword_manager.register('类型断言', [
477
483
  {'name': '值', 'mapping': 'value', 'description': '要检查的值'},
478
484
  {'name': '类型', 'mapping': 'type', 'description': '预期的类型 (string, number, boolean, list, object, null)'},
479
- {'name': '消息', 'mapping': 'message', 'description': '断言失败时的错误消息'},
485
+ {'name': '消息', 'mapping': 'message', 'description': '断言失败时的错误消息', 'default': '类型断言失败'},
480
486
  ])
481
487
  def assert_type(**kwargs):
482
488
  """断言值的类型
@@ -545,8 +551,8 @@ def assert_type(**kwargs):
545
551
  @keyword_manager.register('数据比较', [
546
552
  {'name': '实际值', 'mapping': 'actual', 'description': '实际值'},
547
553
  {'name': '预期值', 'mapping': 'expected', 'description': '预期值'},
548
- {'name': '操作符', 'mapping': 'operator', 'description': '比较操作符,默认为"=="'},
549
- {'name': '消息', 'mapping': 'message', 'description': '断言失败时的错误消息'},
554
+ {'name': '操作符', 'mapping': 'operator', 'description': '比较操作符', 'default': '=='},
555
+ {'name': '消息', 'mapping': 'message', 'description': '断言失败时的错误消息', 'default': '数据比较失败'},
550
556
  ])
551
557
  def compare_values(**kwargs):
552
558
  """比较两个值
@@ -223,14 +223,14 @@ def _normalize_retry_config(config, assert_retry_count=None, assert_retry_interv
223
223
 
224
224
 
225
225
  @keyword_manager.register('HTTP请求', [
226
- {'name': '客户端', 'mapping': 'client', 'description': '客户端名称,对应YAML变量文件中的客户端配置'},
226
+ {'name': '客户端', 'mapping': 'client', 'description': '客户端名称,对应YAML变量文件中的客户端配置', 'default': 'default'},
227
227
  {'name': '配置', 'mapping': 'config', 'description': '包含请求、捕获和断言的YAML配置'},
228
228
  {'name': '会话', 'mapping': 'session', 'description': '会话名称,用于在多个请求间保持会话状态'},
229
229
  {'name': '保存响应', 'mapping': 'save_response', 'description': '将完整响应保存到指定变量名中'},
230
- {'name': '禁用授权', 'mapping': 'disable_auth', 'description': '禁用客户端配置中的授权机制,默认为false'},
230
+ {'name': '禁用授权', 'mapping': 'disable_auth', 'description': '禁用客户端配置中的授权机制', 'default': False},
231
231
  {'name': '模板', 'mapping': 'template', 'description': '使用YAML变量文件中定义的请求模板'},
232
- {'name': '断言重试次数', 'mapping': 'assert_retry_count', 'description': '断言失败时的重试次数'},
233
- {'name': '断言重试间隔', 'mapping': 'assert_retry_interval', 'description': '断言重试间隔时间(秒)'}
232
+ {'name': '断言重试次数', 'mapping': 'assert_retry_count', 'description': '断言失败时的重试次数', 'default': 0},
233
+ {'name': '断言重试间隔', 'mapping': 'assert_retry_interval', 'description': '断言重试间隔时间(秒)', 'default': 1}
234
234
  ])
235
235
  def http_request(context, **kwargs):
236
236
  """执行HTTP请求
@@ -24,46 +24,54 @@ def return_result(**kwargs):
24
24
 
25
25
 
26
26
  @keyword_manager.register('等待', [
27
- {'name': '秒数', 'mapping': 'seconds', 'description': '等待的秒数,可以是小数'}
27
+ {'name': '秒数', 'mapping': 'seconds',
28
+ 'description': '等待的秒数,可以是小数', 'default': 1}
28
29
  ])
29
30
  def wait_seconds(**kwargs):
30
- """等待指定的秒数
31
+ """等待指定的时间
31
32
 
32
33
  Args:
33
- seconds: 等待的秒数,可以是小数表示毫秒级等待
34
+ seconds: 等待的秒数,默认为1秒
34
35
  """
35
- seconds = float(kwargs.get('seconds', 0))
36
+ seconds = float(kwargs.get('seconds', 1))
37
+
36
38
  with allure.step(f"等待 {seconds} 秒"):
37
39
  time.sleep(seconds)
38
- return True
40
+ allure.attach(
41
+ f"等待时间: {seconds} 秒",
42
+ name="等待完成",
43
+ attachment_type=allure.attachment_type.TEXT
44
+ )
39
45
 
40
46
 
41
47
  @keyword_manager.register('获取当前时间', [
42
- {'name': '格式', 'mapping': 'format', 'description': '时间格式,例如 "%Y-%m-%d %H:%M:%S",默认返回时间戳'},
43
- {'name': '时区', 'mapping': 'timezone', 'description': '时区,例如 "Asia/Shanghai",默认为本地时区'}
48
+ {'name': '格式', 'mapping': 'format',
49
+ 'description': '时间格式,例如 "%Y-%m-%d %H:%M:%S"', 'default': 'timestamp'},
50
+ {'name': '时区', 'mapping': 'timezone',
51
+ 'description': '时区,例如 "Asia/Shanghai"', 'default': 'Asia/Shanghai'}
44
52
  ])
45
53
  def get_current_time(**kwargs):
46
54
  """获取当前时间
47
55
 
48
56
  Args:
49
- format: 时间格式,如果不提供则返回时间戳
50
- timezone: 时区,默认为本地时区
57
+ format: 时间格式,如果设置为'timestamp'则返回时间戳
58
+ timezone: 时区,默认为Asia/Shanghai
51
59
 
52
60
  Returns:
53
- str: 格式化的时间字符串或时间戳
61
+ str/float: 格式化的时间字符串或时间戳
54
62
  """
55
- time_format = kwargs.get('format')
56
- timezone = kwargs.get('timezone')
63
+ format_str = kwargs.get('format', 'timestamp')
64
+ timezone_str = kwargs.get('timezone', 'Asia/Shanghai')
57
65
 
58
66
  # 获取当前时间
59
- if timezone:
67
+ if timezone_str and timezone_str != 'local':
60
68
  import pytz
61
69
  try:
62
- tz = pytz.timezone(timezone)
70
+ tz = pytz.timezone(timezone_str)
63
71
  current_time = datetime.datetime.now(tz)
64
72
  except Exception as e:
65
73
  allure.attach(
66
- f"时区设置异常: {str(e)}",
74
+ f"时区设置异常: {str(e)},使用本地时区",
67
75
  name="时区设置异常",
68
76
  attachment_type=allure.attachment_type.TEXT
69
77
  )
@@ -72,34 +80,36 @@ def get_current_time(**kwargs):
72
80
  current_time = datetime.datetime.now()
73
81
 
74
82
  # 格式化时间
75
- if time_format:
83
+ if format_str and format_str != 'timestamp':
76
84
  try:
77
- result = current_time.strftime(time_format)
85
+ result = current_time.strftime(format_str)
78
86
  except Exception as e:
79
87
  allure.attach(
80
- f"时间格式化异常: {str(e)}",
88
+ f"时间格式化异常: {str(e)},返回默认格式",
81
89
  name="时间格式化异常",
82
90
  attachment_type=allure.attachment_type.TEXT
83
91
  )
84
- result = str(current_time)
92
+ result = current_time.strftime('%Y-%m-%d %H:%M:%S')
85
93
  else:
86
94
  # 返回时间戳
87
- result = str(int(current_time.timestamp()))
95
+ result = int(current_time.timestamp())
88
96
 
89
97
  return result
90
98
 
91
99
 
92
100
  @keyword_manager.register('生成随机字符串', [
93
- {'name': '长度', 'mapping': 'length', 'description': '随机字符串的长度,默认为8'},
101
+ {'name': '长度', 'mapping': 'length',
102
+ 'description': '随机字符串的长度', 'default': 8},
94
103
  {'name': '类型', 'mapping': 'type',
95
- 'description': '字符类型:字母(letters)、数字(digits)、字母数字(alphanumeric)、全部(all),默认为字母数字'}
104
+ 'description': '字符类型:字母(letters)、数字(digits)、字母数字(alphanumeric)、全部(all)',
105
+ 'default': 'alphanumeric'}
96
106
  ])
97
107
  def generate_random_string(**kwargs):
98
- """生成随机字符串
108
+ """生成指定长度和类型的随机字符串
99
109
 
100
110
  Args:
101
- length: 随机字符串的长度
102
- type: 字符类型:字母、数字、字母数字、全部
111
+ length: 字符串长度
112
+ type: 字符类型
103
113
 
104
114
  Returns:
105
115
  str: 生成的随机字符串
@@ -134,12 +144,15 @@ def generate_random_string(**kwargs):
134
144
 
135
145
 
136
146
  @keyword_manager.register('生成随机数', [
137
- {'name': '最小值', 'mapping': 'min', 'description': '随机数的最小值,默认为0'},
138
- {'name': '最大值', 'mapping': 'max', 'description': '随机数的最大值,默认为100'},
139
- {'name': '小数位数', 'mapping': 'decimals', 'description': '小数位数,默认为0(整数)'}
147
+ {'name': '最小值', 'mapping': 'min',
148
+ 'description': '随机数的最小值', 'default': 0},
149
+ {'name': '最大值', 'mapping': 'max',
150
+ 'description': '随机数的最大值', 'default': 100},
151
+ {'name': '小数位数', 'mapping': 'decimals',
152
+ 'description': '小数位数,0表示整数', 'default': 0}
140
153
  ])
141
154
  def generate_random_number(**kwargs):
142
- """生成随机数
155
+ """生成指定范围内的随机数
143
156
 
144
157
  Args:
145
158
  min: 随机数的最小值
@@ -172,24 +185,27 @@ def generate_random_number(**kwargs):
172
185
 
173
186
  @keyword_manager.register('字符串操作', [
174
187
  {'name': '操作', 'mapping': 'operation',
175
- 'description': '操作类型:拼接(concat)、替换(replace)、分割(split)、大写(upper)、小写(lower)、去空格(strip)'},
188
+ 'description': '操作类型:拼接(concat)、替换(replace)、分割(split)、大写(upper)、小写(lower)、去空格(strip)',
189
+ 'default': 'strip'},
176
190
  {'name': '字符串', 'mapping': 'string', 'description': '要操作的字符串'},
177
- {'name': '参数1', 'mapping': 'param1', 'description': '操作参数1,根据操作类型不同而不同'},
178
- {'name': '参数2', 'mapping': 'param2', 'description': '操作参数2,根据操作类型不同而不同'}
191
+ {'name': '参数1', 'mapping': 'param1',
192
+ 'description': '操作参数1,根据操作类型不同而不同', 'default': ''},
193
+ {'name': '参数2', 'mapping': 'param2',
194
+ 'description': '操作参数2,根据操作类型不同而不同', 'default': ''}
179
195
  ])
180
196
  def string_operation(**kwargs):
181
197
  """字符串操作
182
198
 
183
199
  Args:
184
- operation: 操作类型
200
+ operation: 操作类型,默认为strip(去空格)
185
201
  string: 要操作的字符串
186
- param1: 操作参数1
187
- param2: 操作参数2
202
+ param1: 操作参数1,默认为空字符串
203
+ param2: 操作参数2,默认为空字符串
188
204
 
189
205
  Returns:
190
206
  str: 操作结果
191
207
  """
192
- operation = kwargs.get('operation', '').lower()
208
+ operation = kwargs.get('operation', 'strip').lower()
193
209
  string = str(kwargs.get('string', ''))
194
210
  param1 = kwargs.get('param1', '')
195
211
  param2 = kwargs.get('param2', '')
@@ -204,12 +220,16 @@ def string_operation(**kwargs):
204
220
  result = string.replace(str(param1), str(param2))
205
221
  elif operation == 'split':
206
222
  # 分割字符串
207
- result = string.split(str(param1))
208
- if param2 and param2.isdigit():
209
- # 如果提供了索引,返回指定位置的元素
210
- index = int(param2)
211
- if 0 <= index < len(result):
212
- result = result[index]
223
+ if param1: # 如果提供了分隔符
224
+ result = string.split(str(param1))
225
+ if param2 and param2.isdigit():
226
+ # 如果提供了索引,返回指定位置的元素
227
+ index = int(param2)
228
+ if 0 <= index < len(result):
229
+ result = result[index]
230
+ else:
231
+ # 默认按空格分割
232
+ result = string.split()
213
233
  elif operation == 'upper':
214
234
  # 转大写
215
235
  result = string.upper()
@@ -217,19 +237,21 @@ def string_operation(**kwargs):
217
237
  # 转小写
218
238
  result = string.lower()
219
239
  elif operation == 'strip':
220
- # 去空格
240
+ # 去空格(默认操作)
221
241
  result = string.strip()
222
242
  else:
223
243
  # 未知操作,返回原字符串
224
244
  allure.attach(
225
- f"未知的字符串操作: {operation}",
245
+ f"未知的字符串操作: {operation},使用默认操作strip",
226
246
  name="字符串操作错误",
227
247
  attachment_type=allure.attachment_type.TEXT
228
248
  )
249
+ result = string.strip()
229
250
 
230
251
  with allure.step(f"字符串操作: {operation}"):
231
252
  allure.attach(
232
- f"原字符串: {string}\n操作: {operation}\n参数1: {param1}\n参数2: {param2}\n结果: {result}",
253
+ f"原字符串: {string}\n操作: {operation}\n参数1: {param1}\n"
254
+ f"参数2: {param2}\n结果: {result}",
233
255
  name="字符串操作结果",
234
256
  attachment_type=allure.attachment_type.TEXT
235
257
  )
@@ -239,7 +261,8 @@ def string_operation(**kwargs):
239
261
 
240
262
  @keyword_manager.register('日志', [
241
263
  {'name': '级别', 'mapping': 'level',
242
- 'description': '日志级别:DEBUG, INFO, WARNING, ERROR, CRITICAL,默认为INFO'},
264
+ 'description': '日志级别:DEBUG, INFO, WARNING, ERROR, CRITICAL',
265
+ 'default': 'INFO'},
243
266
  {'name': '消息', 'mapping': 'message', 'description': '日志消息内容'}
244
267
  ])
245
268
  def log_message(**kwargs):
@@ -270,8 +293,10 @@ def log_message(**kwargs):
270
293
 
271
294
  @keyword_manager.register('执行命令', [
272
295
  {'name': '命令', 'mapping': 'command', 'description': '要执行的系统命令'},
273
- {'name': '超时', 'mapping': 'timeout', 'description': '命令执行超时时间(秒),默认为60秒'},
274
- {'name': '捕获输出', 'mapping': 'capture_output', 'description': '是否捕获命令输出,默认为True'}
296
+ {'name': '超时', 'mapping': 'timeout',
297
+ 'description': '命令执行超时时间(秒)', 'default': 60},
298
+ {'name': '捕获输出', 'mapping': 'capture_output',
299
+ 'description': '是否捕获命令输出', 'default': True}
275
300
  ])
276
301
  def execute_command(**kwargs):
277
302
  """执行系统命令
@@ -56,30 +56,51 @@ class RemoteKeywordClient:
56
56
  try:
57
57
  param_names = self.server.get_keyword_arguments(name)
58
58
  doc = self.server.get_keyword_documentation(name)
59
-
60
- print(f"注册远程关键字: {name}, 参数: {param_names}")
59
+
60
+ # 尝试获取参数详细信息(包括默认值)
61
+ param_details = []
62
+ try:
63
+ param_details = self.server.get_keyword_parameter_details(name)
64
+ except Exception as e:
65
+ print(f"获取关键字 {name} 的参数详细信息失败,使用基本信息: {e}")
66
+ # 如果新方法不可用,使用旧的方式
67
+ for param_name in param_names:
68
+ param_details.append({
69
+ 'name': param_name,
70
+ 'mapping': param_name,
71
+ 'description': f'远程关键字参数: {param_name}',
72
+ 'default': None
73
+ })
74
+
75
+ print(f"注册远程关键字: {name}, 参数详情: {param_details}")
61
76
 
62
77
  # 创建参数列表
63
78
  parameters = []
64
79
  param_mapping = {} # 为每个关键字创建参数映射
65
80
 
66
- for param_name in param_names:
81
+ for param_detail in param_details:
82
+ param_name = param_detail['name']
83
+ param_mapping_name = param_detail.get('mapping', param_name)
84
+ param_desc = param_detail.get('description', f'远程关键字参数: {param_name}')
85
+ param_default = param_detail.get('default')
86
+
67
87
  # 确保参数名称正确映射
68
- # 这里我们保持原始参数名称,但在执行时会进行正确映射
69
88
  parameters.append({
70
89
  'name': param_name,
71
- 'mapping': param_name, # 保持原始参数名称
72
- 'description': f'远程关键字参数: {param_name}'
90
+ 'mapping': param_mapping_name,
91
+ 'description': param_desc,
92
+ 'default': param_default # 添加默认值支持
73
93
  })
74
94
  # 添加到参数映射
75
- param_mapping[param_name] = param_name
95
+ param_mapping[param_name] = param_mapping_name
76
96
 
77
97
  # 添加步骤名称参数,这是所有关键字都应该有的
78
98
  if not any(p['name'] == '步骤名称' for p in parameters):
79
99
  parameters.append({
80
100
  'name': '步骤名称',
81
101
  'mapping': 'step_name',
82
- 'description': '自定义的步骤名称,用于在报告中显示'
102
+ 'description': '自定义的步骤名称,用于在报告中显示',
103
+ 'default': None
83
104
  })
84
105
  param_mapping['步骤名称'] = 'step_name'
85
106
 
@@ -93,6 +114,7 @@ class RemoteKeywordClient:
93
114
  'func': remote_func,
94
115
  'mapping': {p['name']: p['mapping'] for p in parameters},
95
116
  'parameters': [Parameter(**p) for p in parameters],
117
+ 'defaults': {p['mapping']: p['default'] for p in parameters if p['default'] is not None}, # 添加默认值支持
96
118
  'remote': True, # 标记为远程关键字
97
119
  'alias': self.alias,
98
120
  'original_name': name
@@ -101,7 +123,8 @@ class RemoteKeywordClient:
101
123
  # 缓存关键字信息
102
124
  self.keyword_cache[name] = {
103
125
  'parameters': param_names, # 注意这里只缓存原始参数,不包括步骤名称
104
- 'doc': doc
126
+ 'doc': doc,
127
+ 'param_details': param_details # 缓存详细参数信息
105
128
  }
106
129
 
107
130
  # 保存参数映射
@@ -141,6 +141,7 @@ class RemoteKeywordServer:
141
141
  self.server.register_function(self.get_keyword_names)
142
142
  self.server.register_function(self.run_keyword)
143
143
  self.server.register_function(self.get_keyword_arguments)
144
+ self.server.register_function(self.get_keyword_parameter_details)
144
145
  self.server.register_function(self.get_keyword_documentation)
145
146
  self.server.register_function(self.authenticate)
146
147
 
@@ -289,6 +290,30 @@ class RemoteKeywordServer:
289
290
 
290
291
  return [param.name for param in keyword_info['parameters']]
291
292
 
293
+ def get_keyword_parameter_details(self, name):
294
+ """获取关键字的参数详细信息,包括默认值
295
+
296
+ Args:
297
+ name: 关键字名称
298
+
299
+ Returns:
300
+ list: 参数详细信息列表,每个元素包含name, mapping, description, default
301
+ """
302
+ keyword_info = keyword_manager.get_keyword_info(name)
303
+ if not keyword_info:
304
+ return []
305
+
306
+ param_details = []
307
+ for param in keyword_info['parameters']:
308
+ param_details.append({
309
+ 'name': param.name,
310
+ 'mapping': param.mapping,
311
+ 'description': param.description,
312
+ 'default': param.default
313
+ })
314
+
315
+ return param_details
316
+
292
317
  def get_keyword_documentation(self, name):
293
318
  """获取关键字的文档信息"""
294
319
  keyword_info = keyword_manager.get_keyword_info(name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-dsl
3
- Version: 0.9.1
3
+ Version: 0.10.0
4
4
  Summary: A DSL testing framework based on pytest
5
5
  Author: Chen Shuanglin
6
6
  License: MIT
@@ -42,11 +42,12 @@ pytest-dsl是一个革命性的关键字驱动测试框架,基于pytest构建
42
42
  ## ✨ 核心特性
43
43
 
44
44
  - 🎯 **门槛上手低** - 自然语言风格,只需少量编程基础
45
- - 🔧 **高度可扩展** - 轻松创建自定义关键字
45
+ - 🔧 **高度可扩展** - 轻松创建自定义关键字,支持参数默认值
46
46
  - 🌐 **分布式执行** - 支持远程关键字调用
47
47
  - 🔄 **无缝集成** - 完美兼容pytest生态
48
48
  - 📊 **丰富报告** - 集成Allure测试报告
49
49
  - 🛡️ **企业级** - 支持变量管理、环境隔离
50
+ - ⚡ **智能简化** - 参数默认值让DSL更加简洁易读
50
51
 
51
52
  ## 🚀 5分钟快速开始
52
53
 
@@ -850,17 +851,19 @@ def database_query(**kwargs):
850
851
 
851
852
  @keyword_manager.register('发送邮件', [
852
853
  {'name': '收件人', 'mapping': 'to_email', 'description': '收件人邮箱'},
853
- {'name': '主题', 'mapping': 'subject', 'description': '邮件主题'},
854
- {'name': '内容', 'mapping': 'content', 'description': '邮件内容'}
854
+ {'name': '主题', 'mapping': 'subject', 'description': '邮件主题', 'default': '测试邮件'},
855
+ {'name': '内容', 'mapping': 'content', 'description': '邮件内容', 'default': '这是一封测试邮件'},
856
+ {'name': '优先级', 'mapping': 'priority', 'description': '邮件优先级', 'default': 'normal'}
855
857
  ])
856
858
  def send_email(**kwargs):
857
859
  """发送邮件通知"""
858
860
  to_email = kwargs.get('to_email')
859
- subject = kwargs.get('subject')
860
- content = kwargs.get('content')
861
+ subject = kwargs.get('subject', '测试邮件')
862
+ content = kwargs.get('content', '这是一封测试邮件')
863
+ priority = kwargs.get('priority', 'normal')
861
864
 
862
865
  # 实现邮件发送逻辑
863
- print(f"发送邮件到 {to_email}: {subject}")
866
+ print(f"发送邮件到 {to_email}: {subject} (优先级: {priority})")
864
867
 
865
868
  return True
866
869
  ```
@@ -874,10 +877,71 @@ def send_email(**kwargs):
874
877
  users = [数据库查询], 查询语句: "SELECT * FROM users WHERE active = 1"
875
878
  [打印], 内容: "查询到 ${len(users)} 个活跃用户"
876
879
 
877
- # 发送测试报告邮件
878
- [发送邮件], 收件人: "admin@example.com", 主题: "测试报告", 内容: "测试已完成"
880
+ # 发送测试报告邮件 - 使用默认值
881
+ [发送邮件], 收件人: "admin@example.com" # 主题和内容使用默认值
882
+
883
+ # 发送自定义邮件 - 覆盖默认值
884
+ [发送邮件], 收件人: "dev@example.com", 主题: "部署完成", 内容: "系统已成功部署到生产环境"
885
+ ```
886
+
887
+ ### 5. 参数默认值功能 🆕
888
+
889
+ pytest-dsl 现在支持为关键字参数设置默认值,让DSL编写更加简洁:
890
+
891
+ #### 定义带默认值的关键字
892
+
893
+ ```python
894
+ from pytest_dsl.core.keyword_manager import keyword_manager
895
+
896
+ @keyword_manager.register('HTTP请求', [
897
+ {'name': '地址', 'mapping': 'url', 'description': '请求地址'},
898
+ {'name': '方法', 'mapping': 'method', 'description': 'HTTP方法', 'default': 'GET'},
899
+ {'name': '超时', 'mapping': 'timeout', 'description': '超时时间(秒)', 'default': 30},
900
+ {'name': '重试次数', 'mapping': 'retries', 'description': '重试次数', 'default': 3},
901
+ {'name': '验证SSL', 'mapping': 'verify_ssl', 'description': '是否验证SSL证书', 'default': True}
902
+ ])
903
+ def http_request(**kwargs):
904
+ """HTTP请求关键字,支持默认值"""
905
+ url = kwargs.get('url')
906
+ method = kwargs.get('method', 'GET') # 默认值也会自动应用
907
+ timeout = kwargs.get('timeout', 30)
908
+ retries = kwargs.get('retries', 3)
909
+ verify_ssl = kwargs.get('verify_ssl', True)
910
+
911
+ # 执行HTTP请求逻辑
912
+ return {"status": "success", "method": method, "url": url}
879
913
  ```
880
914
 
915
+ #### 在DSL中使用默认值
916
+
917
+ ```python
918
+ @name: "默认值功能演示"
919
+
920
+ # 只传递必需参数,其他使用默认值
921
+ response1 = [HTTP请求], 地址: "https://api.example.com/users"
922
+ # 等价于:方法: "GET", 超时: 30, 重试次数: 3, 验证SSL: True
923
+
924
+ # 部分覆盖默认值
925
+ response2 = [HTTP请求], 地址: "https://api.example.com/users", 方法: "POST", 超时: 60
926
+ # 只覆盖方法和超时,重试次数和SSL验证仍使用默认值
927
+
928
+ # 内置关键字也支持默认值
929
+ random_num = [生成随机数] # 使用默认范围 0-100,整数
930
+ custom_num = [生成随机数], 最大值: 50 # 只修改最大值,其他保持默认
931
+
932
+ # 生成随机字符串
933
+ default_string = [生成随机字符串] # 长度8,字母数字混合
934
+ custom_string = [生成随机字符串], 长度: 12, 类型: "letters" # 自定义长度和类型
935
+ ```
936
+
937
+ #### 默认值的优势
938
+
939
+ - **🎯 简化调用** - 只需传递关键参数,常用配置自动应用
940
+ - **🔧 灵活覆盖** - 可选择性地覆盖任何默认值
941
+ - **📖 提高可读性** - DSL更加简洁,重点突出
942
+ - **🛡️ 减少错误** - 避免重复配置常用参数
943
+ - **🌐 远程支持** - 远程关键字也完整支持默认值功能
944
+
881
945
  #### 支持远程模式的关键字
882
946
 
883
947
  ```python
@@ -1037,20 +1101,6 @@ CMD ["pytest-dsl", "tests/", "--yaml-vars", "config/prod.yaml"]
1037
1101
  - **集成测试** - 跨系统测试协调
1038
1102
  - **性能测试** - 结合其他工具进行性能测试
1039
1103
 
1040
- ## 🤝 贡献与支持
1041
-
1042
- 我们欢迎您的贡献和反馈!
1043
-
1044
- - 🐛 [报告问题](https://github.com/your-repo/pytest-dsl/issues)
1045
- - 💡 [功能建议](https://github.com/your-repo/pytest-dsl/discussions)
1046
- - 🔧 [提交PR](https://github.com/your-repo/pytest-dsl/pulls)
1047
-
1048
- ## 📄 许可证
1049
-
1050
- MIT License - 详见 [LICENSE](LICENSE) 文件
1051
-
1052
- ---
1053
-
1054
1104
  ## 📋 示例验证
1055
1105
 
1056
1106
  本README.md中的大部分示例都已经过验证,确保可以正常运行。验证示例位于 `examples/readme_validation/` 目录中。
@@ -1086,3 +1136,18 @@ pytest-dsl api_basic.dsl
1086
1136
  ---
1087
1137
 
1088
1138
  🚀 **开始使用pytest-dsl,让测试自动化变得简单而强大!**
1139
+
1140
+
1141
+ ## 🤝 贡献与支持
1142
+
1143
+ 我们欢迎您的贡献和反馈!
1144
+
1145
+ - 🐛 [报告问题](https://github.com/felix-1991/pytest-dsl/issues)
1146
+ - 💡 [功能建议](https://github.com/felix-1991/pytest-dsl/discussions)
1147
+ - 🔧 [提交PR](https://github.com/felix-1991/pytest-dsl/pulls)
1148
+
1149
+ ## 📄 许可证
1150
+
1151
+ MIT License - 详见 [LICENSE](LICENSE) 文件
1152
+
1153
+ ---
@@ -1,5 +1,5 @@
1
1
  pytest_dsl/__init__.py,sha256=FzwXGvmuvMhRBKxvCdh1h-yJ2wUOnDxcTbU4Nt5fHn8,301
2
- pytest_dsl/cli.py,sha256=x3zSHJqAGUNPUsJfhFRzVVwRE0zM02Lcr2bx_V8v4YA,15806
2
+ pytest_dsl/cli.py,sha256=1lsjCkfRk-goGoi4E37FjFTGrg74idyB1ktV2J1F2IM,17763
3
3
  pytest_dsl/conftest_adapter.py,sha256=cevEb0oEZKTZfUrGe1-CmkFByxKhUtjuurBJP7kpLc0,149
4
4
  pytest_dsl/main_adapter.py,sha256=pUIPN_EzY3JCDlYK7yF_OeLDVqni8vtG15G7gVzPJXg,181
5
5
  pytest_dsl/plugin.py,sha256=MEQcdK0xdxwxCxPEDLNHX_kGF9Jc7bNxlNi4mx588DU,1190
@@ -14,7 +14,7 @@ pytest_dsl/core/dsl_executor_utils.py,sha256=cFoR2p3qQ2pb-UhkoefleK-zbuFqf0aBLh2
14
14
  pytest_dsl/core/global_context.py,sha256=NcEcS2V61MT70tgAsGsFWQq0P3mKjtHQr1rgT3yTcyY,3535
15
15
  pytest_dsl/core/http_client.py,sha256=1AHqtM_fkXf8JrM0ljMsJwUkyt-ysjR16NoyCckHfGc,15810
16
16
  pytest_dsl/core/http_request.py,sha256=nGMlx0mFc7rDLIdp9GJ3e09OQH3ryUA56yJdRZ99dOQ,57445
17
- pytest_dsl/core/keyword_manager.py,sha256=FtPsXlI7PxvVQMJfDN_nQYvRhkag5twvaHXjELQsCEo,4068
17
+ pytest_dsl/core/keyword_manager.py,sha256=vm78au2s8waWD7k2sxltkOhJNgEJOGccDBTr8fgN3eU,4773
18
18
  pytest_dsl/core/lexer.py,sha256=WaLzt9IhtHiA90Fg2WGgfVztveCUhtgxzANBaEiy-F8,4347
19
19
  pytest_dsl/core/parser.py,sha256=xxy6yC6NdwHxln200aIuaWWN3w44uI8kkNlw8PTVpYI,11855
20
20
  pytest_dsl/core/parsetab.py,sha256=aN-2RRTr3MSbMyfe-9zOj_t96Xu84avE29GWqH6nqmg,31472
@@ -55,18 +55,18 @@ pytest_dsl/examples/quickstart/api_basics.auto,sha256=SrDRBASqy5ZMXnmfczuKNCXoTi
55
55
  pytest_dsl/examples/quickstart/assertions.auto,sha256=FWrwod3L8oxuKCnA-lnVO2fzs5bZsXWaVqd6zehKrzw,859
56
56
  pytest_dsl/examples/quickstart/loops.auto,sha256=ZNZ6qP636v8QMY8QRyTUBB43gWCsqHbpPQw2RqamvOk,516
57
57
  pytest_dsl/keywords/__init__.py,sha256=5aiyPU_t1UiB2MEZ6M9ffOKnV1mFT_2YHxnZvyWaBNI,372
58
- pytest_dsl/keywords/assertion_keywords.py,sha256=WOCGP7WX2wZ6mQPDGmi38LWdG2NaThHoNU54xc8VpxI,23027
58
+ pytest_dsl/keywords/assertion_keywords.py,sha256=obW06H_3AizsvEM_9VE2JVuwvgrNVqP1kUTDd3U1SMk,23240
59
59
  pytest_dsl/keywords/global_keywords.py,sha256=4yw5yeXoGf_4W26F39EA2Pp-mH9GiKGy2jKgFO9a_wM,2509
60
- pytest_dsl/keywords/http_keywords.py,sha256=DY1SvUgOziN6Ga-QgE0q4XFd2qGGwvbv1B_k2gTdPLw,25554
61
- pytest_dsl/keywords/system_keywords.py,sha256=n_jRrMvSv2v6Pm_amokfyLNVOLYP7CFWbBE3_dlO7h4,11299
60
+ pytest_dsl/keywords/http_keywords.py,sha256=B2IoWZjmAk0bQf1o5hHhHEDHpAl010PrDVA4iJ1mLk0,25605
61
+ pytest_dsl/keywords/system_keywords.py,sha256=e65Mzyt56FYmw0vHegajLUSLeEYVI9Y-WSB1h6SKKLo,12089
62
62
  pytest_dsl/remote/__init__.py,sha256=syRSxTlTUfdAPleJnVS4MykRyEN8_SKiqlsn6SlIK8k,120
63
63
  pytest_dsl/remote/hook_manager.py,sha256=0hwRKP8yhcnfAnrrnZGVT-S0TBgo6c0A4qO5XRpvV1U,4899
64
- pytest_dsl/remote/keyword_client.py,sha256=Zem86qW6TudPAiiqU8wAFQeCoBl5urPRiAhFDPkAGgQ,15618
65
- pytest_dsl/remote/keyword_server.py,sha256=5nKLUIqgAVJoLL2t2dHaJb9S95z4K3UusSMEBylU6X4,20549
64
+ pytest_dsl/remote/keyword_client.py,sha256=doXmg2Aenx9SclKtd5bT3n-DkxGK3MnMRwn4dpdug8s,16848
65
+ pytest_dsl/remote/keyword_server.py,sha256=7eRBHXyTC2AyXBeXMHOYLjgj570ioiAhnq4CuEnkak4,21386
66
66
  pytest_dsl/remote/variable_bridge.py,sha256=KG2xTZcUExb8kNL-rR-IPFJmYGfo7ciE4W7xzet59sw,5520
67
- pytest_dsl-0.9.1.dist-info/licenses/LICENSE,sha256=Rguy8cb9sYhK6cmrBdXvwh94rKVDh2tVZEWptsHIsVM,1071
68
- pytest_dsl-0.9.1.dist-info/METADATA,sha256=tmPXjA1YZtuduO_Fus6vujf1vMOYxx3L2n7c7ZV4oyc,26712
69
- pytest_dsl-0.9.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
70
- pytest_dsl-0.9.1.dist-info/entry_points.txt,sha256=PLOBbH02OGY1XR1JDKIZB1Em87loUvbgMRWaag-5FhY,204
71
- pytest_dsl-0.9.1.dist-info/top_level.txt,sha256=4CrSx4uNqxj7NvK6k1y2JZrSrJSzi-UvPZdqpUhumWM,11
72
- pytest_dsl-0.9.1.dist-info/RECORD,,
67
+ pytest_dsl-0.10.0.dist-info/licenses/LICENSE,sha256=Rguy8cb9sYhK6cmrBdXvwh94rKVDh2tVZEWptsHIsVM,1071
68
+ pytest_dsl-0.10.0.dist-info/METADATA,sha256=wzdKD6pqnD0Xj4xP2GlWg5R0r5JewGH2hk7-rHTadZs,29613
69
+ pytest_dsl-0.10.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
70
+ pytest_dsl-0.10.0.dist-info/entry_points.txt,sha256=PLOBbH02OGY1XR1JDKIZB1Em87loUvbgMRWaag-5FhY,204
71
+ pytest_dsl-0.10.0.dist-info/top_level.txt,sha256=4CrSx4uNqxj7NvK6k1y2JZrSrJSzi-UvPZdqpUhumWM,11
72
+ pytest_dsl-0.10.0.dist-info/RECORD,,