pytest-dsl 0.8.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
@@ -6,7 +6,6 @@ pytest-dsl命令行入口
6
6
 
7
7
  import sys
8
8
  import argparse
9
- import pytest
10
9
  import os
11
10
  from pathlib import Path
12
11
 
@@ -14,8 +13,13 @@ from pytest_dsl.core.lexer import get_lexer
14
13
  from pytest_dsl.core.parser import get_parser
15
14
  from pytest_dsl.core.dsl_executor import DSLExecutor
16
15
  from pytest_dsl.core.yaml_loader import load_yaml_variables_from_args
17
- from pytest_dsl.core.auto_directory import SETUP_FILE_NAME, TEARDOWN_FILE_NAME, execute_hook_file
18
- from pytest_dsl.core.plugin_discovery import load_all_plugins
16
+ from pytest_dsl.core.auto_directory import (
17
+ SETUP_FILE_NAME, TEARDOWN_FILE_NAME, execute_hook_file
18
+ )
19
+ from pytest_dsl.core.plugin_discovery import (
20
+ load_all_plugins, scan_local_keywords
21
+ )
22
+ from pytest_dsl.core.keyword_manager import keyword_manager
19
23
 
20
24
 
21
25
  def read_file(filename):
@@ -26,14 +30,296 @@ def read_file(filename):
26
30
 
27
31
  def parse_args():
28
32
  """解析命令行参数"""
29
- parser = argparse.ArgumentParser(description='执行DSL测试文件')
30
- parser.add_argument('path', help='要执行的DSL文件路径或包含DSL文件的目录')
31
- parser.add_argument('--yaml-vars', action='append', default=[],
32
- help='YAML变量文件路径,可以指定多个文件 (例如: --yaml-vars vars1.yaml --yaml-vars vars2.yaml)')
33
- parser.add_argument('--yaml-vars-dir', default=None,
34
- help='YAML变量文件目录路径,将加载该目录下所有.yaml文件')
33
+ import sys
34
+ argv = sys.argv[1:] # 去掉脚本名
35
35
 
