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.
pytest_dsl/cli.py CHANGED
@@ -63,9 +63,9 @@ def parse_args():
63
63
  help='罗列所有可用关键字和参数信息'
64
64
  )
65
65
  list_parser.add_argument(
66
- '--format', choices=['text', 'json'],
66
+ '--format', choices=['text', 'json', 'html'],
67
67
  default='json',
68
- help='输出格式:json(默认) 或 text'
68
+ help='输出格式:json(默认)、texthtml'
69
69
  )
70
70
  list_parser.add_argument(
71
71
  '--output', '-o', type=str, default=None,
@@ -77,10 +77,14 @@ def parse_args():
77
77
  )
78
78
  list_parser.add_argument(
79
79
  '--category',
80
- choices=['builtin', 'custom', 'remote', 'all'],
80
+ choices=['builtin', 'plugin', 'custom', 'project_custom', 'remote', 'all'],
81
81
  default='all',
82
- help='关键字类别:builtin(内置)、custom(自定义)、'
83
- '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='是否包含远程关键字(默认不包含)'
84
88
  )
85
89
 
86
90
  return parser.parse_args(argv)
@@ -92,7 +96,7 @@ def parse_args():
92
96
  if '--list-keywords' in argv:
93
97
  parser.add_argument('--list-keywords', action='store_true')
94
98
  parser.add_argument(
95
- '--format', choices=['text', 'json'], default='json'
99
+ '--format', choices=['text', 'json', 'html'], default='json'
96
100
  )
97
101
  parser.add_argument(
98
102
  '--output', '-o', type=str, default=None
@@ -100,9 +104,14 @@ def parse_args():
100
104
  parser.add_argument('--filter', type=str, default=None)
101
105
  parser.add_argument(
102
106
  '--category',
103
- choices=['builtin', 'custom', 'remote', 'all'],
107
+ choices=[
108
+ 'builtin', 'plugin', 'custom', 'project_custom', 'remote', 'all'
109
+ ],
104
110
  default='all'
105
111
  )
112
+ parser.add_argument(
113
+ '--include-remote', action='store_true'
114
+ )
106
115
  parser.add_argument('path', nargs='?') # 可选的路径参数
107
116
  parser.add_argument(
108
117
  '--yaml-vars', action='append', default=[]
@@ -125,8 +134,12 @@ def parse_args():
125
134
  return args
126
135
 
127
136
 
128
- def load_all_keywords():
129
- """加载所有可用的关键字"""
137
+ def load_all_keywords(include_remote=False):
138
+ """加载所有可用的关键字
139
+
140
+ Args:
141
+ include_remote: 是否包含远程关键字,默认为False
142
+ """
130
143
  # 首先导入内置关键字模块,确保内置关键字被注册
131
144
  try:
132
145
  import pytest_dsl.keywords # noqa: F401
@@ -139,12 +152,61 @@ def load_all_keywords():
139
152
 
140
153
  # 扫描本地关键字
141
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
142
186
 
143
187
 
144
- def categorize_keyword(keyword_name, keyword_info):
188
+ def categorize_keyword(keyword_name, keyword_info,
189
+ project_custom_keywords=None):
145
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
+ # 向后兼容:使用原有的判断逻辑
146
204
  if keyword_info.get('remote', False):
147
205
  return 'remote'
206
+
207
+ # 检查是否是项目自定义关键字(DSL文件中定义的)
208
+ if project_custom_keywords and keyword_name in project_custom_keywords:
209
+ return 'project_custom'
148
210
 
149
211
  # 检查是否是内置关键字(通过检查函数所在模块)
150
212
  func = keyword_info.get('func')
@@ -156,13 +218,74 @@ def categorize_keyword(keyword_name, keyword_info):
156
218
  return 'custom'
157
219
 
158
220
 
159
- 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):
160
276
  """格式化关键字信息为文本格式"""
161
277
  lines = []
162
278
 
163
279
  # 关键字名称和类别
164
- category = categorize_keyword(keyword_name, keyword_info)
165
- 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
+ }
166
289
 
167
290
  if show_category:
168
291
  category_display = category_names.get(category, '未知')
@@ -177,32 +300,64 @@ def format_keyword_info_text(keyword_name, keyword_info, show_category=True):
177
300
  lines.append(f" 远程服务器: {alias}")
178
301
  lines.append(f" 原始名称: {original_name}")
179
302
 
180
- # 参数信息
181
- parameters = keyword_info.get('parameters', [])
182
- if parameters:
183
- lines.append(" 参数:")
184
- for param in parameters:
185
- param_name = getattr(param, 'name', str(param))
186
- param_mapping = getattr(param, 'mapping', '')
187
- param_desc = getattr(param, 'description', '')
188
- param_default = getattr(param, 'default', None)
189
-
190
- # 构建参数描述
191
- param_info = []
192
- if param_mapping and param_mapping != param_name:
193
- param_info.append(f"{param_name} ({param_mapping})")
194
- else:
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)}")
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(" 参数: 无")
204
334
  else:
