pytest-dsl 0.9.1__py3-none-any.whl → 0.11.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
@@ -63,9 +63,13 @@ def parse_args():
63
63
  help='罗列所有可用关键字和参数信息'
64
64
  )
65
65
  list_parser.add_argument(
66
- '--format', choices=['text', 'json'],
67
- default='text',
68
- help='输出格式:text(默认) 或 json'
66
+ '--format', choices=['text', 'json', 'html'],
67
+ default='json',
68
+ help='输出格式:json(默认)、texthtml'
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,
@@ -73,10 +77,14 @@ def parse_args():
73
77
  )
74
78
  list_parser.add_argument(
75
79
  '--category',
76
- choices=['builtin', 'custom', 'remote', 'all'],
80
+ choices=['builtin', 'plugin', 'custom', 'project_custom', 'remote', 'all'],
77
81
  default='all',
78
- help='关键字类别:builtin(内置)、custom(自定义)、'
79
- 'remote(远程)、all(全部,默认)'
82
+ help='关键字类别:builtin(内置)、plugin(插件)、custom(自定义)、'
83
+ 'project_custom(项目自定义)、remote(远程)、all(全部,默认)'
84
+ )
85
+ list_parser.add_argument(
86
+ '--include-remote', action='store_true',
87
+ help='是否包含远程关键字(默认不包含)'
80
88
  )
81
89
 
82
90
  return parser.parse_args(argv)
@@ -88,14 +96,22 @@ def parse_args():
88
96
  if '--list-keywords' in argv:
89
97
  parser.add_argument('--list-keywords', action='store_true')
90
98
  parser.add_argument(
91
- '--format', choices=['text', 'json'], default='text'
99
+ '--format', choices=['text', 'json', 'html'], default='json'
100
+ )
101
+ parser.add_argument(
102
+ '--output', '-o', type=str, default=None
92
103
  )
93
104
  parser.add_argument('--filter', type=str, default=None)
94
105
  parser.add_argument(
95
106
  '--category',
96
- choices=['builtin', 'custom', 'remote', 'all'],
107
+ choices=[
108
+ 'builtin', 'plugin', 'custom', 'project_custom', 'remote', 'all'
109
+ ],
97
110
  default='all'
98
111
  )
112
+ parser.add_argument(
113
+ '--include-remote', action='store_true'
114
+ )
99
115
  parser.add_argument('path', nargs='?') # 可选的路径参数