36
- return parser.parse_args()
36
+ # 检查是否使用了子命令格式
37
+ if argv and argv[0] in ['run', 'list-keywords']:
38
+ # 使用新的子命令格式
39
+ parser = argparse.ArgumentParser(description='执行DSL测试文件')
40
+ subparsers = parser.add_subparsers(dest='command', help='可用命令')
41
+
42
+ # 执行命令
43
+ run_parser = subparsers.add_parser('run', help='执行DSL文件')
44
+ run_parser.add_argument(
45
+ 'path',
46
+ help='要执行的DSL文件路径或包含DSL文件的目录'
47
+ )
48
+ run_parser.add_argument(
49
+ '--yaml-vars', action='append', default=[],
50
+ help='YAML变量文件路径,可以指定多个文件 '
51
+ '(例如: --yaml-vars vars1.yaml '
52
+ '--yaml-vars vars2.yaml)'
53
+ )
54
+ run_parser.add_argument(
55
+ '--yaml-vars-dir', default=None,
56
+ help='YAML变量文件目录路径,'
57
+ '将加载该目录下所有.yaml文件'
58
+ )
59
+
60
+ # 关键字列表命令
61
+ list_parser = subparsers.add_parser(
62
+ 'list-keywords',
63
+ help='罗列所有可用关键字和参数信息'
64
+ )
65
+ list_parser.add_argument(
66
+ '--format', choices=['text', 'json'],
67
+ default='text',
68
+ help='输出格式:text(默认) 或 json'
69
+ )
70
+ list_parser.add_argument(
71
+ '--filter', type=str, default=None,
72
+ help='过滤关键字名称(支持部分匹配)'
73
+ )
74
+ list_parser.add_argument(
75
+ '--category',
76
+ choices=['builtin', 'custom', 'remote', 'all'],
77
+ default='all',
78
+ help='关键字类别:builtin(内置)、custom(自定义)、'
79
+ 'remote(远程)、all(全部,默认)'
80
+ )
81
+
82
+ return parser.parse_args(argv)
83
+ else:
84
+ # 向后兼容模式
85
+ parser = argparse.ArgumentParser(description='执行DSL测试文件')
86
+
87
+ # 检查是否是list-keywords的旧格式
88
+ if '--list-keywords' in argv:
89
+ parser.add_argument('--list-keywords', action='store_true')
90
+ parser.add_argument(
91
+ '--format', choices=['text', 'json'], default='text'
92
+ )
93
+ parser.add_argument('--filter', type=str, default=None)
94
+ parser.add_argument(
95
+ '--category',
96
+ choices=['builtin', 'custom', 'remote', 'all'],
97
+ default='all'
98
+ )
99
+ parser.add_argument('path', nargs='?') # 可选的路径参数
100
+ parser.add_argument(
101
+ '--yaml-vars', action='append', default=[]
102
+ )
103
+ parser.add_argument('--yaml-vars-dir', default=None)
104
+
105
+ args = parser.parse_args(argv)
106
+ args.command = 'list-keywords-compat' # 标记为兼容模式
107
+ else:
108
+ # 默认为run命令的向后兼容模式
109
+ parser.add_argument('path', nargs='?')
110
+ parser.add_argument(
111
+ '--yaml-vars', action='append', default=[]
112
+ )
113
+ parser.add_argument('--yaml-vars-dir', default=None)
114
+
115
+ args = parser.parse_args(argv)
116
+ args.command = 'run-compat' # 标记为兼容模式
117
+
118
+ return args
119
+
120
+
121
+ def load_all_keywords():
122
+ """加载所有可用的关键字"""
123
+ # 首先导入内置关键字模块,确保内置关键字被注册
124
+ try:
125
+ import pytest_dsl.keywords # noqa: F401
126
+ print("内置关键字模块加载完成")
127
+ except ImportError as e:
128
+ print(f"加载内置关键字模块失败: {e}")
129
+
130
+ # 加载已安装的关键字插件
131
+ load_all_plugins()
132
+
133
+ # 扫描本地关键字
134
+ scan_local_keywords()
135
+
136
+
137
+ def categorize_keyword(keyword_name, keyword_info):
138
+ """判断关键字的类别"""
139
+ if keyword_info.get('remote', False):
140
+ return 'remote'
141
+
142
+ # 检查是否是内置关键字(通过检查函数所在模块)
143
+ func = keyword_info.get('func')
144
+ if func and hasattr(func, '__module__'):
145
+ module_name = func.__module__
146
+ if module_name and module_name.startswith('pytest_dsl.keywords'):
147
+ return 'builtin'
148
+
149
+ return 'custom'
150
+
151
+
152
+ def format_keyword_info_text(keyword_name, keyword_info, show_category=True):
153
+ """格式化关键字信息为文本格式"""
154
+ lines = []
155
+
156
+ # 关键字名称和类别
157
+ category = categorize_keyword(keyword_name, keyword_info)
158
+ category_names = {'builtin': '内置', 'custom': '自定义', 'remote': '远程'}
159
+
160
+ if show_category:
161
+ category_display = category_names.get(category, '未知')
162
+ lines.append(f"关键字: {keyword_name} [{category_display}]")
163
+ else:
164
+ lines.append(f"关键字: {keyword_name}")
165
+
166
+ # 远程关键字特殊标识
167
+ if keyword_info.get('remote', False):
168
+ alias = keyword_info.get('alias', '未知')
169
+ original_name = keyword_info.get('original_name', keyword_name)
170
+ lines.append(f" 远程服务器: {alias}")
171
+ lines.append(f" 原始名称: {original_name}")
172
+
173
+ # 参数信息
174
+ parameters = keyword_info.get('parameters', [])
175
+ if parameters:
176
+ lines.append(" 参数:")
177
+ for param in parameters:
178
+ param_name = getattr(param, 'name', str(param))
179
+ param_mapping = getattr(param, 'mapping', '')
180
+ param_desc = getattr(param, 'description', '')
181
+
182
+ if param_mapping and param_mapping != param_name:
183
+ lines.append(
184
+ f" {param_name} ({param_mapping}): {param_desc}"
185
+ )
186
+ else:
187
+ lines.append(f" {param_name}: {param_desc}")
188
+ else:
189
+ lines.append(" 参数: 无")
190
+
191
+ # 函数文档
192
+ func = keyword_info.get('func')
193
+ if func and hasattr(func, '__doc__') and func.__doc__:
194
+ lines.append(f" 说明: {func.__doc__.strip()}")
195
+
196
+ return '\n'.join(lines)
197
+
198
+
199
+ def format_keyword_info_json(keyword_name, keyword_info):
200
+ """格式化关键字信息为JSON格式"""
201
+ category = categorize_keyword(keyword_name, keyword_info)
202
+
203
+ keyword_data = {
204
+ 'name': keyword_name,
205
+ 'category': category,
206
+ 'parameters': []
207
+ }
208
+
209
+ # 远程关键字特殊信息
210
+ if keyword_info.get('remote', False):
211
+ keyword_data['remote'] = {
212
+ 'alias': keyword_info.get('alias', ''),
213
+ 'original_name': keyword_info.get('original_name', keyword_name)
214
+ }
215
+
216
+ # 参数信息
217
+ parameters = keyword_info.get('parameters', [])
218
+ for param in parameters:
219
+ param_data = {
220
+ 'name': getattr(param, 'name', str(param)),
221
+ 'mapping': getattr(param, 'mapping', ''),
222
+ 'description': getattr(param, 'description', '')
223
+ }
224
+ keyword_data['parameters'].append(param_data)
225
+
226
+ # 函数文档
227
+ func = keyword_info.get('func')
228
+ if func and hasattr(func, '__doc__') and func.__doc__:
229
+ keyword_data['documentation'] = func.__doc__.strip()
230
+
231
+ return keyword_data
232
+
233
+
234
+ def list_keywords(output_format='text', name_filter=None,
235
+ category_filter='all'):
236
+ """罗列所有关键字信息"""
237
+ import json
238
+
239
+ print("正在加载关键字...")
240
+ load_all_keywords()
241
+
242
+ # 获取所有注册的关键字
243
+ all_keywords = keyword_manager._keywords
244
+
245
+ if not all_keywords:
246
+ print("未发现任何关键字")
247
+ return
248
+
249
+ # 过滤关键字
250
+ filtered_keywords = {}
251
+
252
+ for name, info in all_keywords.items():
253
+ # 名称过滤
254
+ if name_filter and name_filter.lower() not in name.lower():
255
+ continue
256
+
257
+ # 类别过滤
258
+ if category_filter != 'all':
259
+ keyword_category = categorize_keyword(name, info)
260
+ if keyword_category != category_filter:
261
+ continue
262
+
263
+ filtered_keywords[name] = info
264
+
265
+ if not filtered_keywords:
266
+ if name_filter:
267
+ print(f"未找到包含 '{name_filter}' 的关键字")
268
+ else:
269
+ print(f"未找到 {category_filter} 类别的关键字")
270
+ return
271
+
272
+ # 输出统计信息
273
+ total_count = len(filtered_keywords)
274
+ category_counts = {}
275
+ for name, info in filtered_keywords.items():
276
+ cat = categorize_keyword(name, info)
277
+ category_counts[cat] = category_counts.get(cat, 0) + 1
278
+
279
+ if output_format == 'text':
280
+ print(f"\n找到 {total_count} 个关键字:")
281
+ for cat, count in category_counts.items():
282
+ cat_names = {'builtin': '内置', 'custom': '自定义', 'remote': '远程'}
283
+ print(f" {cat_names.get(cat, cat)}: {count} 个")
284
+ print("-" * 60)
285
+
286
+ # 按类别分组显示
287
+ for category in ['builtin', 'custom', 'remote']:
288
+ cat_keywords = {
289
+ name: info for name, info in filtered_keywords.items()
290
+ if categorize_keyword(name, info) == category
291
+ }
292
+
293
+ if cat_keywords:
294
+ cat_names = {
295
+ 'builtin': '内置关键字',
296
+ 'custom': '自定义关键字',
297
+ 'remote': '远程关键字'
298
+ }
299
+ print(f"\n=== {cat_names[category]} ===")
300
+
301
+ for name in sorted(cat_keywords.keys()):
302
+ info = cat_keywords[name]
303
+ print()
304
+ print(format_keyword_info_text(
305
+ name, info, show_category=False
306
+ ))
307
+
308
+ elif output_format == 'json':
309
+ keywords_data = {
310
+ 'summary': {
311
+ 'total_count': total_count,
312
+ 'category_counts': category_counts
313
+ },
314
+ 'keywords': []
315
+ }
316
+
317
+ for name in sorted(filtered_keywords.keys()):
318
+ info = filtered_keywords[name]
319
+ keyword_data = format_keyword_info_json(name, info)
320
+ keywords_data['keywords'].append(keyword_data)
321
+
322
+ print(json.dumps(keywords_data, ensure_ascii=False, indent=2))
37
323
 