205
- 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(" 参数: 无")
206
361
 
207
362
  # 函数文档
208
363
  func = keyword_info.get('func')
@@ -212,13 +367,18 @@ def format_keyword_info_text(keyword_name, keyword_info, show_category=True):
212
367
  return '\n'.join(lines)
213
368
 
214
369
 
215
- def format_keyword_info_json(keyword_name, keyword_info):
370
+ def format_keyword_info_json(keyword_name, keyword_info,
371
+ project_custom_keywords=None):
216
372
  """格式化关键字信息为JSON格式"""
217
- 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)
218
377
 
219
378
  keyword_data = {
220
379
  'name': keyword_name,
221
380
  'category': category,
381
+ 'source_info': source_info,
222
382
  'parameters': []
223
383
  }
224
384
 
@@ -229,21 +389,30 @@ def format_keyword_info_json(keyword_name, keyword_info):
229
389
  'original_name': keyword_info.get('original_name', keyword_name)
230
390
  }
231
391
 
232
- # 参数信息
233
- parameters = keyword_info.get('parameters', [])
234
- for param in parameters:
235
- param_data = {
236
- 'name': getattr(param, 'name', str(param)),
237
- 'mapping': getattr(param, 'mapping', ''),
238
- 'description': getattr(param, 'description', '')
239
- }
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']
240
396
 
241
- # 添加默认值信息
242
- param_default = getattr(param, 'default', None)
243
- if param_default is not None:
244
- param_data['default'] = param_default
245
-
246
- keyword_data['parameters'].append(param_data)
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)
247
416
 
248
417
  # 函数文档
249
418
  func = keyword_info.get('func')
@@ -253,13 +422,104 @@ def format_keyword_info_json(keyword_name, keyword_info):
253
422
  return keyword_data
254
423
 
255
424
 
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
+
256
515
  def list_keywords(output_format='json', name_filter=None,
257
- category_filter='all', output_file=None):
516
+ category_filter='all', output_file=None,
517
+ include_remote=False):
258
518
  """罗列所有关键字信息"""
259
519
  import json
260
520
 
261
521
  print("正在加载关键字...")
262
- load_all_keywords()
522
+ project_custom_keywords = load_all_keywords(include_remote=include_remote)
263
523
 
264
524
  # 获取所有注册的关键字
265
525
  all_keywords = keyword_manager._keywords