100
116
  parser.add_argument(
101
117
  '--yaml-vars', action='append', default=[]
@@ -118,8 +134,12 @@ def parse_args():
118
134
  return args
119
135
 
120
136
 
121
- def load_all_keywords():
122
- """加载所有可用的关键字"""
137
+ def load_all_keywords(include_remote=False):
138
+ """加载所有可用的关键字
139
+
140
+ Args:
141
+ include_remote: 是否包含远程关键字,默认为False
142
+ """
123
143
  # 首先导入内置关键字模块,确保内置关键字被注册
124
144
  try:
125
145
  import pytest_dsl.keywords # noqa: F401
@@ -132,12 +152,61 @@ def load_all_keywords():
132
152
 
133
153
  # 扫描本地关键字
134
154
  scan_local_keywords()
155
+
156
+ # 扫描项目中的自定义关键字(.resource文件中定义的)
157
+ project_custom_keywords = scan_project_custom_keywords()
158
+ if project_custom_keywords:
159
+ print(f"发现 {len(project_custom_keywords)} 个项目自定义关键字")
160
+
161
+ # 加载.resource文件中的关键字到关键字管理器
162
+ from pytest_dsl.core.custom_keyword_manager import (
163
+ custom_keyword_manager
164
+ )
165
+ from pathlib import Path
166
+
167
+ project_root = Path(os.getcwd())
168
+ resource_files = list(project_root.glob('**/*.resource'))
169
+
170
+ for resource_file in resource_files:
171
+ try:
172
+ custom_keyword_manager.load_resource_file(str(resource_file))
173
+ print(f"已加载资源文件: {resource_file}")
174
+ except Exception as e:
175
+ print(f"加载资源文件失败 {resource_file}: {e}")
176
+
177
+ # 根据参数决定是否加载远程关键字
178
+ if include_remote:
179
+ print("正在扫描远程关键字...")
180
+ # 这里可以添加远程关键字的扫描逻辑
181
+ # 目前远程关键字是通过DSL文件中的@remote导入指令动态加载的
182
+ else:
183
+ print("跳过远程关键字扫描")
184
+
185
+ return project_custom_keywords
135
186
 
136
187
 
137
- def categorize_keyword(keyword_name, keyword_info):
188
+ def categorize_keyword(keyword_name, keyword_info,
189
+ project_custom_keywords=None):
138
190
  """判断关键字的类别"""
191
+ # 优先使用存储的来源信息
192
+ source_type = keyword_info.get('source_type')
193
+ if source_type:
194
+ if source_type == 'builtin':
195
+ return 'builtin'
196
+ elif source_type == 'plugin':
197
+ return 'plugin'
198
+ elif source_type in ['external', 'local']:
199
+ return 'custom'
200
+ elif source_type == 'project_custom':
201
+ return 'project_custom'
202
+
203
+ # 向后兼容:使用原有的判断逻辑
139
204
  if keyword_info.get('remote', False):
140
205
  return 'remote'
206
+
207
+ # 检查是否是项目自定义关键字(DSL文件中定义的)
208
+ if project_custom_keywords and keyword_name in project_custom_keywords:
209
+ return 'project_custom'
141
210
 
142
211
  # 检查是否是内置关键字(通过检查函数所在模块)
143
212
  func = keyword_info.get('func')
@@ -149,13 +218,74 @@ def categorize_keyword(keyword_name, keyword_info):
149
218
  return 'custom'
150
219
 
151
220
 
152
- def format_keyword_info_text(keyword_name, keyword_info, show_category=True):
221
+ def get_keyword_source_info(keyword_info):
222
+ """获取关键字的详细来源信息"""
223
+ source_type = keyword_info.get('source_type', 'unknown')
224
+ source_name = keyword_info.get('source_name', '未知')
225
+
226
+ return {
227
+ 'type': source_type,
228
+ 'name': source_name,
229
+ 'display_name': source_name,
230
+ 'module': keyword_info.get('module_name', ''),
231
+ 'plugin_module': keyword_info.get('plugin_module', '')
232
+ }
233
+
234
+
235
+ def group_keywords_by_source(keywords_dict, project_custom_keywords=None):
236
+ """按来源分组关键字
237
+
238
+ Returns:
239
+ dict: 格式为 {source_group: {source_name: [keywords]}}
240
+ """
241
+ groups = {
242
+ 'builtin': {},
243
+ 'plugin': {},
244
+ 'custom': {},
245
+ 'project_custom': {},
246
+ 'remote': {}
247
+ }
248
+
249
+ for keyword_name, keyword_info in keywords_dict.items():
250
+ category = categorize_keyword(
251
+ keyword_name, keyword_info, project_custom_keywords
252
+ )
253
+ source_info = get_keyword_source_info(keyword_info)
254
+
255
+ # 特殊处理项目自定义关键字
256
+ if category == 'project_custom' and project_custom_keywords:
257
+ custom_info = project_custom_keywords[keyword_name]
258
+ source_name = custom_info['file']
259
+ else:
260
+ source_name = source_info['name']
261
+
262
+ if source_name not in groups[category]:
263
+ groups[category][source_name] = []
264
+
265
+ groups[category][source_name].append({
266
+ 'name': keyword_name,
267
+ 'info': keyword_info,
268
+ 'source_info': source_info
269
+ })
270
+
271
+ return groups
272
+
273
+
274
+ def format_keyword_info_text(keyword_name, keyword_info, show_category=True,
275
+ project_custom_keywords=None):
153
276
  """格式化关键字信息为文本格式"""
154
277
  lines = []
155
278
 
156
279
  # 关键字名称和类别
157
- category = categorize_keyword(keyword_name, keyword_info)
158
- category_names = {'builtin': '内置', 'custom': '自定义', 'remote': '远程'}
280
+ category = categorize_keyword(
281
+ keyword_name, keyword_info, project_custom_keywords
282
+ )
283
+ category_names = {
284
+ 'builtin': '内置',
285
+ 'custom': '自定义',
286
+ 'project_custom': '项目自定义',
287
+ 'remote': '远程'
288
+ }
159
289
 
160
290
  if show_category:
161
291
  category_display = category_names.get(category, '未知')
@@ -170,23 +300,64 @@ def format_keyword_info_text(keyword_name, keyword_info, show_category=True):
170
300
  lines.append(f" 远程服务器: {alias}")
171
301
  lines.append(f" 原始名称: {original_name}")
172
302
 
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}")
303
+ # 项目自定义关键字特殊标识
304
+ if category == 'project_custom' and project_custom_keywords:
305
+ custom_info = project_custom_keywords[keyword_name]
306
+ lines.append(f" 文件位置: {custom_info['file']}")
307
+
308
+ # 对于项目自定义关键字,使用从AST中提取的参数信息
309
+ custom_parameters = custom_info.get('parameters', [])
310
+ if custom_parameters:
311
+ lines.append(" 参数:")
312
+ for param_info in custom_parameters:
313
+ param_name = param_info['name']
314
+ param_mapping = param_info.get('mapping', '')
315
+ param_desc = param_info.get('description', '')
316
+ param_default = param_info.get('default', None)
317
+
318
+ # 构建参数描述
319
+ param_parts = []
320
+ if param_mapping and param_mapping != param_name:
321
+ param_parts.append(f"{param_name} ({param_mapping})")
322
+ else:
323
+ param_parts.append(param_name)
324
+
325
+ param_parts.append(f": {param_desc}")
326
+
327
+ # 添加默认值信息
328
+ if param_default is not None:
329
+ param_parts.append(f" (默认值: {param_default})")
330
+
331
+ lines.append(f" {''.join(param_parts)}")
332
+ else:
333
+ lines.append(" 参数: 无")
188
334
  else:
189
- lines.append(" 参数: 无")
335
+ # 参数信息(对于其他类型的关键字)
336
+ parameters = keyword_info.get('parameters', [])
337
+ if parameters:
338
+ lines.append(" 参数:")
339
+ for param in parameters:
340
+ param_name = getattr(param, 'name', str(param))
341
+ param_mapping = getattr(param, 'mapping', '')
342
+ param_desc = getattr(param, 'description', '')
343
+ param_default = getattr(param, 'default', None)
344
+
345
+ # 构建参数描述
346
+ param_info = []
347
+ if param_mapping and param_mapping != param_name:
348
+ param_info.append(f"{param_name} ({param_mapping})")
349
+ else:
350
+ param_info.append(param_name)
351
+
352
+ param_info.append(f": {param_desc}")
353
+
354
+ # 添加默认值信息
355
+ if param_default is not None:
356
+ param_info.append(f" (默认值: {param_default})")
357
+
358
+ lines.append(f" {''.join(param_info)}")
359
+ else:
360
+ lines.append(" 参数: 无")
190
361
 
191
362
  # 函数文档
192
363
  func = keyword_info.get('func')
@@ -196,13 +367,18 @@ def format_keyword_info_text(keyword_name, keyword_info, show_category=True):
196
367
  return '\n'.join(lines)
197
368
 
198
369
 