38
324
 
39
325
  def load_yaml_variables(args):
@@ -68,18 +354,22 @@ def find_dsl_files(directory):
68
354
  dsl_files = []
69
355
  for root, _, files in os.walk(directory):
70
356
  for file in files:
71
- if file.endswith(('.dsl', '.auto')) and file not in [SETUP_FILE_NAME, TEARDOWN_FILE_NAME]:
357
+ if (file.endswith(('.dsl', '.auto')) and
358
+ file not in [SETUP_FILE_NAME, TEARDOWN_FILE_NAME]):
72
359
  dsl_files.append(os.path.join(root, file))
73
360
  return dsl_files
74
361
 
75
362
 
76
- def main():
77
- """命令行入口点"""
78
- args = parse_args()
363
+ def run_dsl_tests(args):
364
+ """执行DSL测试的主函数"""
79
365
  path = args.path
80
366
 
367
+ if not path:
368
+ print("错误: 必须指定要执行的DSL文件路径或目录")
369
+ sys.exit(1)
370
+
81
371
  # 加载内置关键字插件
82
- load_all_plugins()
372
+ load_all_keywords()
83
373
 
84
374
  # 加载YAML变量(包括远程服务器自动连接)
85
375
  load_yaml_variables(args)
@@ -134,5 +424,62 @@ def main():
134
424
  sys.exit(1)
