pytest-dsl 0.13.0__py3-none-any.whl → 0.15.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.
@@ -18,7 +18,7 @@ class KeywordManager:
18
18
 
19
19
  def register(self, name: str, parameters: List[Dict], source_info: Optional[Dict] = None):
20
20
  """关键字注册装饰器
21
-
21
+
22
22
  Args:
23
23
  name: 关键字名称
24
24
  parameters: 参数列表
@@ -29,23 +29,29 @@ class KeywordManager:
29
29
  def wrapper(**kwargs):
30
30
  # 获取自定义步骤名称,如果未指定则使用关键字名称
31
31
  step_name = kwargs.pop('step_name', name)
32
-
32
+
33
+ # 检查是否已经在DSL执行器的步骤中,避免重复记录
34
+ skip_logging = kwargs.pop('skip_logging', False)
35
+
33
36
  with allure.step(f"{step_name}"):
34
37
  try:
35
38
  result = func(**kwargs)
36
- self._log_execution(step_name, kwargs, result)
39
+ if not skip_logging:
40
+ self._log_execution(step_name, kwargs, result)
37
41
  return result
38
42
  except Exception as e:
39
- self._log_failure(step_name, kwargs, e)
43
+ if not skip_logging:
44
+ self._log_failure(step_name, kwargs, e)
40
45
  raise
41
46
 
42
47
  param_list = [Parameter(**p) for p in parameters]
43
48
  mapping = {p.name: p.mapping for p in param_list}
44
- defaults = {p.mapping: p.default for p in param_list if p.default is not None}
45
-
49
+ defaults = {
50
+ p.mapping: p.default for p in param_list if p.default is not None}
51
+
46
52
  # 自动添加 step_name 到 mapping 中
47
53
  mapping["步骤名称"] = "step_name"
48
-
54
+
49
55
  # 构建关键字信息,包含来源信息
50
56
  keyword_info = {
51
57
  'func': wrapper,
@@ -53,14 +59,14 @@ class KeywordManager:
53
59
  'parameters': param_list,
54
60
  'defaults': defaults # 存储默认值
55
61
  }
56
-
62
+
57
63
  # 添加来源信息
58
64
  if source_info:
59
65
  keyword_info.update(source_info)
60
66
  else:
61
67
  # 尝试从函数模块推断来源信息
62
68
  keyword_info.update(self._infer_source_info(func))
63
-
69
+
64
70
  self._keywords[name] = keyword_info
65
71
  return wrapper
66
72
  return decorator
@@ -68,11 +74,11 @@ class KeywordManager:
68
74
  def _infer_source_info(self, func: Callable) -> Dict:
69
75
  """从函数推断来源信息"""
70
76
  source_info = {}
71
-
77
+
72
78
  if hasattr(func, '__module__'):
73
79
  module_name = func.__module__
74
80
  source_info['module_name'] = module_name
75
-
81
+
76
82
  if module_name.startswith('pytest_dsl.keywords'):
77
83
  # 内置关键字
78
84
  source_info['source_type'] = 'builtin'
@@ -90,13 +96,13 @@ class KeywordManager:
90
96
  source_info['source_name'] = parts[0]
91
97
  else:
92
98
  source_info['source_name'] = module_name
93
-
99
+
94
100
  return source_info
95
101
 
96
- def register_with_source(self, name: str, parameters: List[Dict],
97
- source_type: str, source_name: str, **kwargs):
102
+ def register_with_source(self, name: str, parameters: List[Dict],
103
+ source_type: str, source_name: str, **kwargs):
98
104
  """带来源信息的关键字注册装饰器
99
-
105
+
100
106
  Args:
101
107
  name: 关键字名称
102
108
  parameters: 参数列表
@@ -116,18 +122,18 @@ class KeywordManager:
116
122
  keyword_info = self._keywords.get(keyword_name)
117
123
  if not keyword_info:
118
124
  raise KeyError(f"未注册的关键字: {keyword_name}")
119
-
125
+
120
126
  # 应用默认值
121
127
  final_params = {}
122
128
  defaults = keyword_info.get('defaults', {})
123
-
129
+
124
130
  # 首先设置所有默认值
125
131
  for param_key, default_value in defaults.items():
126
132
  final_params[param_key] = default_value
127
-
133
+
128
134
  # 然后用传入的参数覆盖默认值
129
135
  final_params.update(params)
130
-
136
+
131
137
  return keyword_info['func'](**final_params)
132
138
 
133
139
  def get_keyword_info(self, keyword_name: str) -> Dict:
@@ -135,7 +141,7 @@ class KeywordManager:
135
141
  keyword_info = self._keywords.get(keyword_name)
136
142
  if not keyword_info:
137
143
  return None
138
-
144
+
139
145
  # 动态添加step_name参数到参数列表中
