pytest-dsl 0.6.0__py3-none-any.whl → 0.8.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.
@@ -13,6 +13,16 @@ from pytest_dsl.core.yaml_vars import yaml_vars
13
13
  from pytest_dsl.core.variable_utils import VariableReplacer
14
14
 
15
15
 
16
+ class BreakException(Exception):
17
+ """Break控制流异常"""
18
+ pass
19
+
20
+
21
+ class ContinueException(Exception):
22
+ """Continue控制流异常"""
23
+ pass
24
+
25
+
16
26
  class DSLExecutor:
17
27
  """DSL执行器,负责执行解析后的AST
18
28
 
@@ -90,6 +100,15 @@ class DSLExecutor:
90
100
  item_value = self.eval_expression(item)
91
101
  result.append(item_value)
92
102
  return result
103
+ elif expr_node.type == 'DictExpr':
104
+ # 处理字典表达式
105
+ result = {}
106
+ for item in expr_node.children:
107
+ # 每个item是DictItem节点,包含键和值
108
+ key_value = self.eval_expression(item.children[0])
109
+ value_value = self.eval_expression(item.children[1])
110
+ result[key_value] = value_value
111
+ return result
93
112
  elif expr_node.type == 'BooleanExpr':
94
113
  # 处理布尔值表达式
95
114
  return expr_node.value
@@ -111,13 +130,14 @@ class DSLExecutor:
111
130
  if value in self.variable_replacer.local_variables:
112
131
  return self.variable_replacer.local_variables[value]
113
132
 
114
- # 定义变量引用模式
115
- pattern = r'\$\{([a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)\}'
133
+ # 定义扩展的变量引用模式,支持数组索引和字典键访问
134
+ pattern = r'\$\{([a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*(?:(?:\.[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)|(?:\[[^\]]+\]))*)\}'
116
135
  # 检查整个字符串是否完全匹配单一变量引用模式
117
136
  match = re.fullmatch(pattern, value)
118
137
  if match:
119
- var_name = match.group(1)
120
- return self.variable_replacer.get_variable(var_name)
138
+ var_ref = match.group(1)
139
+ # 使用新的变量路径解析器
140
+ return self.variable_replacer._parse_variable_path(var_ref)
121
141
  else:
122
142
  # 如果不是单一变量,则替换字符串中的所有变量引用
123
143
  return self.variable_replacer.replace_in_string(value)
@@ -165,7 +185,7 @@ class DSLExecutor:
165
185
  """
166
186
  left_value = self.eval_expression(expr_node.children[0])
167
187
  right_value = self.eval_expression(expr_node.children[1])
168
- operator = expr_node.value # 操作符: +, -, *, /
188
+ operator = expr_node.value # 操作符: +, -, *, /, %
169
189
 
170
190
  # 尝试类型转换 - 如果是字符串数字则转为数字
171
191
  if isinstance(left_value, str) and str(left_value).replace('.', '', 1).isdigit():
@@ -200,6 +220,11 @@ class DSLExecutor:
200
220
  if right_value == 0:
201
221
  raise Exception("除法错误: 除数不能为0")
202
222
  return left_value / right_value
223
+ elif operator == '%':
224
+ # 模运算时检查除数是否为0
225
+ if right_value == 0:
226
+ raise Exception("模运算错误: 除数不能为0")
227
+ return left_value % right_value
203
228
  else:
204
229
  raise Exception(f"未知的算术操作符: {operator}")
205
230
 
@@ -448,7 +473,24 @@ class DSLExecutor:
448
473
  self.variable_replacer.local_variables[var_name] = i
449
474
  self.test_context.set(var_name, i) # 同时添加到测试上下文
450
475
  with allure.step(f"循环轮次: {var_name} = {i}"):
451
- self.execute(node.children[2])
476
+ try:
477
+ self.execute(node.children[2])
478
+ except BreakException:
479
+ # 遇到break语句,退出循环
480
+ allure.attach(
481
+ f"在 {var_name} = {i} 时遇到break语句,退出循环",
482
+ name="循环Break",
483
+ attachment_type=allure.attachment_type.TEXT
484
+ )
485
+ break
486
+ except ContinueException:
487
+ # 遇到continue语句,跳过本次循环
488
+ allure.attach(
489
+ f"在 {var_name} = {i} 时遇到continue语句,跳过本次循环",
490
+ name="循环Continue",
491
+ attachment_type=allure.attachment_type.TEXT
492
+ )
493
+ continue
452
494
 