135
425
 
136
426
 
427
+ def main():
428
+ """命令行入口点"""
429
+ args = parse_args()
430
+
431
+ # 处理子命令
432
+ if args.command == 'list-keywords':
433
+ list_keywords(
434
+ output_format=args.format,
435
+ name_filter=args.filter,
436
+ category_filter=args.category
437
+ )
438
+ elif args.command == 'run':
439
+ run_dsl_tests(args)
440
+ elif args.command == 'list-keywords-compat':
441
+ # 向后兼容:旧的--list-keywords格式
442
+ list_keywords(
443
+ output_format=args.format,
444
+ name_filter=args.filter,
445
+ category_filter=args.category
446
+ )
447
+ elif args.command == 'run-compat':
448
+ # 向后兼容:默认执行DSL测试
449
+ run_dsl_tests(args)
450
+ else:
451
+ # 如果没有匹配的命令,显示帮助
452
+ print("错误: 未知命令")
453
+ sys.exit(1)
454
+
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
+
137
484
  if __name__ == '__main__':
138
485
  main()
@@ -10,38 +10,38 @@ from pytest_dsl.core.context import TestContext
10
10
 
11
11
  class CustomKeywordManager:
12
12
  """自定义关键字管理器
13
-
13
+
14
14
  负责加载和注册自定义关键字
15
15
  """
16
-
16
+
17
17
  def __init__(self):
18
18
  """初始化自定义关键字管理器"""
19
19
  self.resource_cache = {} # 缓存已加载的资源文件
20
20
  self.resource_paths = [] # 资源文件搜索路径