140
146
  if not any(p.name == "步骤名称" for p in keyword_info['parameters']):
141
147
  keyword_info['parameters'].append(Parameter(
@@ -143,19 +149,19 @@ class KeywordManager:
143
149
  mapping="step_name",
144
150
  description="自定义的步骤名称,用于在报告中显示"
145
151
  ))
146
-
152
+
147
153
  return keyword_info
148
154
 
149
155
  def get_keywords_by_source(self) -> Dict[str, List[str]]:
150
156
  """按来源分组获取关键字"""
151
157
  by_source = {}
152
-
158
+
153
159
  for name, info in self._keywords.items():
154
160
  source_name = info.get('source_name', '未知来源')
155
161
  if source_name not in by_source:
156
162
  by_source[source_name] = []
157
163
  by_source[source_name].append(name)
158
-
164
+
159
165
  return by_source
160
166
 
161
167
  def _log_execution(self, keyword_name: str, params: Dict, result: Any) -> None:
pytest_dsl/core/parser.py CHANGED
@@ -1,12 +1,30 @@
1
1
  import ply.yacc as yacc
2
- from pytest_dsl.core.lexer import tokens, get_lexer
2
+ from pytest_dsl.core.lexer import tokens
3
3
 
4
4
 
5
5
  class Node:
6
- def __init__(self, type, children=None, value=None):
6
+ def __init__(self, type, children=None, value=None, line_number=None,
7
+ column=None):
7
8
  self.type = type
8
9
  self.children = children if children else []
9
10
  self.value = value
11
+ self.line_number = line_number # 添加行号信息
12
+ self.column = column # 添加列号信息
13
+
14
+ def set_position(self, line_number, column=None):
15
+ """设置节点位置信息"""
16
+ self.line_number = line_number
17
+ self.column = column
18
+ return self
19
+
20
+ def get_position_info(self):
21
+ """获取位置信息的字符串表示"""
22
+ if self.line_number is not None:
23
+ if self.column is not None:
24
+ return f"行{self.line_number}:列{self.column}"
25
+ else:
26
+ return f"行{self.line_number}"
27
+ return "位置未知"
10
28
 
11
29
 
12
30
  # 定义优先级和结合性
@@ -25,17 +43,23 @@ def p_start(p):
25
43
  | statements teardown
26
44
  | statements'''
27
45
 
46
+ # 获取起始行号
47
+ line_number = getattr(p.slice[1], 'lineno', 1) if len(p) > 1 else 1
48
+
28
49
  if len(p) == 4:
29
- p[0] = Node('Start', [p[1], p[2], p[3]])
50
+ p[0] = Node('Start', [p[1], p[2], p[3]], line_number=line_number)
30
51
  elif len(p) == 3:
31
52
  # 判断第二个元素是teardown还是statements
32
53
  if p[2].type == 'Teardown':
33
- p[0] = Node('Start', [Node('Metadata', []), p[1], p[2]])
54
+ p[0] = Node('Start', [Node('Metadata', [],
55
+ line_number=line_number), p[1], p[2]],
56
+ line_number=line_number)
34
57
  else:
35
- p[0] = Node('Start', [p[1], p[2]])
58
+ p[0] = Node('Start', [p[1], p[2]], line_number=line_number)
36
59
  else:
37
60
  # 没有metadata和teardown
38
- p[0] = Node('Start', [Node('Metadata', []), p[1]])
61
+ p[0] = Node('Start', [Node('Metadata', [],
62
+ line_number=line_number), p[1]], line_number=line_number)
39
63
 
40
64
 
41
65
  def p_metadata(p):
@@ -135,12 +159,17 @@ def p_assignment(p):
135
159
  '''assignment : ID EQUALS expression
136
160
  | ID EQUALS keyword_call
137
161
  | ID EQUALS remote_keyword_call'''
162
+ line_number = getattr(p.slice[1], 'lineno', None)
163
+
138
164
  if isinstance(p[3], Node) and p[3].type == 'KeywordCall':
139
- p[0] = Node('AssignmentKeywordCall', [p[3]], p[1])
165
+ p[0] = Node('AssignmentKeywordCall', [p[3]],
166
+ p[1], line_number=line_number)
140
167
  elif isinstance(p[3], Node) and p[3].type == 'RemoteKeywordCall':
141
- p[0] = Node('AssignmentRemoteKeywordCall', [p[3]], p[1])
168
+ p[0] = Node('AssignmentRemoteKeywordCall', [
169
+ p[3]], p[1], line_number=line_number)
142
170
  else:
143
- p[0] = Node('Assignment', value=p[1], children=[p[3]])
171
+ p[0] = Node('Assignment', value=p[1], children=[
172
+ p[3]], line_number=line_number)
144
173
 
145
174
 
146
175
  def p_expression(p):
@@ -225,17 +254,20 @@ def p_dict_item(p):
225
254
 
226
255
 
227
256
  def p_loop(p):
228
- '''loop : FOR ID IN RANGE LPAREN expression COMMA expression RPAREN DO statements END'''
229
- p[0] = Node('ForLoop', [p[6], p[8], p[11]], p[2])
257
+ '''loop : FOR ID IN RANGE LPAREN expression COMMA expression RPAREN DO statements END''' # noqa: E501
258
+ line_number = getattr(p.slice[1], 'lineno', None)
259
+ p[0] = Node('ForLoop', [p[6], p[8], p[11]], p[2], line_number=line_number)
230
260
 
231
261
 
232
262
  def p_keyword_call(p):
233
263
  '''keyword_call : LBRACKET ID RBRACKET COMMA parameter_list
