pytest-dsl 0.14.0__py3-none-any.whl → 0.15.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.
@@ -0,0 +1,609 @@
1
+ """
2
+ pytest-dsl关键字工具
3
+
4
+ 提供统一的关键字列表查看、格式化和导出功能,供CLI和其他程序使用。
5
+ """
6
+
7
+ import json
8
+ import os
9
+ from typing import Dict, Any, Optional, Union, List
10
+
11
+ from pytest_dsl.core.keyword_loader import (
12
+ load_all_keywords, categorize_keyword, get_keyword_source_info,
13
+ group_keywords_by_source
14
+ )
15
+ from pytest_dsl.core.keyword_manager import keyword_manager
16
+
17
+
18
+ class KeywordInfo:
19
+ """关键字信息类"""
20
+
21
+ def __init__(self, name: str, info: Dict[str, Any],
22
+ project_custom_keywords: Optional[Dict[str, Any]] = None):
23
+ self.name = name
24
+ self.info = info
25
+ self.project_custom_keywords = project_custom_keywords
26
+ self._category = None
27
+ self._source_info = None
28
+
29
+ @property
30
+ def category(self) -> str:
31
+ """获取关键字类别"""
32
+ if self._category is None:
33
+ self._category = categorize_keyword(
34
+ self.name, self.info, self.project_custom_keywords
35
+ )
36
+ return self._category
37
+
38
+ @property
39
+ def source_info(self) -> Dict[str, Any]:
40
+ """获取来源信息"""
41
+ if self._source_info is None:
42
+ self._source_info = get_keyword_source_info(self.info)
43
+ return self._source_info
44
+
45
+ @property
46
+ def parameters(self) -> List[Dict[str, Any]]:
47
+ """获取参数信息"""
48
+ if (self.category == 'project_custom' and
49
+ self.project_custom_keywords and
50
+ self.name in self.project_custom_keywords):
51
+ return self.project_custom_keywords[self.name].get('parameters', [])
52
+
53
+ # 对于其他类型的关键字
54
+ parameters = self.info.get('parameters', [])
55
+ param_list = []
56
+ for param in parameters:
57
+ param_data = {
58
+ 'name': getattr(param, 'name', str(param)),
59
+ 'mapping': getattr(param, 'mapping', ''),
60
+ 'description': getattr(param, 'description', '')
61
+ }
62
+
63
+ # 添加默认值信息
64
+ param_default = getattr(param, 'default', None)
65
+ if param_default is not None:
66
+ param_data['default'] = param_default
67
+
68
+ param_list.append(param_data)
69
+
70
+ return param_list
71
+
72
+ @property
73
+ def documentation(self) -> str:
74
+ """获取文档信息"""
75
+ func = self.info.get('func')
76
+ if func and hasattr(func, '__doc__') and func.__doc__:
77
+ return func.__doc__.strip()
78
+ return ""
79
+
80
+ @property
81
+ def file_location(self) -> Optional[str]:
82
+ """获取文件位置(仅适用于项目自定义关键字)"""
83
+ if (self.category == 'project_custom' and
84
+ self.project_custom_keywords and
85
+ self.name in self.project_custom_keywords):
86
+ return self.project_custom_keywords[self.name]['file']
87
+ return None
88
+
89
+ @property
90
+ def remote_info(self) -> Optional[Dict[str, str]]:
91
+ """获取远程关键字信息"""
92
+ if self.info.get('remote', False):
93
+ return {
94
+ 'alias': self.info.get('alias', ''),
95
+ 'original_name': self.info.get('original_name', self.name)
96
+ }
97
+ return None
98
+
99
+
100
+ class KeywordListOptions:
101
+ """关键字列表选项"""
102
+
103
+ def __init__(self,
104
+ output_format: str = 'json',
105
+ name_filter: Optional[str] = None,
106
+ category_filter: str = 'all',
107
+ include_remote: bool = False,
108
+ output_file: Optional[str] = None):
109
+ self.output_format = output_format
110
+ self.name_filter = name_filter
111
+ self.category_filter = category_filter
112
+ self.include_remote = include_remote
113
+ self.output_file = output_file
114
+
115
+ def should_include_keyword(self, keyword_info: KeywordInfo) -> bool:
116
+ """判断是否应该包含此关键字"""
117
+ # 名称过滤
118
+ if (self.name_filter and
119
+ self.name_filter.lower() not in keyword_info.name.lower()):
120
+ return False
121
+
122
+ # 远程关键字过滤
123
+ if (not self.include_remote and
124
+ keyword_info.info.get('remote', False)):
125
+ return False
126
+
127
+ # 类别过滤
128
+ if (self.category_filter != 'all' and
129
+ keyword_info.category != self.category_filter):
130
+ return False
131
+
132
+ return True
133
+
134
+
135
+ class KeywordFormatter:
136
+ """关键字格式化器"""
137
+
138
+ def __init__(self):
139
+ self.category_names = {
140
+ 'builtin': '内置',
141
+ 'plugin': '插件',
142
+ 'custom': '自定义',
143
+ 'project_custom': '项目自定义',
144
+ 'remote': '远程'
145
+ }
146
+
147
+ def format_text(self, keyword_info: KeywordInfo,
148
+ show_category: bool = True) -> str:
149
+ """格式化为文本格式"""
150
+ lines = []
151
+
152
+ # 关键字名称和类别
153
+ if show_category:
154
+ category_display = self.category_names.get(
155
+ keyword_info.category, '未知'
156
+ )
157
+ lines.append(f"关键字: {keyword_info.name} [{category_display}]")
158
+ else:
159
+ lines.append(f"关键字: {keyword_info.name}")
160
+
161
+ # 远程关键字特殊信息
162
+ if keyword_info.remote_info:
163
+ remote = keyword_info.remote_info
164
+ lines.append(f" 远程服务器: {remote['alias']}")
165
+ lines.append(f" 原始名称: {remote['original_name']}")
166
+
167
+ # 项目自定义关键字文件位置
168
+ if keyword_info.file_location:
169
+ lines.append(f" 文件位置: {keyword_info.file_location}")
170
+
171
+ # 参数信息
172
+ parameters = keyword_info.parameters
173
+ if parameters:
174
+ lines.append(" 参数:")
175
+ for param_info in parameters:
176
+ param_name = param_info['name']
177
+ param_mapping = param_info.get('mapping', '')
178
+ param_desc = param_info.get('description', '')
179
+ param_default = param_info.get('default', None)
180
+
181
+ # 构建参数描述
182
+ param_parts = []
183
+ if param_mapping and param_mapping != param_name:
184
+ param_parts.append(f"{param_name} ({param_mapping})")
185
+ else:
186
+ param_parts.append(param_name)
187
+
188
+ param_parts.append(f": {param_desc}")
189
+
190
+ # 添加默认值信息
191
+ if param_default is not None:
192
+ param_parts.append(f" (默认值: {param_default})")
193
+
194
+ lines.append(f" {''.join(param_parts)}")
195
+ else:
196
+ lines.append(" 参数: 无")
197
+
198
+ # 函数文档
199
+ if keyword_info.documentation:
200
+ lines.append(f" 说明: {keyword_info.documentation}")
201
+
202
+ return '\n'.join(lines)
203
+
204
+ def format_json(self, keyword_info: KeywordInfo) -> Dict[str, Any]:
205
+ """格式化为JSON格式"""
206
+ keyword_data = {
207
+ 'name': keyword_info.name,
208
+ 'category': keyword_info.category,
209
+ 'source_info': keyword_info.source_info,
210
+ 'parameters': keyword_info.parameters
211
+ }
212
+
213
+ # 远程关键字特殊信息
214
+ if keyword_info.remote_info:
215
+ keyword_data['remote'] = keyword_info.remote_info
216
+
217
+ # 项目自定义关键字文件位置
218
+ if keyword_info.file_location:
219
+ keyword_data['file_location'] = keyword_info.file_location
220
+
221
+ # 函数文档
222
+ if keyword_info.documentation:
223
+ keyword_data['documentation'] = keyword_info.documentation
224
+
225
+ return keyword_data
226
+
227
+
228
+ class KeywordLister:
229
+ """关键字列表器"""
230
+
231
+ def __init__(self):
232
+ self.formatter = KeywordFormatter()
233
+ self._project_custom_keywords = None
234
+
235
+ def get_keywords(self, options: KeywordListOptions) -> List[KeywordInfo]:
236
+ """获取关键字列表
237
+
238
+ Args:
239
+ options: 列表选项
240
+
241
+ Returns:
242
+ 符合条件的关键字信息列表
243
+ """
244
+ # 加载关键字
245
+ if self._project_custom_keywords is None:
246
+ self._project_custom_keywords = load_all_keywords(
247
+ include_remote=options.include_remote
248
+ )
249
+
250
+ # 获取所有注册的关键字
251
+ all_keywords = keyword_manager._keywords
252
+
253
+ if not all_keywords:
254
+ return []
255
+
256
+ # 过滤关键字
257
+ filtered_keywords = []
258
+ for name, info in all_keywords.items():
259
+ keyword_info = KeywordInfo(
260
+ name, info, self._project_custom_keywords
261
+ )
262
+
263
+ if options.should_include_keyword(keyword_info):
264
+ filtered_keywords.append(keyword_info)
265
+
266
+ return filtered_keywords
267
+
268
+ def get_keywords_summary(self, keywords: List[KeywordInfo]) -> Dict[str, Any]:
269
+ """获取关键字统计摘要
270
+
271
+ Args:
272
+ keywords: 关键字列表
273
+
274
+ Returns:
275
+ 统计摘要信息
276
+ """
277
+ total_count = len(keywords)
278
+ category_counts = {}
279
+ source_counts = {}
280
+
281
+ for keyword_info in keywords:
282
+ # 类别统计
283
+ cat = keyword_info.category
284
+ category_counts[cat] = category_counts.get(cat, 0) + 1
285
+
286
+ # 来源统计
287
+ source_name = keyword_info.source_info['name']
288
+ if keyword_info.file_location:
289
+ source_name = keyword_info.file_location
290
+
291
+ source_key = f"{cat}:{source_name}"
292
+ source_counts[source_key] = source_counts.get(source_key, 0) + 1
293
+
294
+ return {
295
+ 'total_count': total_count,
296
+ 'category_counts': category_counts,
297
+ 'source_counts': source_counts
298
+ }
299
+
300
+ def list_keywords_text(self, options: KeywordListOptions) -> str:
301
+ """以文本格式列出关键字"""
302
+ keywords = self.get_keywords(options)
303
+ summary = self.get_keywords_summary(keywords)
304
+
305
+ if not keywords:
306
+ if options.name_filter:
307
+ return f"未找到包含 '{options.name_filter}' 的关键字"
308
+ else:
309
+ return f"未找到 {options.category_filter} 类别的关键字"
310
+
311
+ lines = []
312
+
313
+ # 统计信息
314
+ lines.append(f"找到 {summary['total_count']} 个关键字:")
315
+ for cat, count in summary['category_counts'].items():
316
+ cat_display = self.formatter.category_names.get(cat, cat)
317
+ lines.append(f" {cat_display}: {count} 个")
318
+ lines.append("-" * 60)
319
+
320
+ # 按类别和来源分组显示
321
+ all_keywords_dict = {kw.name: kw.info for kw in keywords}
322
+ grouped = group_keywords_by_source(
323
+ all_keywords_dict, self._project_custom_keywords
324
+ )
325
+
326
+ for category in ['builtin', 'plugin', 'custom', 'project_custom', 'remote']:
327
+ if category not in grouped or not grouped[category]:
328
+ continue
329
+
330
+ cat_names = {
331
+ 'builtin': '内置关键字',
332
+ 'plugin': '插件关键字',
333
+ 'custom': '自定义关键字',
334
+ 'project_custom': '项目自定义关键字',
335
+ 'remote': '远程关键字'
336
+ }
337
+ lines.append(f"\n=== {cat_names[category]} ===")
338
+
339
+ for source_name, keyword_list in grouped[category].items():
340
+ if len(grouped[category]) > 1: # 如果有多个来源,显示来源名
341
+ lines.append(f"\n--- {source_name} ---")
342
+
343
+ for keyword_data in keyword_list:
344
+ name = keyword_data['name']
345
+ keyword_info = next(
346
+ kw for kw in keywords if kw.name == name)
347
+ lines.append("")
348
+ lines.append(self.formatter.format_text(
349
+ keyword_info, show_category=False
350
+ ))
351
+
352
+ return '\n'.join(lines)
353
+
354
+ def list_keywords_json(self, options: KeywordListOptions) -> Dict[str, Any]:
355
+ """以JSON格式列出关键字"""
356
+ keywords = self.get_keywords(options)
357
+ summary = self.get_keywords_summary(keywords)
358
+
359
+ keywords_data = {
360
+ 'summary': summary,
361
+ 'keywords': []
362
+ }
363
+
364
+ # 按名称排序
365
+ keywords.sort(key=lambda x: x.name)
366
+
367
+ for keyword_info in keywords:
368
+ keyword_data = self.formatter.format_json(keyword_info)
369
+ keywords_data['keywords'].append(keyword_data)
370
+
371
+ return keywords_data
372
+
373
+ def list_keywords(self, options: KeywordListOptions) -> Union[str, Dict[str, Any]]:
374
+ """列出关键字(根据格式返回不同类型)
375
+
376
+ Args:
377
+ options: 列表选项
378
+
379
+ Returns:
380
+ 文本格式返回str,JSON格式返回dict,HTML格式返回dict
381
+ """
382
+ if options.output_format == 'text':
383
+ return self.list_keywords_text(options)
384
+ elif options.output_format in ['json', 'html']:
385
+ return self.list_keywords_json(options)
386
+ else:
387
+ raise ValueError(f"不支持的输出格式: {options.output_format}")
388
+
389
+
390
+ def generate_html_report(keywords_data: Dict[str, Any], output_file: str):
391
+ """生成HTML格式的关键字报告
392
+
393
+ Args:
394
+ keywords_data: 关键字数据(JSON格式)
395
+ output_file: 输出文件路径
396
+ """
397
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
398
+
399
+ # 准备数据
400
+ summary = keywords_data['summary']
401
+ keywords = keywords_data['keywords']
402
+
403
+ # 按类别分组
404
+ categories = {}
405
+ for keyword in keywords:
406
+ category = keyword['category']
407
+ if category not in categories:
408
+ categories[category] = []
409
+ categories[category].append(keyword)
410
+
411
+ # 按来源分组
412
+ source_groups = {}
413
+ for keyword in keywords:
414
+ source_info = keyword.get('source_info', {})
415
+ category = keyword['category']
416
+ source_name = source_info.get('name', '未知来源')
417
+
418
+ # 构建分组键
419
+ if category == 'plugin':
420
+ group_key = f"插件 - {source_name}"
421
+ elif category == 'builtin':
422
+ group_key = "内置关键字"
423
+ elif category == 'project_custom':
424
+ group_key = f"项目自定义 - {keyword.get('file_location', source_name)}"
425
+ elif category == 'remote':
426
+ group_key = f"远程 - {source_name}"
427
+ else:
428
+ group_key = f"自定义 - {source_name}"
429
+
430
+ if group_key not in source_groups:
431
+ source_groups[group_key] = []
432
+ source_groups[group_key].append(keyword)
433
+
434
+ # 类别名称映射
435
+ category_names = {
436
+ 'builtin': '内置',
437
+ 'plugin': '插件',
438
+ 'custom': '自定义',
439
+ 'project_custom': '项目自定义',
440
+ 'remote': '远程'
441
+ }
442
+
443
+ # 设置Jinja2环境
444
+ template_dir = os.path.join(os.path.dirname(__file__), '..', 'templates')
445
+
446
+ env = Environment(
447
+ loader=FileSystemLoader(template_dir),
448
+ autoescape=select_autoescape(['html', 'xml'])
449
+ )
450
+
451
+ # 加载模板
452
+ template = env.get_template('keywords_report.html')
453
+
454
+ # 渲染模板
455
+ html_content = template.render(
456
+ summary=summary,
457
+ keywords=keywords,
458
+ categories=categories,
459
+ source_groups=source_groups,
460
+ category_names=category_names
461
+ )
462
+
463
+ # 写入文件
464
+ with open(output_file, 'w', encoding='utf-8') as f:
465
+ f.write(html_content)
466
+
467
+
468
+ # 创建全局实例
469
+ keyword_lister = KeywordLister()
470
+
471
+
472
+ # 便捷函数
473
+ def list_keywords(output_format: str = 'json',
474
+ name_filter: Optional[str] = None,
475
+ category_filter: str = 'all',
476
+ include_remote: bool = False,
477
+ output_file: Optional[str] = None,
478
+ print_summary: bool = True) -> Union[str, Dict[str, Any], None]:
479
+ """列出关键字的便捷函数
480
+
481
+ Args:
482
+ output_format: 输出格式 ('text', 'json', 'html')
483
+ name_filter: 名称过滤器(支持部分匹配)
484
+ category_filter: 类别过滤器
485
+ include_remote: 是否包含远程关键字
486
+ output_file: 输出文件路径(可选)
487
+ print_summary: 是否打印摘要信息
488
+
489
+ Returns:
490
+ 根据输出格式返回相应的数据,如果输出到文件则返回None
491
+ """
492
+ options = KeywordListOptions(
493
+ output_format=output_format,
494
+ name_filter=name_filter,
495
+ category_filter=category_filter,
496
+ include_remote=include_remote,
497
+ output_file=output_file
498
+ )
499
+
500
+ # 获取数据
501
+ result = keyword_lister.list_keywords(options)
502
+
503
+ if isinstance(result, str):
504
+ # 文本格式
505
+ if output_file:
506
+ with open(output_file, 'w', encoding='utf-8') as f:
507
+ f.write(result)
508
+ if print_summary:
509
+ print(f"关键字信息已保存到文件: {output_file}")
510
+ return None
511
+ else:
512
+ return result
513
+
514
+ elif isinstance(result, dict):
515
+ # JSON或HTML格式
516
+ if output_format == 'json':
517
+ json_output = json.dumps(result, ensure_ascii=False, indent=2)
518
+
519
+ if output_file:
520
+ with open(output_file, 'w', encoding='utf-8') as f:
521
+ f.write(json_output)
522
+ if print_summary:
523
+ _print_json_summary(result, output_file)
524
+ return None
525
+ else:
526
+ return result
527
+
528
+ elif output_format == 'html':
529
+ if not output_file:
530
+ output_file = 'keywords.html'
531
+
532
+ try:
533
+ generate_html_report(result, output_file)
534
+ if print_summary:
535
+ _print_json_summary(result, output_file, is_html=True)
536
+ return None
537
+ except Exception as e:
538
+ if print_summary:
539
+ print(f"生成HTML报告失败: {e}")
540
+ raise
541
+
542
+ return result
543
+
544
+
545
+ def _print_json_summary(keywords_data: Dict[str, Any],
546
+ output_file: str, is_html: bool = False):
547
+ """打印JSON数据的摘要信息"""
548
+ summary = keywords_data['summary']
549
+ total_count = summary['total_count']
550
+ category_counts = summary['category_counts']
551
+
552
+ if is_html:
553
+ print(f"HTML报告已生成: {output_file}")
554
+ else:
555
+ print(f"关键字信息已保存到文件: {output_file}")
556
+
557
+ print(f"共 {total_count} 个关键字")
558
+
559
+ category_names = {
560
+ 'builtin': '内置',
561
+ 'plugin': '插件',
562
+ 'custom': '自定义',
563
+ 'project_custom': '项目自定义',
564
+ 'remote': '远程'
565
+ }
566
+
567
+ for cat, count in category_counts.items():
568
+ cat_display = category_names.get(cat, cat)
569
+ print(f" {cat_display}: {count} 个")
570
+
571
+
572
+ def get_keyword_info(keyword_name: str,
573
+ include_remote: bool = False) -> Optional[KeywordInfo]:
574
+ """获取单个关键字的详细信息
575
+
576
+ Args:
577
+ keyword_name: 关键字名称
578
+ include_remote: 是否包含远程关键字
579
+
580
+ Returns:
581
+ 关键字信息对象,如果未找到则返回None
582
+ """
583
+ # 加载关键字
584
+ project_custom_keywords = load_all_keywords(include_remote=include_remote)
585
+
586
+ # 获取关键字信息
587
+ keyword_info = keyword_manager.get_keyword_info(keyword_name)
588
+ if not keyword_info:
589
+ return None
590
+
591
+ return KeywordInfo(keyword_name, keyword_info, project_custom_keywords)
592
+
593
+
594
+ def search_keywords(pattern: str,
595
+ include_remote: bool = False) -> List[KeywordInfo]:
596
+ """搜索匹配模式的关键字
597
+
598
+ Args:
599
+ pattern: 搜索模式(支持部分匹配)
600
+ include_remote: 是否包含远程关键字
601
+
602
+ Returns:
603
+ 匹配的关键字信息列表
604
+ """
605
+ options = KeywordListOptions(
606
+ name_filter=pattern,
607
+ include_remote=include_remote
608
+ )
609
+ return keyword_lister.get_keywords(options)
pytest_dsl/core/lexer.py CHANGED
@@ -90,6 +90,7 @@ t_PLACEHOLDER = (r'\$\{[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*'
90
90
  # 添加管道符的正则表达式定义
91
91
  t_PIPE = r'\|'
92
92
 
93
+
93
94
  # 添加@remote关键字的token规则
94
95
  def t_REMOTE_KEYWORD(t):
95
96
  r'@remote'
@@ -111,7 +112,14 @@ def t_STRING(t):
111
112
  r"""(\'\'\'[\s\S]*?\'\'\'|\"\"\"[\s\S]*?\"\"\"|'[^']*'|\"[^\"]*\")"""
112
113
  # 处理单引号和双引号的多行/单行字符串
113
114
  if t.value.startswith("'''") or t.value.startswith('"""'):
115
+ # 对于多行字符串,需要正确更新行号
116
+ original_value = t.value
114
117
  t.value = t.value[3:-3] # 去掉三引号
118
+
119
+ # 计算多行字符串包含的换行符数量,更新词法分析器的行号
120
+ newlines = original_value.count('\n')
121
+ if newlines > 0:
122
+ t.lexer.lineno += newlines
115
123
  else:
116
124
  t.value = t.value[1:-1] # 去掉单引号或双引号
117
125
  return t