21
-
21
+
22
22
  def add_resource_path(self, path: str) -> None:
23
23
  """添加资源文件搜索路径
24
-
24
+
25
25
  Args:
26
26
  path: 资源文件路径
27
27
  """
28
28
  if path not in self.resource_paths:
29
29
  self.resource_paths.append(path)
30
-
30
+
31
31
  def load_resource_file(self, file_path: str) -> None:
32
32
  """加载资源文件
33
-
33
+
34
34
  Args:
35
35
  file_path: 资源文件路径
36
36
  """
37
37
  # 规范化路径,解决路径叠加的问题
38
38
  file_path = os.path.normpath(file_path)
39
-
39
+
40
40
  # 如果已经缓存,则跳过
41
41
  absolute_path = os.path.abspath(file_path)
42
42
  if absolute_path in self.resource_cache:
43
43
  return
44
-
44
+
45
45
  # 读取文件内容
46
46
  if not os.path.exists(file_path):
47
47
  # 尝试在资源路径中查找
@@ -54,84 +54,85 @@ class CustomKeywordManager:
54
54
  else:
55
55
  # 如果文件不存在,尝试在根项目目录中查找
56
56
  # 一般情况下文件路径可能是相对于项目根目录的
57
- project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
57
+ project_root = os.path.dirname(
58
+ os.path.dirname(os.path.dirname(__file__)))
58
59
  full_path = os.path.join(project_root, file_path)
59
60
  if os.path.exists(full_path):
60
61
  file_path = full_path
61
62
  absolute_path = os.path.abspath(file_path)
62
63
  else:
63
64
  raise FileNotFoundError(f"资源文件不存在: {file_path}")
64
-
65
+
65
66
  try:
66
67
  with open(file_path, 'r', encoding='utf-8') as f:
67
68
  content = f.read()
68
-
69
+
69
70
  # 解析资源文件
70
71
  lexer = get_lexer()
71
72
  parser = get_parser()
72
73
  ast = parser.parse(content, lexer=lexer)
73
-
74
+
74
75
  # 标记为已加载
75
76
  self.resource_cache[absolute_path] = True
76
-
77
+
77
78
  # 处理导入指令
78
79
  self._process_imports(ast, os.path.dirname(file_path))
79
-
80
+
80
81
  # 注册关键字
81
82
  self._register_keywords(ast, file_path)
82
83
  except Exception as e:
83
84
  print(f"资源文件 {file_path} 加载失败: {str(e)}")
84
85
  raise
85
-
86
+
86
87
  def _process_imports(self, ast: Node, base_dir: str) -> None:
87
88
  """处理资源文件中的导入指令
88
-
89
+
89
90
  Args:
90
91
  ast: 抽象语法树
91
92
  base_dir: 基础目录
92
93
  """
93
94
  if ast.type != 'Start' or not ast.children:
94
95
  return
95
-
96
+
96
97
  metadata_node = ast.children[0]
97
98
  if metadata_node.type != 'Metadata':
98
99
  return
99
-
100
+
100
101
  for item in metadata_node.children:
101
102
  if item.type == '@import':
102
103
  imported_file = item.value
103
104
  # 处理相对路径
104
105
  if not os.path.isabs(imported_file):
105
106
  imported_file = os.path.join(base_dir, imported_file)
106
-
107
+
107
108
  # 规范化路径,避免路径叠加问题
108
109
  imported_file = os.path.normpath(imported_file)
109
-
110
+
110
111
  # 递归加载导入的资源文件
111
112
  self.load_resource_file(imported_file)
112
-
113
+
113
114
  def _register_keywords(self, ast: Node, file_path: str) -> None:
114
115
  """从AST中注册关键字
115
-
116
+
116
117
  Args:
117
118
  ast: 抽象语法树
118
119
  file_path: 文件路径
119
120
  """
120
121
  if ast.type != 'Start' or len(ast.children) < 2:
121
122
  return
122
-
123
+
123
124
  # 遍历语句节点
124
125
  statements_node = ast.children[1]