199
- def format_keyword_info_json(keyword_name, keyword_info):
370
+ def format_keyword_info_json(keyword_name, keyword_info,
371
+ project_custom_keywords=None):
200
372
  """格式化关键字信息为JSON格式"""
201
- category = categorize_keyword(keyword_name, keyword_info)
373
+ category = categorize_keyword(
374
+ keyword_name, keyword_info, project_custom_keywords
375
+ )
376
+ source_info = get_keyword_source_info(keyword_info)
202
377
 
203
378
  keyword_data = {
204
379
  'name': keyword_name,
205
380
  'category': category,
381
+ 'source_info': source_info,
206
382
  'parameters': []
207
383
  }
208
384
 
@@ -213,15 +389,30 @@ def format_keyword_info_json(keyword_name, keyword_info):
213
389
  'original_name': keyword_info.get('original_name', keyword_name)
214
390
  }
215
391
 
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)
392
+ # 项目自定义关键字特殊信息
393
+ if category == 'project_custom' and project_custom_keywords:
394
+ custom_info = project_custom_keywords[keyword_name]
395
+ keyword_data['file_location'] = custom_info['file']
396
+
397
+ # 对于项目自定义关键字,使用从AST中提取的参数信息
398
+ for param_info in custom_info.get('parameters', []):
399
+ keyword_data['parameters'].append(param_info)
400
+ else:
401
+ # 参数信息(对于其他类型的关键字)
402
+ parameters = keyword_info.get('parameters', [])
403
+ for param in parameters:
404
+ param_data = {
405
+ 'name': getattr(param, 'name', str(param)),
406
+ 'mapping': getattr(param, 'mapping', ''),
407
+ 'description': getattr(param, 'description', '')
408
+ }
409
+
410
+ # 添加默认值信息
411
+ param_default = getattr(param, 'default', None)
412
+ if param_default is not None:
413
+ param_data['default'] = param_default
414
+
415
+ keyword_data['parameters'].append(param_data)
225
416
 
226
417
  # 函数文档
227
418
  func = keyword_info.get('func')
@@ -231,13 +422,104 @@ def format_keyword_info_json(keyword_name, keyword_info):
231
422
  return keyword_data
232
423
 
233
424
 
234
- def list_keywords(output_format='text', name_filter=None,
235
- category_filter='all'):
425
+ def generate_html_report(keywords_data, output_file):
426
+ """生成HTML格式的关键字报告"""
427
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
428
+ import os
429
+
430
+ # 准备数据
431
+ summary = keywords_data['summary']
432
+ keywords = keywords_data['keywords']
433
+
434
+ # 按类别分组
435
+ categories = {}
436
+ for keyword in keywords:
437
+ category = keyword['category']
438
+ if category not in categories:
439
+ categories[category] = []
440
+ categories[category].append(keyword)
441
+
442
+ # 按来源分组(用于更详细的分组视图)
443
+ source_groups = {}
444
+ for keyword in keywords:
445
+ source_info = keyword.get('source_info', {})
446
+ category = keyword['category']
447
+ source_name = source_info.get('name', '未知来源')
448
+
449
+ # 构建分组键
450
+ if category == 'plugin':
451
+ group_key = f"插件 - {source_name}"
452
+ elif category == 'builtin':
453
+ group_key = "内置关键字"
454
+ elif category == 'project_custom':
455
+ group_key = f"项目自定义 - {keyword.get('file_location', source_name)}"
456
+ elif category == 'remote':
457
+ group_key = f"远程 - {source_name}"
458
+ else:
459
+ group_key = f"自定义 - {source_name}"
460
+
461
+ if group_key not in source_groups:
462
+ source_groups[group_key] = []
463
+ source_groups[group_key].append(keyword)
464
+
465
+ # 按位置分组(用于全部关键字视图,保持向后兼容)
466
+ location_groups = {}
467
+ for keyword in keywords:
468
+ # 优先使用file_location,然后使用source_info中的name
469
+ location = keyword.get('file_location')
470
+ if not location:
471
+ source_info = keyword.get('source_info', {})
472
+ location = source_info.get('name', '内置/插件')
473
+
474
+ if location not in location_groups:
475
+ location_groups[location] = []
476
+ location_groups[location].append(keyword)
477
+
478
+ # 类别名称映射
479
+ category_names = {
480
+ 'builtin': '内置',
481
+ 'plugin': '插件',
482
+ 'custom': '自定义',
483
+ 'project_custom': '项目自定义',
484
+ 'remote': '远程'
485
+ }
486
+
487
+ # 设置Jinja2环境
488
+ template_dir = os.path.join(os.path.dirname(__file__), 'templates')
489
+
490
+ env = Environment(
491
+ loader=FileSystemLoader(template_dir),
492
+ autoescape=select_autoescape(['html', 'xml'])
493
+ )
494
+
495
+ # 加载模板
496
+ template = env.get_template('keywords_report.html')
497
+
498
+ # 渲染模板
499
+ html_content = template.render(
500
+ summary=summary,
501
+ keywords=keywords,
502
+ categories=categories,
503
+ source_groups=source_groups,
504
+ location_groups=location_groups,
505
+ category_names=category_names
506
+ )
507
+
508
+ # 写入文件
509
+ with open(output_file, 'w', encoding='utf-8') as f:
510
+ f.write(html_content)
511
+
512
+ print(f"HTML报告已生成: {output_file}")
513
+
514
+
515
+ def list_keywords(output_format='json', name_filter=None,
516
+ category_filter='all', output_file=None,
517
+ include_remote=False):
236
518
  """罗列所有关键字信息"""