453
495
  def _execute_keyword_call(self, node):
454
496
  """执行关键字调用"""
@@ -501,26 +543,63 @@ class DSLExecutor:
501
543
  expr_node = node.children[0]
502
544
  return self.eval_expression(expr_node)
503
545
 
546
+ @allure.step("执行break语句")
547
+ def _handle_break(self, node):
548
+ """处理break语句
549
+
550
+ Args:
551
+ node: Break节点
552
+
553
+ Raises:
554
+ BreakException: 抛出异常来实现break控制流
555
+ """
556
+ raise BreakException()
557
+
558
+ @allure.step("执行continue语句")
559
+ def _handle_continue(self, node):
560
+ """处理continue语句
561
+
562
+ Args:
563
+ node: Continue节点
564
+
565
+ Raises:
566
+ ContinueException: 抛出异常来实现continue控制流
567
+ """
568
+ raise ContinueException()
569
+
504
570
  @allure.step("执行条件语句")
505
571
  def _handle_if_statement(self, node):
506
- """处理if-else语句
572
+ """处理if-elif-else语句
507
573
 
508
574
  Args:
509
- node: IfStatement节点,包含条件表达式、if分支和可选的else分支
575
+ node: IfStatement节点,包含条件表达式、if分支、可选的elif分支和可选的else分支
510
576
  """
577
+ # 首先检查if条件
511
578
  condition = self.eval_expression(node.children[0])
512
579
 
513
- # 将条件转换为布尔值进行评估
514
580
  if condition:
515
581
  # 执行if分支
516
582
  with allure.step("执行if分支"):
517
583
  return self.execute(node.children[1])
518
- elif len(node.children) > 2:
519
- # 如果存在else分支且条件为假,则执行else分支
520
- with allure.step("执行else分支"):
521
- return self.execute(node.children[2])
522
584
 
523
- # 如果条件为假且没有else分支,则不执行任何操作
585
+ # 如果if条件为假,检查elif分支
586
+ for i in range(2, len(node.children)):
587
+ child = node.children[i]
588
+
589
+ # 如果是ElifClause节点
590
+ if hasattr(child, 'type') and child.type == 'ElifClause':
591
+ elif_condition = self.eval_expression(child.children[0])
592
+ if elif_condition:
593
+ with allure.step(f"执行elif分支 {i-1}"):
594
+ return self.execute(child.children[1])
595
+
596
+ # 如果是普通的statements节点(else分支)
597
+ elif not hasattr(child, 'type') or child.type == 'Statements':
598
+ # 这是else分支,只有在所有前面的条件都为假时才执行
599
+ with allure.step("执行else分支"):
600
+ return self.execute(child)
601
+
602
+ # 如果所有条件都为假且没有else分支,则不执行任何操作
524
603
  return None
525
604
 
526
605
  def _execute_remote_keyword_call(self, node):
@@ -641,7 +720,9 @@ class DSLExecutor:
641
720
  'CustomKeyword': lambda _: None, # 添加对CustomKeyword节点的处理,只需注册不需执行
642
721
  'RemoteImport': self._handle_remote_import,
643
722
  'RemoteKeywordCall': self._execute_remote_keyword_call,
644
- 'AssignmentRemoteKeywordCall': self._handle_assignment_remote_keyword_call
723
+ 'AssignmentRemoteKeywordCall': self._handle_assignment_remote_keyword_call,
724
+ 'Break': self._handle_break,
725
+ 'Continue': self._handle_continue
645
726
  }
646
727
 
647
728
  handler = handlers.get(node.type)
pytest_dsl/core/lexer.py CHANGED
@@ -12,10 +12,13 @@ reserved = {
12
12
  'False': 'FALSE', # 添加布尔值支持
13
13
  'return': 'RETURN', # 添加return关键字支持
14
14
  'else': 'ELSE', # 添加else关键字支持
15
+ 'elif': 'ELIF', # 添加elif关键字支持
15
16
  'if': 'IF', # 添加if关键字支持
16
17
  'as': 'AS', # 添加as关键字支持,用于远程关键字别名
17
18
  'function': 'FUNCTION', # 添加function关键字支持,用于自定义关键字定义
18
- 'teardown': 'TEARDOWN' # 添加teardown关键字支持,用于清理操作
19
+ 'teardown': 'TEARDOWN', # 添加teardown关键字支持,用于清理操作
20
+ 'break': 'BREAK', # 添加break关键字支持,用于循环控制
21
+ 'continue': 'CONTINUE' # 添加continue关键字支持,用于循环控制
19
22
  }
20
23
 
21
24
  # token 名称列表
@@ -29,6 +32,8 @@ tokens = [
29
32
  'RPAREN',
30
33
  'LBRACKET',
31
34
  'RBRACKET',
35
+ 'LBRACE', # 左大括号 {,用于字典字面量
36
+ 'RBRACE', # 右大括号 },用于字典字面量
32
37
  'COLON',
33
38
  'COMMA',
34
39
  'PLACEHOLDER',
@@ -50,6 +55,7 @@ tokens = [
50
55
  'MINUS', # 减法 -
51
56
  'TIMES', # 乘法 *
52
57
  'DIVIDE', # 除法 /
58
+ 'MODULO', # 模运算 %
53
59
  'PIPE', # 管道符 |,用于远程关键字调用
54
60
  ] + list(reserved.values())
55
61
 
@@ -58,6 +64,8 @@ t_LPAREN = r'\('
58
64
  t_RPAREN = r'\)'
59
65
  t_LBRACKET = r'\['
60
66
  t_RBRACKET = r'\]'
67
+ t_LBRACE = r'\{'
68
+ t_RBRACE = r'\}'
61
69
  t_COLON = r':'
62
70
  t_COMMA = r','
63
71
  t_EQUALS = r'='
@@ -71,9 +79,13 @@ t_PLUS = r'\+'
71
79
  t_MINUS = r'-'
72
80
  t_TIMES = r'\*'
73
81
  t_DIVIDE = r'/'
82
+ t_MODULO = r'%'
74
83
 
75
- # 增加PLACEHOLDER规则,匹配 ${变量名} 格式
76
- t_PLACEHOLDER = r'\$\{[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*\}'
84
+ # 增加PLACEHOLDER规则,匹配 ${变量名} 格式,支持点号、数组索引和字典键访问
85
+ # 匹配: ${variable}, ${obj.prop}, ${arr[0]}, ${dict["key"]}, ${obj[0].prop} 等
86
+ t_PLACEHOLDER = (r'\$\{[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*'
87
+ r'(?:(?:\.[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)'
88
+ r'|(?:\[[^\]]+\]))*\}')
77
89
 
78
90
  # 添加管道符的正则表达式定义
79
91
  t_PIPE = r'\|'
@@ -143,8 +155,12 @@ def t_IMPORT_KEYWORD(t):
143
155
 
144
156
 
145
157
  def t_NUMBER(t):
146
- r'\d+'
147
- t.value = int(t.value)
158
+ r'\d+(\.\d+)?'
159
+ # 如果包含小数点,转换为浮点数;否则转换为整数
160
+ if '.' in t.value:
161
+ t.value = float(t.value)
162
+ else:
163
+ t.value = int(t.value)
148
164
  return t
149
165
 
150
166
 
pytest_dsl/core/parser.py CHANGED
@@ -14,7 +14,7 @@ precedence = (
14
14
  ('left', 'COMMA'),
15
15
  ('left', 'GT', 'LT', 'GE', 'LE', 'EQ', 'NE'), # 比较运算符优先级
16
16
  ('left', 'PLUS', 'MINUS'), # 加减运算符优先级
17
- ('left', 'TIMES', 'DIVIDE'), # 乘除运算符优先级
17
+ ('left', 'TIMES', 'DIVIDE', 'MODULO'), # 乘除模运算符优先级
18
18
  ('right', 'EQUALS'),
19
19
  )
20
20
 
@@ -124,7 +124,9 @@ def p_statement(p):
124
124
  | loop
125
125
  | custom_keyword
126
126
  | return_statement
127
- | if_statement'''
127
+ | if_statement
128
+ | break_statement
129
+ | continue_statement'''
128
130
  p[0] = p[1]
129
131
 
130
132
 
@@ -158,6 +160,7 @@ def p_expr_atom(p):
158
160
  | ID
159
161
  | boolean_expr
160
162
  | list_expr
163
+ | dict_expr
161
164
  | LPAREN expression RPAREN'''
162
165
  if p[1] == '(':
163
166
  # 处理括号表达式,直接返回括号内的表达式节点
@@ -197,6 +200,29 @@ def p_list_item(p):
197
200
  p[0] = p[1]
198
201
 
199
202
 
203
+ def p_dict_expr(p):
204
+ '''dict_expr : LBRACE dict_items RBRACE
205
+ | LBRACE RBRACE'''
206
+ if len(p) == 4:
207
+ p[0] = Node('DictExpr', children=p[2])
208
+ else:
209
+ p[0] = Node('DictExpr', children=[]) # 空字典
210
+
211
+
212
+ def p_dict_items(p):
213
+ '''dict_items : dict_item
214
+ | dict_item COMMA dict_items'''
215
+ if len(p) == 2:
216
+ p[0] = [p[1]]
217
+ else:
218
+ p[0] = [p[1]] + p[3]
219
+
220
+
221
+ def p_dict_item(p):
222
+ '''dict_item : expression COLON expression'''
223
+ p[0] = Node('DictItem', children=[p[1], p[3]])
224
+
225
+
200
226
  def p_loop(p):
201
227
  '''loop : FOR ID IN RANGE LPAREN expression COMMA expression RPAREN DO statements END'''
202
228
  p[0] = Node('ForLoop', [p[6], p[8], p[11]], p[2])
@@ -278,13 +304,47 @@ def p_return_statement(p):
278
304
  p[0] = Node('Return', [p[2]])
279
305
 
280
306
 
307
+ def p_break_statement(p):
308
+ '''break_statement : BREAK'''
309
+ p[0] = Node('Break', [])
310
+
311
+
312
+ def p_continue_statement(p):
313
+ '''continue_statement : CONTINUE'''
314
+ p[0] = Node('Continue', [])
315
+
316
+
281
317
  def p_if_statement(p):
282
318
  '''if_statement : IF expression DO statements END
283
- | IF expression DO statements ELSE statements END'''
319
+ | IF expression DO statements elif_clauses END
320
+ | IF expression DO statements ELSE statements END
321
+ | IF expression DO statements elif_clauses ELSE statements END'''
284
322
  if len(p) == 6:
323
+ # if condition do statements end
285
324
  p[0] = Node('IfStatement', [p[2], p[4]], None)
286
- else:
325
+ elif len(p) == 7:
326
+ # if condition do statements elif_clauses end
327
+ p[0] = Node('IfStatement', [p[2], p[4]] + p[5], None)
328
+ elif len(p) == 8:
329
+ # if condition do statements else statements end
287
330
  p[0] = Node('IfStatement', [p[2], p[4], p[6]], None)
331
+ else:
332
+ # if condition do statements elif_clauses else statements end
333
+ p[0] = Node('IfStatement', [p[2], p[4]] + p[5] + [p[7]], None)
334
+
335
+
336
+ def p_elif_clauses(p):
337
+ '''elif_clauses : elif_clause
338
+ | elif_clause elif_clauses'''
339
+ if len(p) == 2:
340
+ p[0] = [p[1]]
341
+ else:
342
+ p[0] = [p[1]] + p[2]
343
+
344
+
345
+ def p_elif_clause(p):
346
+ '''elif_clause : ELIF expression DO statements'''
347
+ p[0] = Node('ElifClause', [p[2], p[4]], None)
288
348
 
289
349
 
290
350
  def p_comparison_expr(p):
@@ -319,7 +379,8 @@ def p_arithmetic_expr(p):
319
379
  '''arithmetic_expr : expression PLUS expression
320
380
  | expression MINUS expression
321
381
  | expression TIMES expression
322
- | expression DIVIDE expression'''
382
+ | expression DIVIDE expression
383
+ | expression MODULO expression'''
323
384
 
324
385
  # 根据规则索引判断使用的是哪个操作符
325
386
  if p.slice[2].type == 'PLUS':
@@ -330,6 +391,8 @@ def p_arithmetic_expr(p):
330
391
  operator = '*'
331
392
  elif p.slice[2].type == 'DIVIDE':
332
393
  operator = '/'
394
+ elif p.slice[2].type == 'MODULO':
395
+ operator = '%'
333
396
  else:
334
397
  print(f"警告: 无法识别的操作符类型 {p.slice[2].type}")
335
398
  operator = None