125
126
  if statements_node.type != 'Statements':
126
127
  return
127
-
128
+
128
129
  for node in statements_node.children:
129
130
  if node.type == 'CustomKeyword':
130
131
  self._register_custom_keyword(node, file_path)
131
-
132
+
132
133
  def _register_custom_keyword(self, node: Node, file_path: str) -> None:
133
134
  """注册自定义关键字
134
-
135
+
135
136
  Args:
136
137
  node: 关键字节点
137
138
  file_path: 资源文件路径
@@ -140,74 +141,74 @@ class CustomKeywordManager:
140
141
  keyword_name = node.value
141
142
  params_node = node.children[0]
142
143
  body_node = node.children[1]
143
-
144
+
144
145
  # 构建参数列表
145
146
  parameters = []
146
147
  param_mapping = {}
147
148
  param_defaults = {} # 存储参数默认值
148
-
149
+
149
150
  for param in params_node if params_node else []:
150
151
  param_name = param.value
151
152
  param_default = None
152
-
153
+
153
154
  # 检查是否有默认值
154
155
  if param.children and param.children[0]:
155
156
  param_default = param.children[0].value
156
157
  param_defaults[param_name] = param_default # 保存默认值
157
-
158
+
158
159
  # 添加参数定义
159
160
  parameters.append({
160
161
  'name': param_name,
161
162
  'mapping': param_name, # 中文参数名和内部参数名相同
162
163
  'description': f'自定义关键字参数 {param_name}'
163
164
  })
164
-
165
+
165
166
  param_mapping[param_name] = param_name
166
-
167
+
167
168
  # 注册自定义关键字到关键字管理器
168
169
  @keyword_manager.register(keyword_name, parameters)
169
170
  def custom_keyword_executor(**kwargs):
170
171
  """自定义关键字执行器"""
171
172
  # 创建一个新的DSL执行器
172
173
  executor = DSLExecutor()
173
-
174
+
175
+ # 导入ReturnException以避免循环导入
176
+ from pytest_dsl.core.dsl_executor import ReturnException
177
+
174
178
  # 获取传递的上下文
175
179
  context = kwargs.get('context')
176
180
  if context:
177
181
  executor.test_context = context
178
-
182
+
179
183
  # 先应用默认值
180
184
  for param_name, default_value in param_defaults.items():
181
185
  executor.variables[param_name] = default_value
182
186
  executor.test_context.set(param_name, default_value)
183
-
187
+
184
188
  # 然后应用传入的参数值(覆盖默认值)
185
189
  for param_name, param_mapping_name in param_mapping.items():
186
190
  if param_mapping_name in kwargs:
187
191
  # 确保参数值在标准变量和测试上下文中都可用
188
192
  executor.variables[param_name] = kwargs[param_mapping_name]
189
- executor.test_context.set(param_name, kwargs[param_mapping_name])
190
-
193
+ executor.test_context.set(
194
+ param_name, kwargs[param_mapping_name])
195
+
191
196
  # 执行关键字体中的语句
192
197
  result = None
193
198
  try:
194
199
  for stmt in body_node.children:
195
- # 检查是否是return语句
196
- if stmt.type == 'Return':
197
- # 对表达式求值
198
- result = executor.eval_expression(stmt.children[0])
199
- break
200
- else:
201
- # 执行普通语句
202
- executor.execute(stmt)
200
+ executor.execute(stmt)
201
+ except ReturnException as e:
202
+ # 捕获return异常,提取返回值
203
+ result = e.return_value
203
204
  except Exception as e:
204
205
  print(f"执行自定义关键字 {keyword_name} 时发生错误: {str(e)}")
205
206
  raise
206
-
207
+
207
208
  return result
208
-
209
+
209
210
  print(f"已注册自定义关键字: {keyword_name} 来自文件: {file_path}")
210
211
 
211
212
 
212
213
  # 创建全局自定义关键字管理器实例
213
- custom_keyword_manager = CustomKeywordManager()
214
+ custom_keyword_manager = CustomKeywordManager()