237
519
  import json
238
520
 
239
521
  print("正在加载关键字...")
240
- load_all_keywords()
522
+ project_custom_keywords = load_all_keywords(include_remote=include_remote)
241
523
 
242
524
  # 获取所有注册的关键字
243
525
  all_keywords = keyword_manager._keywords
@@ -254,9 +536,15 @@ def list_keywords(output_format='text', name_filter=None,
254
536
  if name_filter and name_filter.lower() not in name.lower():
255
537
  continue
256
538
 
539
+ # 远程关键字过滤
540
+ if not include_remote and info.get('remote', False):
541
+ continue
542
+
257
543
  # 类别过滤
258
544
  if category_filter != 'all':
259
- keyword_category = categorize_keyword(name, info)
545
+ keyword_category = categorize_keyword(
546
+ name, info, project_custom_keywords
547
+ )
260
548
  if keyword_category != category_filter:
261
549
  continue
262
550
 
@@ -272,54 +560,139 @@ def list_keywords(output_format='text', name_filter=None,
272
560
  # 输出统计信息
273
561
  total_count = len(filtered_keywords)
274
562
  category_counts = {}
563
+ source_counts = {}
564
+
275
565
  for name, info in filtered_keywords.items():
276
- cat = categorize_keyword(name, info)
566
+ cat = categorize_keyword(name, info, project_custom_keywords)
277
567
  category_counts[cat] = category_counts.get(cat, 0) + 1
568
+
569
+ # 统计各来源的关键字数量
570
+ source_info = get_keyword_source_info(info)
571
+ source_name = source_info['name']
572
+ if cat == 'project_custom' and project_custom_keywords:
573
+ custom_info = project_custom_keywords[name]
574
+ source_name = custom_info['file']
575
+
576
+ source_key = f"{cat}:{source_name}"
577
+ source_counts[source_key] = source_counts.get(source_key, 0) + 1
278
578
 
279
579
  if output_format == 'text':
280
580
  print(f"\n找到 {total_count} 个关键字:")
281
581
  for cat, count in category_counts.items():
282
- cat_names = {'builtin': '内置', 'custom': '自定义', 'remote': '远程'}
582
+ cat_names = {
583
+ 'builtin': '内置', 'plugin': '插件', 'custom': '自定义',
584
+ 'project_custom': '项目自定义', 'remote': '远程'
585
+ }
283
586
  print(f" {cat_names.get(cat, cat)}: {count} 个")
284
587
  print("-" * 60)
285
588
 
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
589
+ # 按类别和来源分组显示
590
+ grouped = group_keywords_by_source(
591
+ filtered_keywords, project_custom_keywords
592
+ )
593
+
594
+ for category in [
595
+ 'builtin', 'plugin', 'custom', 'project_custom', 'remote'
596
+ ]:
597
+ if category not in grouped or not grouped[category]:
598
+ continue
599
+
600
+ cat_names = {
601
+ 'builtin': '内置关键字',
602
+ 'plugin': '插件关键字',
603
+ 'custom': '自定义关键字',
604
+ 'project_custom': '项目自定义关键字',
605
+ 'remote': '远程关键字'
291
606
  }
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]
607
+ print(f"\n=== {cat_names[category]} ===")
608
+
609
+ for source_name, keyword_list in grouped[category].items():
610
+ if len(grouped[category]) > 1: # 如果有多个来源,显示来源名
611
+ print(f"\n--- {source_name} ---")
612
+
613
+ for keyword_data in keyword_list:
614
+ name = keyword_data['name']
615
+ info = keyword_data['info']
303
616
  print()
304
617
  print(format_keyword_info_text(
305
- name, info, show_category=False
618
+ name, info, show_category=False,
619
+ project_custom_keywords=project_custom_keywords
306
620
  ))
307
621
 
308
622
  elif output_format == 'json':
309
623
  keywords_data = {
310
624
  'summary': {
311
625
  'total_count': total_count,
312
- 'category_counts': category_counts
626
+ 'category_counts': category_counts,
627
+ 'source_counts': source_counts
313
628
  },
314
629
  'keywords': []
315
630
  }
316
631
 
317
632
  for name in sorted(filtered_keywords.keys()):
318
633
  info = filtered_keywords[name]
319
- keyword_data = format_keyword_info_json(name, info)
634
+ keyword_data = format_keyword_info_json(
635
+ name, info, project_custom_keywords
636
+ )
320
637
  keywords_data['keywords'].append(keyword_data)
321
638
 
322
- print(json.dumps(keywords_data, ensure_ascii=False, indent=2))
639
+ json_output = json.dumps(keywords_data, ensure_ascii=False, indent=2)
640
+
641
+ # 确定输出文件名
642
+ if output_file is None:
643
+ output_file = 'keywords.json'
644
+
645
+ # 写入到文件
646
+ try:
647
+ with open(output_file, 'w', encoding='utf-8') as f:
648
+ f.write(json_output)
649
+ print(f"关键字信息已保存到文件: {output_file}")
650
+ print(f"共 {total_count} 个关键字")
651
+ for cat, count in category_counts.items():
652
+ cat_names = {
653
+ 'builtin': '内置', 'plugin': '插件', 'custom': '自定义',
654
+ 'project_custom': '项目自定义', 'remote': '远程'
655
+ }
656
+ print(f" {cat_names.get(cat, cat)}: {count} 个")
657
+ except Exception as e:
658
+ print(f"保存文件失败: {e}")
659
+ # 如果写入文件失败,则回退到打印
660
+ print(json_output)
661
+
662
+ elif output_format == 'html':
663
+ keywords_data = {
664
+ 'summary': {
665
+ 'total_count': total_count,
666
+ 'category_counts': category_counts,
667
+ 'source_counts': source_counts
668
+ },
669
+ 'keywords': []
670
+ }
671
+
672
+ for name in sorted(filtered_keywords.keys()):
673
+ info = filtered_keywords[name]
674
+ keyword_data = format_keyword_info_json(
675
+ name, info, project_custom_keywords
676
+ )
677
+ keywords_data['keywords'].append(keyword_data)
678
+
679
+ # 确定输出文件名
680
+ if output_file is None:
681
+ output_file = 'keywords.html'
682
+
683
+ # 生成HTML报告
684
+ try:
685
+ generate_html_report(keywords_data, output_file)
686
+ print(f"共 {total_count} 个关键字")
687
+ for cat, count in category_counts.items():
688
+ cat_names = {
689
+ 'builtin': '内置', 'plugin': '插件', 'custom': '自定义',
690
+ 'project_custom': '项目自定义', 'remote': '远程'
691
+ }
692
+ print(f" {cat_names.get(cat, cat)}: {count} 个")
693
+ except Exception as e:
694
+ print(f"生成HTML报告失败: {e}")
695
+ raise
323
696
 
324
697
 
325
698
  def load_yaml_variables(args):
@@ -368,8 +741,8 @@ def run_dsl_tests(args):
368
741
  print("错误: 必须指定要执行的DSL文件路径或目录")
369
742
  sys.exit(1)
370
743
 
371
- # 加载内置关键字插件
372
- load_all_keywords()
744
+ # 加载内置关键字插件(运行时总是包含远程关键字)
745
+ load_all_keywords(include_remote=True)
373
746
 
374
747
  # 加载YAML变量(包括远程服务器自动连接)
375
748
  load_yaml_variables(args)
@@ -433,16 +806,22 @@ def main():
433
806
  list_keywords(
434
807
  output_format=args.format,
435
808
  name_filter=args.filter,
436
- category_filter=args.category
809
+ category_filter=args.category,
810
+ output_file=args.output,
811
+ include_remote=args.include_remote
437
812
  )
438
813
  elif args.command == 'run':
439
814
  run_dsl_tests(args)
440
815
  elif args.command == 'list-keywords-compat':
441
816
  # 向后兼容:旧的--list-keywords格式
817
+ output_file = getattr(args, 'output', None)
818
+ include_remote = getattr(args, 'include_remote', False)
442
819
  list_keywords(
443
820
  output_format=args.format,
444
821
  name_filter=args.filter,
445
- category_filter=args.category
822
+ category_filter=args.category,
823
+ output_file=output_file,
824
+ include_remote=include_remote
446
825
  )
447
826
  elif args.command == 'run-compat':
448
827
  # 向后兼容:默认执行DSL测试
@@ -457,9 +836,13 @@ def main_list_keywords():
457
836
  """关键字列表命令的专用入口点"""
458
837
  parser = argparse.ArgumentParser(description='查看pytest-dsl可用关键字列表')
459
838
  parser.add_argument(
460
- '--format', choices=['text', 'json'],
461
- default='text',
462
- help='输出格式:text(默认) 或 json'
839
+ '--format', choices=['text', 'json', 'html'],
840
+ default='json',
841
+ help='输出格式:json(默认)、texthtml'
842
+ )
843
+ parser.add_argument(
844
+ '--output', '-o', type=str, default=None,
845
+ help='输出文件路径(json格式默认为keywords.json,html格式默认为keywords.html)'
463
846
  )
464
847
  parser.add_argument(
465
848
  '--filter', type=str, default=None,
@@ -467,9 +850,14 @@ def main_list_keywords():
467
850
  )
468
851
  parser.add_argument(
469
852
  '--category',
470
- choices=['builtin', 'custom', 'remote', 'all'],
853
+ choices=['builtin', 'plugin', 'custom', 'project_custom', 'remote', 'all'],
471
854
  default='all',
472
- help='关键字类别:builtin(内置)、custom(自定义)、remote(远程)、all(全部,默认)'
855
+ help='关键字类别:builtin(内置)、plugin(插件)、custom(自定义)、'
856
+ 'project_custom(项目自定义)、remote(远程)、all(全部,默认)'
857
+ )
858
+ parser.add_argument(
859
+ '--include-remote', action='store_true',
860
+ help='是否包含远程关键字(默认不包含)'
473
861
  )
474
862
 
475
863
  args = parser.parse_args()
@@ -477,9 +865,113 @@ def main_list_keywords():
477
865
  list_keywords(
478
866
  output_format=args.format,
479
867
  name_filter=args.filter,
480
- category_filter=args.category
868
+ category_filter=args.category,
869
+ output_file=args.output,
870
+ include_remote=args.include_remote
481
871
  )
482
872
 
483
873
 
874
+ def scan_project_custom_keywords(project_root=None):
875
+ """扫描项目中.resource文件中的自定义关键字
876
+
877
+ Args:
878
+ project_root: 项目根目录,默认为当前工作目录
879
+
880
+ Returns:
881
+ dict: 自定义关键字信息,格式为
882
+ {keyword_name: {'file': file_path, 'node': ast_node}}
883
+ """
884
+ if project_root is None:
885
+ project_root = os.getcwd()
886
+
887
+ project_root = Path(project_root)
888
+ custom_keywords = {}
889
+
890
+ # 查找所有.resource文件
891
+ resource_files = list(project_root.glob('**/*.resource'))
892
+
893
+ if not resource_files:
894
+ return custom_keywords
895
+
896
+ lexer = get_lexer()
897
+ parser = get_parser()
898
+
899
+ for file_path in resource_files:
900
+ try:
901
+ # 读取并解析文件
902
+ content = read_file(str(file_path))
903
+ ast = parser.parse(content, lexer=lexer)
904
+
905
+ # 查找自定义关键字定义
906
+ keywords_in_file = extract_custom_keywords_from_ast(
907
+ ast, str(file_path)
908
+ )
909
+ custom_keywords.update(keywords_in_file)
910
+
911
+ except Exception as e:
912
+ print(f"解析资源文件 {file_path} 时出错: {e}")
913
+
914
+ return custom_keywords
915
+
916
+
917
+ def extract_custom_keywords_from_ast(ast, file_path):
918
+ """从AST中提取自定义关键字定义
919
+
920
+ Args:
921
+ ast: 抽象语法树
922
+ file_path: 文件路径
923
+
924
+ Returns:
925
+ dict: 自定义关键字信息
926
+ """
927
+ custom_keywords = {}
928
+
929
+ if ast.type != 'Start' or len(ast.children) < 2:
930
+ return custom_keywords
931
+
932
+ # 遍历语句节点
933
+ statements_node = ast.children[1]
934
+ if statements_node.type != 'Statements':
935
+ return custom_keywords
936
+
937
+ for node in statements_node.children:
938
+ # 支持两种格式:CustomKeyword(旧格式)和Function(新格式)
939
+ if node.type in ['CustomKeyword', 'Function']:
940
+ keyword_name = node.value
941
+
942
+ # 提取参数信息
943
+ params_node = node.children[0] if node.children else None
944
+ parameters = []
945
+
946
+ if params_node:
947
+ for param in params_node:
948
+ param_name = param.value
949
+ param_default = None
950
+
951
+ # 检查是否有默认值
952
+ if param.children and param.children[0]:
953
+ param_default = param.children[0].value
954
+
955
+ param_info = {
956
+ 'name': param_name,
957
+ 'mapping': param_name,
958
+ 'description': f'自定义关键字参数 {param_name}'
959
+ }
960
+
961
+ if param_default is not None:
962
+ param_info['default'] = param_default
963
+
964
+ parameters.append(param_info)
965
+
966
+ custom_keywords[keyword_name] = {
967
+ 'file': file_path,
968
+ 'node': node,
969
+ 'type': 'project_custom',
970
+ 'parameters': parameters
971
+ }
972
+
973
+ return custom_keywords
974
+
975
+
484
976
  if __name__ == '__main__':
485
977
  main()