pytest-dsl 0.6.0__py3-none-any.whl → 0.7.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.
@@ -8,44 +8,44 @@ from pytest_dsl.core.yaml_vars import yaml_vars
8
8
 
9
9
  class VariableReplacer:
10
10
  """统一的变量替换工具类
11
-
11
+
12
12
  提供统一的变量替换功能,支持字符串、字典和列表中的变量替换。
13
13
  变量查找优先级:本地变量 > 测试上下文 > YAML变量 > 全局上下文
14
14
  """
15
-
15
+
16
16
  def __init__(self, local_variables: Dict[str, Any] = None, test_context: TestContext = None):
17
17
  """初始化变量替换器
18
-
18
+
19
19
  Args:
20
20
  local_variables: 本地变量字典
21
21
  test_context: 测试上下文对象
22
22
  """
23
23
  self.local_variables = local_variables or {}
24
24
  self._test_context = test_context or TestContext()
25
-
25
+
26
26
  @property
27
27
  def test_context(self) -> TestContext:
28
28
  """获取测试上下文,确保始终使用最新的上下文对象
29
-
29
+
30
30
  如果上下文对象中包含executor属性,则使用executor的上下文
31
31
  (这确保即使上下文被替换也能获取正确的引用)
32
-
32
+
33
33
  Returns:
34
34
  测试上下文对象
35
35
  """
36
36
  if hasattr(self._test_context, 'executor') and self._test_context.executor is not None:
37
37
  return self._test_context.executor.test_context
38
38
  return self._test_context
39
-
39
+
40
40
  def get_variable(self, var_name: str) -> Any:
41
41
  """获取变量值,按照优先级查找
42
-
42
+
43
43
  Args:
44
44
  var_name: 变量名
45
-
45
+
46
46
  Returns:
47
47
  变量值,如果变量不存在则抛出 KeyError
48
-
48
+
49
49
  Raises:
50
50
  KeyError: 当变量不存在时
51
51
  """
@@ -53,31 +53,31 @@ class VariableReplacer:
53
53
  if var_name in self.local_variables:
54
54
  value = self.local_variables[var_name]
55
55
  return self._convert_value(value)
56
-
56
+
57
57
  # 从测试上下文中获取
58
58
  if self.test_context.has(var_name):
59
59
  value = self.test_context.get(var_name)
60
60
  return self._convert_value(value)
61
-
61
+
62
62
  # 从YAML变量中获取
63
63
  yaml_value = yaml_vars.get_variable(var_name)
64
64
  if yaml_value is not None:
65
65
  return self._convert_value(yaml_value)
66
-
66
+
67
67
  # 从全局上下文获取
68
68
  if global_context.has_variable(var_name):
69
69
  value = global_context.get_variable(var_name)
70
70
  return self._convert_value(value)
71
-
71
+
72
72
  # 如果变量不存在,抛出异常
73
73
  raise KeyError(f"变量 '{var_name}' 不存在")
74
-
74
+
75
75
  def _convert_value(self, value: Any) -> Any:
76
76
  """转换值为正确的类型
77
-
77
+
78
78
  Args:
79
79
  value: 要转换的值
80
-
80
+
81
81
  Returns:
82
82
  转换后的值
83
83
  """
@@ -93,68 +93,213 @@ class VariableReplacer:
93
93
  except (ValueError, TypeError):
94
94
  pass
95
95
  return value
96
-
96
+
97
97
  def replace_in_string(self, value: str) -> str:
98
98
  """替换字符串中的变量引用
99
-
99
+
100
+ 支持多种访问语法:
101
+ - 基本变量: ${variable}
102
+ - 点号访问: ${object.property}
103
+ - 数组索引: ${array[0]}, ${array[-1]}
104
+ - 字典键访问: ${dict["key"]}, ${dict['key']}
105
+ - 混合访问: ${users[0].name}, ${data["users"][0]["name"]}
106
+
100
107
  Args:
101
108
  value: 包含变量引用的字符串
102
-
109
+
103
110
  Returns:
104
111
  替换后的字符串
105
-
112
+
106
113
  Raises:
107
114
  KeyError: 当变量不存在时
108
115
  """
109
116
  if not isinstance(value, str) or '${' not in value:
110
117
  return value
111
-
112
- # 处理变量引用模式: ${variable} 或 ${variable.field.subfield}
113
- pattern = r'\$\{([a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*(?:\.[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)*)\}'
114
-
118
+
119
+ # 扩展的变量引用模式,支持数组索引和字典键访问
120
+ # 匹配: ${variable}, ${obj.prop}, ${arr[0]}, ${dict["key"]}, ${obj[0].prop}
121
+ pattern = r'\$\{([a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*(?:(?:\.[a-zA-Z_\u4e00-\u9fa5][a-zA-Z0-9_\u4e00-\u9fa5]*)|(?:\[[^\]]+\]))*)\}'
122
+
115
123
  result = value
116
124
  matches = list(re.finditer(pattern, result))
117
-
125
+
118
126
  # 从后向前替换,避免位置偏移
119
127
  for match in reversed(matches):
