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.
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):
@@ -169,7 +198,12 @@ def p_expr_atom(p):
169
198
  elif isinstance(p[1], Node):
170
199
  p[0] = p[1]
171
200
  else:
172
- p[0] = Node('Expression', value=p[1])
201
+ # 为基本表达式设置行号信息
202
+ expr_line = getattr(p.slice[1], 'lineno', None)
203
+ expr_node = Node('Expression', value=p[1])
204
+ if expr_line is not None:
205
+ expr_node.set_position(expr_line)
206
+ p[0] = expr_node
173
207
 
174
208
 
175
209
  def p_boolean_expr(p):
@@ -225,17 +259,41 @@ def p_dict_item(p):
225
259
 
226
260
 
227
261
  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])
262
+ '''loop : FOR ID IN RANGE LPAREN expression COMMA expression RPAREN DO statements END''' # noqa: E501
263
+ line_number = getattr(p.slice[1], 'lineno', None)
264
+ p[0] = Node('ForLoop', [p[6], p[8], p[11]], p[2], line_number=line_number)
230
265
 
231
266
 
232
267
  def p_keyword_call(p):
233
268
  '''keyword_call : LBRACKET ID RBRACKET COMMA parameter_list
234
269
  | LBRACKET ID RBRACKET'''
270
+ line_number = getattr(p.slice[1], 'lineno', None)
271
+
235
272
  if len(p) == 6:
236
- p[0] = Node('KeywordCall', [p[5]], p[2])
273
+ # 对于有参数的关键字调用,尝试获取更精确的行号
274
+ # 优先使用关键字名称的行号,其次是左括号的行号
275
+ keyword_line = getattr(p.slice[2], 'lineno', None)
276
+ if keyword_line is not None:
277
+ line_number = keyword_line
278
+
279
+ keyword_node = Node('KeywordCall', [p[5]], p[2],
280
+ line_number=line_number)
281
+
282
+ # 为参数列表中的每个参数也设置行号信息(如果可用)
283
+ if p[5] and isinstance(p[5], list):
284
+ for param in p[5]:
285
+ if (hasattr(param, 'set_position') and
286
+ not hasattr(param, 'line_number')):
287
+ # 如果参数没有行号,使用关键字的行号作为默认值
288
+ param.set_position(line_number)
289
+
290
+ p[0] = keyword_node
237
291
  else:
238
- p[0] = Node('KeywordCall', [[]], p[2])
292
+ # 对于无参数的关键字调用,也优先使用关键字名称的行号
293
+ keyword_line = getattr(p.slice[2], 'lineno', None)
294
+ if keyword_line is not None:
295
+ line_number = keyword_line
296
+ p[0] = Node('KeywordCall', [[]], p[2], line_number=line_number)
239
297
 
240
298
 
241
299
  def p_parameter_list(p):
@@ -254,7 +312,15 @@ def p_parameter_items(p):
254
312
 
255
313
  def p_parameter_item(p):
256
314
  '''parameter_item : ID COLON expression'''
257
- p[0] = Node('ParameterItem', value=p[1], children=[p[3]])
315
+ # 获取参数名的行号
316
+ param_line = getattr(p.slice[1], 'lineno', None)
317
+ param_node = Node('ParameterItem', value=p[1], children=[p[3]])
318
+
319
+ # 设置参数节点的行号
320
+ if param_line is not None:
321
+ param_node.set_position(param_line)
322
+
323
+ p[0] = param_node
258
324
 
259
325
 
260
326
  def p_teardown(p):
@@ -268,7 +334,7 @@ def p_data_source(p):
268
334
 
269
335
 
270
336
  def p_custom_keyword(p):
271
- '''custom_keyword : FUNCTION ID LPAREN param_definitions RPAREN DO statements END'''
337
+ '''custom_keyword : FUNCTION ID LPAREN param_definitions RPAREN DO statements END''' # noqa: E501
272
338
  p[0] = Node('CustomKeyword', [p[4], p[7]], p[2])
273
339
 