@@ -276,9 +536,15 @@ def list_keywords(output_format='json', name_filter=None,
276
536
  if name_filter and name_filter.lower() not in name.lower():
277
537
  continue
278
538
 
539
+ # 远程关键字过滤
540
+ if not include_remote and info.get('remote', False):
541
+ continue
542
+
279
543
  # 类别过滤
280
544
  if category_filter != 'all':
281
- keyword_category = categorize_keyword(name, info)
545
+ keyword_category = categorize_keyword(
546
+ name, info, project_custom_keywords
547
+ )
282
548
  if keyword_category != category_filter:
283
549
  continue
284
550
 
@@ -294,51 +560,80 @@ def list_keywords(output_format='json', name_filter=None,
294
560
  # 输出统计信息
295
561
  total_count = len(filtered_keywords)
296
562
  category_counts = {}
563
+ source_counts = {}
564
+
297
565
  for name, info in filtered_keywords.items():
298
- cat = categorize_keyword(name, info)
566
+ cat = categorize_keyword(name, info, project_custom_keywords)
299
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
300
578
 
301
579
  if output_format == 'text':
302
580
  print(f"\n找到 {total_count} 个关键字:")
303
581
  for cat, count in category_counts.items():
304
- cat_names = {'builtin': '内置', 'custom': '自定义', 'remote': '远程'}
582
+ cat_names = {
583
+ 'builtin': '内置', 'plugin': '插件', 'custom': '自定义',
584
+ 'project_custom': '项目自定义', 'remote': '远程'
585
+ }
305
586
  print(f" {cat_names.get(cat, cat)}: {count} 个")
306
587
  print("-" * 60)
307
588
 
308
- # 按类别分组显示
309
- for category in ['builtin', 'custom', 'remote']:
310
- cat_keywords = {
311
- name: info for name, info in filtered_keywords.items()
312
- 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': '远程关键字'
313
606
  }
314
-
315
- if cat_keywords:
316
- cat_names = {
317
- 'builtin': '内置关键字',
318
- 'custom': '自定义关键字',
319
- 'remote': '远程关键字'
320
- }
321
- print(f"\n=== {cat_names[category]} ===")
322
-
323
- for name in sorted(cat_keywords.keys()):
324
- 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']
325
616
  print()
326
617
  print(format_keyword_info_text(
327
- name, info, show_category=False
618
+ name, info, show_category=False,
619
+ project_custom_keywords=project_custom_keywords
328
620
  ))
329
621
 
330
622
  elif output_format == 'json':
331
623
  keywords_data = {
332
624
  'summary': {
333
625
  'total_count': total_count,
334
- 'category_counts': category_counts
626
+ 'category_counts': category_counts,
627
+ 'source_counts': source_counts
335
628
  },
336
629
  'keywords': []
337
630
  }
338
631
 
339
632
  for name in sorted(filtered_keywords.keys()):
340
633
  info = filtered_keywords[name]
341
- keyword_data = format_keyword_info_json(name, info)
634
+ keyword_data = format_keyword_info_json(
635
+ name, info, project_custom_keywords
636
+ )
342
637
  keywords_data['keywords'].append(keyword_data)
343
638
 
344
639
  json_output = json.dumps(keywords_data, ensure_ascii=False, indent=2)
@@ -354,13 +649,51 @@ def list_keywords(output_format='json', name_filter=None,
354
649
  print(f"关键字信息已保存到文件: {output_file}")
355
650
  print(f"共 {total_count} 个关键字")
356
651
  for cat, count in category_counts.items():
357
- cat_names = {'builtin': '内置', 'custom': '自定义', 'remote': '远程'}
652
+ cat_names = {
653
+ 'builtin': '内置', 'plugin': '插件', 'custom': '自定义',
654
+ 'project_custom': '项目自定义', 'remote': '远程'
655
+ }
358
656
  print(f" {cat_names.get(cat, cat)}: {count} 个")
359
657
  except Exception as e:
360
658
  print(f"保存文件失败: {e}")
361
659
  # 如果写入文件失败,则回退到打印
362
660
  print(json_output)
363
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
696
+
364
697
 
365
698
  def load_yaml_variables(args):
366
699
  """从命令行参数加载YAML变量"""
@@ -408,8 +741,8 @@ def run_dsl_tests(args):
408
741
  print("错误: 必须指定要执行的DSL文件路径或目录")
409
742
  sys.exit(1)
410
743
 
411
- # 加载内置关键字插件
412
- load_all_keywords()
744
+ # 加载内置关键字插件(运行时总是包含远程关键字)
745
+ load_all_keywords(include_remote=True)
413
746
 
414
747
  # 加载YAML变量(包括远程服务器自动连接)
415
748
  load_yaml_variables(args)
@@ -474,18 +807,21 @@ def main():
474
807
  output_format=args.format,
475
808
  name_filter=args.filter,
476
809
  category_filter=args.category,
477
- output_file=args.output
810
+ output_file=args.output,
811
+ include_remote=args.include_remote
478
812
  )
479
813
  elif args.command == 'run':
480
814
  run_dsl_tests(args)
481
815
  elif args.command == 'list-keywords-compat':
482
816
  # 向后兼容:旧的--list-keywords格式
483
817
  output_file = getattr(args, 'output', None)
818
+ include_remote = getattr(args, 'include_remote', False)
484
819
  list_keywords(
485
820
  output_format=args.format,
486
821
  name_filter=args.filter,
487
822
  category_filter=args.category,
488
- output_file=output_file
823
+ output_file=output_file,
824
+ include_remote=include_remote
489
825
  )
490
826
  elif args.command == 'run-compat':
491
827
  # 向后兼容:默认执行DSL测试
@@ -500,13 +836,13 @@ def main_list_keywords():
500
836
  """关键字列表命令的专用入口点"""
501
837
  parser = argparse.ArgumentParser(description='查看pytest-dsl可用关键字列表')
502
838
  parser.add_argument(
503
- '--format', choices=['text', 'json'],
839
+ '--format', choices=['text', 'json', 'html'],
504
840
  default='json',
505
- help='输出格式:json(默认) 或 text'
841
+ help='输出格式:json(默认)、texthtml'
506
842
  )
507
843
  parser.add_argument(
508
844
  '--output', '-o', type=str, default=None,
509
- help='输出文件路径(仅对 json 格式有效,默认为 keywords.json)'
845
+ help='输出文件路径(json格式默认为keywords.json,html格式默认为keywords.html)'
510
846
  )
511
847
  parser.add_argument(
512
848
  '--filter', type=str, default=None,
@@ -514,9 +850,14 @@ def main_list_keywords():
514
850
  )
515
851
  parser.add_argument(
516
852
  '--category',
517
- choices=['builtin', 'custom', 'remote', 'all'],
853
+ choices=['builtin', 'plugin', 'custom', 'project_custom', 'remote', 'all'],
518
854
  default='all',
519
- 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='是否包含远程关键字(默认不包含)'
520
861
  )
521
862
 
522
863
  args = parser.parse_args()
@@ -525,9 +866,112 @@ def main_list_keywords():
525
866
  output_format=args.format,
526
867
  name_filter=args.filter,
527
868
  category_filter=args.category,
528
- output_file=args.output
869
+ output_file=args.output,
870
+ include_remote=args.include_remote
529
871
  )
530
872
 
531
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
+
532
976
  if __name__ == '__main__':
533
977
  main()