120
- var_ref = match.group(1) # 例如: "api_test_data.user_id" 或 "variable"
121
- parts = var_ref.split('.')
122
-
123
- # 获取根变量
124
- root_var_name = parts[0]
128
+ var_ref = match.group(1) # 例如: "users[0].name" 或 "data['key']"
129
+
125
130
  try:
126
- root_var = self.get_variable(root_var_name)
127
- except KeyError:
128
- raise KeyError(f"变量 '{root_var_name}' 不存在")
129
-
130
- # 递归访问嵌套属性
131
- var_value = root_var
132
- for part in parts[1:]:
133
- if isinstance(var_value, dict) and part in var_value:
134
- var_value = var_value[part]
135
- else:
136
- raise KeyError(f"无法访问属性 '{part}',变量 '{root_var_name}' 的类型是 {type(var_value).__name__}")
137
-
138
- # 替换变量引用
139
- result = result[:match.start()] + str(var_value) + result[match.end():]
140
-
131
+ var_value = self._parse_variable_path(var_ref)
132
+ # 替换变量引用
133
+ result = result[:match.start()] + str(var_value) + result[match.end():]
134
+ except (KeyError, IndexError, TypeError) as e:
135
+ raise KeyError(f"无法解析变量引用 '${{{var_ref}}}': {str(e)}")
136
+
141
137
  return result
142
-
138
+
139
+ def _parse_variable_path(self, var_ref: str):
140
+ """解析复杂的变量访问路径
141
+
142
+ 支持的语法:
143
+ - variable
144
+ - object.property
145
+ - array[0]
146
+ - dict["key"]
147
+ - users[0].name
148
+ - data["users"][0]["name"]
149
+
150
+ Args:
151
+ var_ref: 变量引用路径,如 "users[0].name"
152
+
153
+ Returns:
154
+ 解析后的变量值
155
+
156
+ Raises:
157
+ KeyError: 当变量不存在时
158
+ IndexError: 当数组索引超出范围时
159
+ TypeError: 当类型不匹配时
160
+ """
161
+ # 解析访问路径
162
+ path_parts = self._tokenize_path(var_ref)
163
+
164
+ # 获取根变量
165
+ root_var_name = path_parts[0]
166
+ try:
167
+ current_value = self.get_variable(root_var_name)
168
+ except KeyError:
169
+ raise KeyError(f"变量 '{root_var_name}' 不存在")
170
+
171
+ # 逐步访问路径
172
+ for part in path_parts[1:]:
173
+ current_value = self._access_value(current_value, part, root_var_name)
174
+
175
+ return current_value
176
+
177
+ def _tokenize_path(self, var_ref: str) -> list:
178
+ """将变量路径分解为访问令牌
179
+
180
+ 例如:
181
+ - "users[0].name" -> ["users", "[0]", "name"]
182
+ - "data['key'].items[1]" -> ["data", "['key']", "items", "[1]"]
183
+
184
+ Args:
185
+ var_ref: 变量引用路径
186
+
187
+ Returns:
188
+ 访问令牌列表
189
+ """
190
+ tokens = []
191
+ current_token = ""
192
+ i = 0
193
+
194
+ while i < len(var_ref):
195
+ char = var_ref[i]
196
+
197
+ if char == '.':
198
+ if current_token:
199
+ tokens.append(current_token)
200
+ current_token = ""
201
+ elif char == '[':
202
+ if current_token:
203
+ tokens.append(current_token)
204
+ current_token = ""
205
+
206
+ # 找到匹配的右括号
207
+ bracket_count = 1
208
+ bracket_content = "["
209
+ i += 1
210
+
211
+ while i < len(var_ref) and bracket_count > 0:
212
+ char = var_ref[i]
213
+ bracket_content += char
214
+ if char == '[':
215
+ bracket_count += 1
216
+ elif char == ']':
217
+ bracket_count -= 1
218
+ i += 1
219
+
220
+ tokens.append(bracket_content)
221
+ i -= 1 # 回退一位,因为外层循环会自增
222
+ else:
223
+ current_token += char
224
+
225
+ i += 1
226
+
227
+ if current_token:
228
+ tokens.append(current_token)
229
+
230
+ return tokens
231
+
232
+ def _access_value(self, current_value, access_token: str, root_var_name: str):
233
+ """根据访问令牌获取值
234
+
235
+ Args:
236
+ current_value: 当前值
237
+ access_token: 访问令牌,如 "name", "[0]", "['key']"
238
+ root_var_name: 根变量名(用于错误信息)
239
+
240
+ Returns:
241
+ 访问后的值
242
+
243
+ Raises:
244
+ KeyError: 当键不存在时
245
+ IndexError: 当索引超出范围时
246
+ TypeError: 当类型不匹配时
247
+ """
248
+ if access_token.startswith('[') and access_token.endswith(']'):
249
+ # 数组索引或字典键访问
250
+ key_content = access_token[1:-1].strip()
251
+
252
+ # 处理字符串键(带引号)
253
+ if (key_content.startswith('"') and key_content.endswith('"')) or \
254
+ (key_content.startswith("'") and key_content.endswith("'")):
255
+ key = key_content[1:-1] # 去掉引号
256
+ if isinstance(current_value, dict):
257
+ if key not in current_value:
258
+ raise KeyError(f"字典中不存在键 '{key}'")
259
+ return current_value[key]
260
+ else:
261
+ raise TypeError(f"无法在 {type(current_value).__name__} 类型上使用字符串键访问")
262
+
263
+ # 处理数字索引
264
+ try:
265
+ index = int(key_content)
266
+ if isinstance(current_value, (list, tuple)):
267
+ if index >= len(current_value) or index < -len(current_value):
268
+ raise IndexError(f"数组索引 {index} 超出范围,数组长度为 {len(current_value)}")
269
+ return current_value[index]
270
+ elif isinstance(current_value, dict):
271
+ # 字典也可以用数字键
272
+ str_key = str(index)
273
+ if str_key not in current_value and index not in current_value:
274
+ raise KeyError(f"字典中不存在键 '{index}' 或 '{str_key}'")
275
+ return current_value.get(index, current_value.get(str_key))
276
+ else:
277
+ raise TypeError(f"无法在 {type(current_value).__name__} 类型上使用索引访问")
278
+ except ValueError:
279
+ raise ValueError(f"无效的索引格式: '{key_content}'")
280
+
281
+ else:
282
+ # 属性访问(点号语法)
283
+ if isinstance(current_value, dict) and access_token in current_value:
284
+ return current_value[access_token]
285
+ else:
286
+ raise KeyError(f"无法访问属性 '{access_token}',当前值类型是 {type(current_value).__name__}")
287
+
143
288
  def replace_in_dict(self, data: Dict[str, Any]) -> Dict[str, Any]:
144
289
  """递归替换字典中的变量引用
145
-
290
+
146
291
  Args:
147
292
  data: 包含变量引用的字典
148
-
293
+
149
294
  Returns:
150
295
  替换后的字典
151
-
296
+
152
297
  Raises:
153
298
  KeyError: 当变量不存在时
154
299
  """
155
300
  if not isinstance(data, dict):
156
301
  return data
157
-
302
+
158
303
  result = {}
159
304
  for key, value in data.items():
160
305
  # 替换键中的变量
@@ -162,35 +307,35 @@ class VariableReplacer:
162
307
  # 替换值中的变量
163
308
  new_value = self.replace_in_value(value)
164
309
  result[new_key] = new_value
165
-
310
+
166
311
  return result
167
-
312
+
168
313
  def replace_in_list(self, data: List[Any]) -> List[Any]:
169
314
  """递归替换列表中的变量引用
170
-
315
+
171
316
  Args:
172
317
  data: 包含变量引用的列表
173
-
318
+
174
319
  Returns:
175
320
  替换后的列表
176
-
321
+
177
322
  Raises:
178
323
  KeyError: 当变量不存在时
179
324
  """
180
325
  if not isinstance(data, list):
181
326
  return data
182
-
327
+
183
328
  return [self.replace_in_value(item) for item in data]
184
-
329
+
185
330
  def replace_in_value(self, value: Any) -> Any:
186
331
  """递归替换任意值中的变量引用
187
-
332
+
188
333
  Args:
189
334
  value: 任意值,可能是字符串、字典、列表等
190
-
335
+
191
336
  Returns:
192
337
  替换后的值
193
-
338
+
194
339
  Raises:
195
340
  KeyError: 当变量不存在时
196
341
  """
@@ -217,16 +362,16 @@ class VariableReplacer:
217
362
  return value
218
363
  except:
219
364
  return value
220
-
365
+
221
366
  def replace_in_json(self, json_str: str) -> str:
222
367
  """替换JSON字符串中的变量引用
223
-
368
+
224
369
  Args:
225
370
  json_str: 包含变量引用的JSON字符串
226
-
371
+
227
372
  Returns:
228
373
  替换后的JSON字符串
229
-
374
+
230
375
  Raises:
231
376
  KeyError: 当变量不存在时
232
377
  json.JSONDecodeError: 当JSON解析失败时
@@ -241,16 +386,16 @@ class VariableReplacer:
241
386
  except json.JSONDecodeError:
242
387
  # 如果JSON解析失败,直接作为字符串处理
243
388
  return self.replace_in_string(json_str)
244
-
389
+
245
390
  def replace_in_yaml(self, yaml_str: str) -> str:
246
391
  """替换YAML字符串中的变量引用
247
-
392
+
248
393
  Args:
249
394
  yaml_str: 包含变量引用的YAML字符串
250
-
395
+
251
396
  Returns:
252
397
  替换后的YAML字符串
253
-
398
+
254
399
  Raises:
255
400
  KeyError: 当变量不存在时
256
401
  """
@@ -264,4 +409,4 @@ class VariableReplacer:
264
409
  return yaml.dump(replaced_data, allow_unicode=True)
265
410
  except:
266
411
  # 如果YAML解析失败,直接作为字符串处理
267
- return self.replace_in_string(yaml_str)
412
+ return self.replace_in_string(yaml_str)