234
264
  | LBRACKET ID RBRACKET'''
265
+ line_number = getattr(p.slice[1], 'lineno', None)
266
+
235
267
  if len(p) == 6:
236
- p[0] = Node('KeywordCall', [p[5]], p[2])
268
+ p[0] = Node('KeywordCall', [p[5]], p[2], line_number=line_number)
237
269
  else:
238
- p[0] = Node('KeywordCall', [[]], p[2])
270
+ p[0] = Node('KeywordCall', [[]], p[2], line_number=line_number)
239
271
 
240
272
 
241
273
  def p_parameter_list(p):
@@ -268,7 +300,7 @@ def p_data_source(p):
268
300
 
269
301
 
270
302
  def p_custom_keyword(p):
271
- '''custom_keyword : FUNCTION ID LPAREN param_definitions RPAREN DO statements END'''
303
+ '''custom_keyword : FUNCTION ID LPAREN param_definitions RPAREN DO statements END''' # noqa: E501
272
304
  p[0] = Node('CustomKeyword', [p[4], p[7]], p[2])
273
305
 
274
306
 
@@ -319,7 +351,7 @@ def p_if_statement(p):
319
351
  '''if_statement : IF expression DO statements END
320
352
  | IF expression DO statements elif_clauses END
321
353
  | IF expression DO statements ELSE statements END
322
- | IF expression DO statements elif_clauses ELSE statements END'''
354
+ | IF expression DO statements elif_clauses ELSE statements END''' # noqa: E501
323
355
  if len(p) == 6:
324
356
  # if condition do statements end
325
357
  p[0] = Node('IfStatement', [p[2], p[4]], None)
@@ -401,17 +433,61 @@ def p_arithmetic_expr(p):
401
433
  p[0] = Node('ArithmeticExpr', [p[1], p[3]], operator)
402
434
 
403
435
 
436
+ # 全局变量用于存储解析错误
437
+ _parse_errors = []
438
+
439
+
404
440
  def p_error(p):
441
+ global _parse_errors
405
442
  if p:
406
- print(
407
- f"语法错误: 在第 {p.lineno} 行, 位置 {p.lexpos}, Token {p.type}, 值: {p.value}")
443
+ error_msg = (f"语法错误: 在第 {p.lineno} 行, 位置 {p.lexpos}, "
444
+ f"Token {p.type}, 值: {p.value}")
445
+ _parse_errors.append({
446
+ 'message': error_msg,
447
+ 'line': p.lineno,
448
+ 'position': p.lexpos,
449
+ 'token_type': p.type,
450
+ 'token_value': p.value
451
+ })
452
+ # 不再直接打印,而是存储错误信息
408
453
  else:
409
- print("语法错误: 在文件末尾")
454
+ error_msg = "语法错误: 在文件末尾"
455
+ _parse_errors.append({
456
+ 'message': error_msg,
457
+ 'line': None,
458
+ 'position': None,
459
+ 'token_type': None,
460
+ 'token_value': None
461
+ })
410
462
 
411
463
 
412
464
  def get_parser(debug=False):
413
465
  return yacc.yacc(debug=debug)
414
466
 
467
+
468
+ def parse_with_error_handling(content, lexer=None):
469
+ """带错误处理的解析函数
470
+
471
+ Args:
472
+ content: DSL内容
473
+ lexer: 词法分析器实例
474
+
475
+ Returns:
476
+ tuple: (AST节点, 错误列表)
477
+ """
478
+ global _parse_errors
479
+ _parse_errors = [] # 清空之前的错误
480
+
481
+ if lexer is None:
482
+ from pytest_dsl.core.lexer import get_lexer
483
+ lexer = get_lexer()
484
+
485
+ parser = get_parser()
486
+ ast = parser.parse(content, lexer=lexer)
487
+
488
+ # 返回AST和错误列表
489
+ return ast, _parse_errors.copy()
490
+
415
491
  # 定义远程关键字调用的语法规则
416
492
 
417
493