274
340
 
@@ -319,7 +385,7 @@ def p_if_statement(p):
319
385
  '''if_statement : IF expression DO statements END
320
386
  | IF expression DO statements elif_clauses END
321
387
  | IF expression DO statements ELSE statements END
322
- | IF expression DO statements elif_clauses ELSE statements END'''
388
+ | IF expression DO statements elif_clauses ELSE statements END''' # noqa: E501
323
389
  if len(p) == 6:
324
390
  # if condition do statements end
325
391
  p[0] = Node('IfStatement', [p[2], p[4]], None)
@@ -401,17 +467,61 @@ def p_arithmetic_expr(p):
401
467
  p[0] = Node('ArithmeticExpr', [p[1], p[3]], operator)
402
468
 
403
469
 
470
+ # 全局变量用于存储解析错误
471
+ _parse_errors = []
472
+
473
+
404
474
  def p_error(p):
475
+ global _parse_errors
405
476
  if p:
406
- print(
407
- f"语法错误: 在第 {p.lineno} 行, 位置 {p.lexpos}, Token {p.type}, 值: {p.value}")
477
+ error_msg = (f"语法错误: 在第 {p.lineno} 行, 位置 {p.lexpos}, "
478
+ f"Token {p.type}, 值: {p.value}")
479
+ _parse_errors.append({
480
+ 'message': error_msg,
481
+ 'line': p.lineno,
482
+ 'position': p.lexpos,
483
+ 'token_type': p.type,
484
+ 'token_value': p.value
485
+ })
486
+ # 不再直接打印,而是存储错误信息
408
487
  else:
409
- print("语法错误: 在文件末尾")
488
+ error_msg = "语法错误: 在文件末尾"
489
+ _parse_errors.append({
490
+ 'message': error_msg,
491
+ 'line': None,
492
+ 'position': None,
493
+ 'token_type': None,
494
+ 'token_value': None
495
+ })
410
496
 
411
497
 
412
498
  def get_parser(debug=False):
413
499
  return yacc.yacc(debug=debug)
414
500
 
501
+
502
+ def parse_with_error_handling(content, lexer=None):
503
+ """带错误处理的解析函数
504
+
505
+ Args:
506
+ content: DSL内容
507
+ lexer: 词法分析器实例
508
+
509
+ Returns:
510
+ tuple: (AST节点, 错误列表)
511
+ """
512
+ global _parse_errors
513
+ _parse_errors = [] # 清空之前的错误
514
+
515
+ if lexer is None:
516
+ from pytest_dsl.core.lexer import get_lexer
517
+ lexer = get_lexer()
518
+
519
+ parser = get_parser()
520
+ ast = parser.parse(content, lexer=lexer)
521
+
522
+ # 返回AST和错误列表
523
+ return ast, _parse_errors.copy()
524
+
415
525
  # 定义远程关键字调用的语法规则
416
526
 
417
527
 
@@ -0,0 +1,417 @@
1
+ """
2
+ pytest-dsl DSL格式校验模块
3
+
4
+ 提供DSL语法验证、语义验证、关键字验证等功能
5
+ """
6
+
7
+ import re
8
+ from typing import List, Dict, Optional, Tuple
9
+ from pytest_dsl.core.lexer import get_lexer
10
+ from pytest_dsl.core.parser import get_parser, Node, parse_with_error_handling
11
+ from pytest_dsl.core.keyword_manager import keyword_manager
12
+
13
+
14
+ class DSLValidationError:
15
+ """DSL验证错误"""
16
+
17
+ def __init__(
18
+ self,
19
+ error_type: str,
20
+ message: str,
21
+ line: Optional[int] = None,
22
+ column: Optional[int] = None,
23
+ suggestion: Optional[str] = None):
24
+ self.error_type = error_type
25
+ self.message = message
26
+ self.line = line
27
+ self.column = column
28
+ self.suggestion = suggestion
29
+
30
+ def __str__(self):
31
+ location = ""
32
+ if self.line is not None:
33
+ location = f"第{self.line}行"
34
+ if self.column is not None:
35
+ location += f"第{self.column}列"
36
+ location += ": "
37
+
38
+ result = f"{location}{self.error_type}: {self.message}"
39
+ if self.suggestion:
40
+ result += f"\n建议: {self.suggestion}"
41
+ return result
42
+
43
+
44
+ class DSLValidator:
45
+ """DSL格式校验器"""
46
+
47
+ def __init__(self):
48
+ self.errors: List[DSLValidationError] = []
49
+ self.warnings: List[DSLValidationError] = []
50
+
51
+ def validate(self, content: str, dsl_id: Optional[str] = None
52
+ ) -> Tuple[bool, List[DSLValidationError]]:
53
+ """验证DSL内容
54
+
55
+ Args:
56
+ content: DSL内容
57
+ dsl_id: DSL标识符(可选)
58
+
59
+ Returns:
60
+ (是否验证通过, 错误列表)
61
+ """
62
+ self.errors = []
63
+ self.warnings = []
64
+
65
+ # 基础验证
66
+ self._validate_basic_format(content)
67
+
68
+ # 语法验证
69
+ ast = self._validate_syntax(content)
70
+
71
+ # 如果语法验证通过,进行语义验证
72
+ if ast and not self.errors:
73
+ self._validate_semantics(ast)
74
+
75
+ # 元数据验证
76
+ if ast and not self.errors:
77
+ self._validate_metadata(ast)
78
+
79
+ # 关键字验证
80
+ if ast and not self.errors:
81
+ self._validate_keywords(ast)
82
+
83
+ return len(self.errors) == 0, self.errors + self.warnings
84
+
85
+ def _validate_basic_format(self, content: str) -> None:
86
+ """基础格式验证"""
87
+ if not content or not content.strip():
88
+ self.errors.append(DSLValidationError(
89
+ "格式错误", "DSL内容不能为空"
90
+ ))
91
+ return
92
+
93
+ lines = content.split('\n')
94
+
95
+ # 检查编码
96
+ try:
97
+ content.encode('utf-8')
98
+ except UnicodeEncodeError as e:
99
+ self.errors.append(DSLValidationError(
100
+ "编码错误", f"DSL内容包含无效字符: {str(e)}"
101
+ ))
102
+
103
+ # 检查行长度
104
+ for i, line in enumerate(lines, 1):
105
+ if len(line) > 1000:
106
+ self.warnings.append(DSLValidationError(
107
+ "格式警告", f"第{i}行过长,建议控制在1000字符以内", line=i
108
+ ))
109
+
110
+ # 检查嵌套层级
111
+ max_indent = 0
112
+ for i, line in enumerate(lines, 1):
113
+ if line.strip():
114
+ indent = len(line) - len(line.lstrip())
115
+ if indent > max_indent:
116
+ max_indent = indent
117
+
118
+ if max_indent > 40: # 假设每层缩进4个空格,最多10层
119
+ self.warnings.append(DSLValidationError(
120
+ "格式警告", f"嵌套层级过深({max_indent//4}层),建议简化结构"
121
+ ))
122
+
123
+ def _validate_syntax(self, content: str) -> Optional[Node]:
124
+ """语法验证"""
125
+ try:
126
+ lexer = get_lexer()
127
+ ast, parse_errors = parse_with_error_handling(content, lexer)
128
+
129
+ # 如果有解析错误,添加到错误列表
130
+ if parse_errors:
131
+ for error in parse_errors:
132
+ self.errors.append(DSLValidationError(
133
+ "语法错误",
134
+ error['message'],
135
+ line=error['line'],
136
+ suggestion=self._suggest_syntax_fix(error['message'])
137
+ ))
138
+ return None
139
+
140
+ return ast
141
+
142
+ except Exception as e:
143
+ error_msg = str(e)
144
+ line_num = self._extract_line_number(error_msg)
145
+
146
+ self.errors.append(DSLValidationError(
147
+ "语法错误",
148
+ error_msg,
149
+ line=line_num,
150
+ suggestion=self._suggest_syntax_fix(error_msg)
151
+ ))
152
+ return None
153
+
154
+ def _validate_semantics(self, ast: Node) -> None:
155
+ """语义验证"""
156
+ self._check_node_semantics(ast)
157
+
158
+ def _check_node_semantics(self, node: Node) -> None:
159
+ """检查节点语义"""
160
+ if node.type == 'Assignment':
161
+ # 检查变量名
162
+ var_name = node.value
163
+ if not self._is_valid_variable_name(var_name):
164
+ self.errors.append(DSLValidationError(
165
+ "语义错误",
166
+ f"无效的变量名: {var_name}",
167
+ suggestion="变量名应以字母或下划线开头,只包含字母、数字、下划线或中文字符"
168
+ ))
169
+
170
+ elif node.type == 'ForLoop':
171
+ # 检查循环变量名
172
+ loop_var = node.value
173
+ if not self._is_valid_variable_name(loop_var):
174
+ self.errors.append(DSLValidationError(
175
+ "语义错误",
176
+ f"无效的循环变量名: {loop_var}",
177
+ suggestion="循环变量名应以字母或下划线开头,只包含字母、数字、下划线或中文字符"
178
+ ))
179
+
180
+ elif node.type == 'Expression':
181
+ # 检查表达式中的变量引用
182
+ if isinstance(node.value, str):
183
+ self._validate_variable_references(node.value)
184
+
185
+ # 递归检查子节点
186
+ for child in node.children:
187
+ if isinstance(child, Node):
188
+ self._check_node_semantics(child)
189
+
190
+ def _validate_metadata(self, ast: Node) -> None:
191
+ """验证元数据"""
192
+ metadata_node = None
193
+ for child in ast.children:
194
+ if child.type == 'Metadata':
195
+ metadata_node = child
196
+ break
197
+
198
+ if not metadata_node:
199
+ self.warnings.append(DSLValidationError(
200
+ "元数据警告", "建议添加@name元数据以描述测试用例名称"
201
+ ))
202
+ return
203
+
204
+ has_name = False
205
+ has_description = False
206
+
207
+ for item in metadata_node.children:
208
+ if item.type == '@name':
209
+ has_name = True
210
+ if not item.value or not item.value.strip():
211
+ self.errors.append(DSLValidationError(
212
+ "元数据错误", "@name不能为空"
213
+ ))
214
+ elif item.type == '@description':
215
+ has_description = True
216
+ if not item.value or not item.value.strip():
217
+ self.warnings.append(DSLValidationError(
218
+ "元数据警告", "@description不应为空"
219
+ ))
220
+ elif item.type == '@tags':
221
+ # 验证标签格式
222
+ if not item.value or len(item.value) == 0:
223
+ self.warnings.append(DSLValidationError(
224
+ "元数据警告", "@tags不应为空列表"
225
+ ))
226
+
227
+ if not has_name:
228
+ self.warnings.append(DSLValidationError(
229
+ "元数据警告", "建议添加@name元数据以描述测试用例名称"
230
+ ))
231
+
232
+ if not has_description:
233
+ self.warnings.append(DSLValidationError(
234
+ "元数据警告", "建议添加@description元数据以描述测试用例功能"
235
+ ))
236
+
237
+ def _validate_keywords(self, ast: Node) -> None:
238
+ """验证关键字"""
239
+ self._check_node_keywords(ast)
240
+
241
+ def _check_node_keywords(self, node: Node) -> None:
242
+ """检查节点中的关键字"""
243
+ if node.type == 'KeywordCall':
244
+ keyword_name = node.value
245
+ keyword_info = keyword_manager.get_keyword_info(keyword_name)
246
+
247
+ if not keyword_info:
248
+ self.errors.append(DSLValidationError(
249
+ "关键字错误",
250
+ f"未注册的关键字: {keyword_name}",
251
+ suggestion=self._suggest_similar_keyword(keyword_name)
252
+ ))
253
+ else:
254
+ # 验证参数
255
+ self._validate_keyword_parameters(node, keyword_info)
256
+
257
+ # 递归检查子节点
258
+ for child in node.children:
259
+ if isinstance(child, Node):
260
+ self._check_node_keywords(child)
261
+
262
+ def _validate_keyword_parameters(self, keyword_node: Node,
263
+ keyword_info: Dict) -> None:
264
+ """验证关键字参数"""
265
+ if not keyword_node.children or not keyword_node.children[0]:
266
+ return
267
+
268
+ provided_params = set()
269
+ for param in keyword_node.children[0]:
270
+ param_name = param.value
271
+ provided_params.add(param_name)
272
+
273
+ # 检查参数名是否有效
274
+ mapping = keyword_info.get('mapping', {})
275
+ if param_name not in mapping:
276
+ self.errors.append(DSLValidationError(
277
+ "参数错误",
278
+ f"关键字 {keyword_node.value} 不支持参数: {param_name}",
279
+ suggestion=f"支持的参数: {', '.join(mapping.keys())}"
280
+ ))
281
+
282
+ # 检查必需参数(这里简化处理,实际可能需要更复杂的逻辑)
283
+ required_params = set()
284
+ parameters = keyword_info.get('parameters', [])
285
+ for param in parameters:
286
+ if not hasattr(param, 'default') or param.default is None:
287
+ required_params.add(param.name)
288
+
289
+ missing_params = required_params - provided_params
290
+ if missing_params:
291
+ self.warnings.append(DSLValidationError(
292
+ "参数警告",
293
+ f"关键字 {keyword_node.value} 缺少建议参数: "
294
+ f"{', '.join(missing_params)}"
295
+ ))
296
+
297
+ def _is_valid_variable_name(self, name: str) -> bool:
298
+ """检查变量名是否有效"""
299
+ if not name:
300
+ return False
301
+ # 支持中文、英文、数字、下划线,以字母或下划线开头
302
+ pattern = r'^[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*$'
303
+ return bool(re.match(pattern, name))
304
+
305
+ def _validate_variable_references(self, text: str) -> None:
306
+ """验证文本中的变量引用"""
307
+ # 匹配 ${变量名} 格式
308
+ pattern = r'\$\{([^}]+)\}'
309
+ matches = re.findall(pattern, text)
310
+
311
+ for var_ref in matches:
312
+ # 检查变量引用格式是否正确
313
+ if not self._is_valid_variable_reference(var_ref):
314
+ self.errors.append(DSLValidationError(
315
+ "变量引用错误",
316
+ f"无效的变量引用格式: ${{{var_ref}}}",
317
+ suggestion="变量引用应为 ${变量名} 格式,支持点号访问和数组索引"
318
+ ))
319
+
320
+ def _is_valid_variable_reference(self, var_ref: str) -> bool:
321
+ """检查变量引用是否有效"""
322
+ # 支持: variable, obj.prop, arr[0], dict["key"] 等格式
323
+ pattern = (r'^[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*'
324
+ r'(?:(?:\.[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)'
325
+ r'|(?:\[[^\]]+\]))*$')
326
+ return bool(re.match(pattern, var_ref))
327
+
328
+ def _extract_line_number(self, error_msg: str) -> Optional[int]:
329
+ """从错误消息中提取行号"""
330
+ # 尝试匹配常见的行号模式
331
+ patterns = [
332
+ r'line (\d+)',
333
+ r'第(\d+)行',
334
+ r'在行 (\d+)',
335
+ r'at line (\d+)'
336
+ ]
337
+
338
+ for pattern in patterns:
339
+ match = re.search(pattern, error_msg)
340
+ if match:
341
+ return int(match.group(1))
342
+ return None
343
+
344
+ def _suggest_syntax_fix(self, error_msg: str) -> Optional[str]:
345
+ """根据错误消息建议语法修复"""
346
+ suggestions = {
347
+ "Syntax error": "检查语法是否正确,特别是括号、引号的匹配",
348
+ "unexpected token": "检查是否有多余或缺失的符号",
349
+ "Unexpected end of input": "检查是否缺少end关键字或右括号",
350
+ "illegal character": "检查是否有非法字符,确保使用UTF-8编码"
351
+ }
352
+
353
+ for key, suggestion in suggestions.items():
354
+ if key.lower() in error_msg.lower():
355
+ return suggestion
356
+ return None
357
+
358
+ def _suggest_similar_keyword(self, keyword_name: str) -> Optional[str]:
359
+ """建议相似的关键字"""
360
+ all_keywords = list(keyword_manager._keywords.keys())
361
+
362
+ # 简单的相似度匹配(可以使用更复杂的算法)
363
+ similar_keywords = []
364
+ for kw in all_keywords:
365
+ similarity = self._calculate_similarity(
366
+ keyword_name.lower(), kw.lower())
367
+ if similarity > 0.6:
368
+ similar_keywords.append(kw)
369
+
370
+ if similar_keywords:
371
+ return f"您是否想使用: {', '.join(similar_keywords[:3])}"
372
+ return None
373
+
374
+ def _calculate_similarity(self, s1: str, s2: str) -> float:
375
+ """计算字符串相似度(简单的Jaccard相似度)"""
376
+ if not s1 or not s2:
377
+ return 0.0
378
+
379
+ set1 = set(s1)
380
+ set2 = set(s2)
381
+ intersection = len(set1.intersection(set2))
382
+ union = len(set1.union(set2))
383
+
384
+ return intersection / union if union > 0 else 0.0
385
+
386
+
387
+ def validate_dsl(content: str, dsl_id: Optional[str] = None
388
+ ) -> Tuple[bool, List[DSLValidationError]]:
389
+ """验证DSL内容的便捷函数
390
+
391
+ Args:
392
+ content: DSL内容
393
+ dsl_id: DSL标识符(可选)
394
+
395
+ Returns:
396
+ (是否验证通过, 错误列表)
397
+ """
398
+ validator = DSLValidator()
399
+ return validator.validate(content, dsl_id)
400
+
401
+
402
+ def check_dsl_syntax(content: str) -> bool:
403
+ """快速检查DSL语法是否正确
404
+
405
+ Args:
406
+ content: DSL内容
407
+
408
+ Returns:
409
+ 语法是否正确
410
+ """
411
+ try:
412
+ lexer = get_lexer()
413
+ parser = get_parser()
414
+ parser.parse(content, lexer=lexer)
415
+ return True
416
+ except Exception:
417
+ return False
pytest_dsl/plugin.py CHANGED
@@ -38,6 +38,13 @@ def pytest_configure(config):
38
38
  # 确保全局变量存储目录存在
39
39
  os.makedirs(global_context._storage_dir, exist_ok=True)
40
40
 
41
+ # 首先导入内置关键字模块,确保内置关键字被注册
42
+ try:
43
+ import pytest_dsl.keywords # noqa: F401
44
+ print("pytest环境:内置关键字模块加载完成")
45
+ except ImportError as e:
46
+ print(f"pytest环境:加载内置关键字模块失败: {e}")
47
+
41
48
  # 加载所有已安装的关键字插件
42
49
  load_all_plugins()
43
50
 
@@ -46,7 +53,9 @@ def pytest_configure(config):
46
53
 
47
54
  # 自动导入项目中的resources目录
48
55
  try:
49
- from pytest_dsl.core.custom_keyword_manager import custom_keyword_manager
56
+ from pytest_dsl.core.custom_keyword_manager import (
57
+ custom_keyword_manager
58
+ )
50
59
 
51
60
  # 获取pytest的根目录
52
61
  project_root = str(config.rootdir) if config.rootdir else os.getcwd()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-dsl
3
- Version: 0.14.0
3
+ Version: 0.15.1
4
4
  Summary: A DSL testing framework based on pytest
5
5
  Author: Chen Shuanglin
6
6
  License: MIT