schemathesis 3.39.7__py3-none-any.whl → 3.39.8__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.
- schemathesis/_hypothesis.py +13 -10
- schemathesis/generation/coverage.py +4 -4
- schemathesis/specs/openapi/patterns.py +170 -2
- {schemathesis-3.39.7.dist-info → schemathesis-3.39.8.dist-info}/METADATA +1 -1
- {schemathesis-3.39.7.dist-info → schemathesis-3.39.8.dist-info}/RECORD +8 -8
- {schemathesis-3.39.7.dist-info → schemathesis-3.39.8.dist-info}/WHEEL +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-3.39.8.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-3.39.8.dist-info}/licenses/LICENSE +0 -0
schemathesis/_hypothesis.py
CHANGED
@@ -332,16 +332,19 @@ def _iter_coverage_cases(
|
|
332
332
|
if operation.query:
|
333
333
|
container = template["query"]
|
334
334
|
for parameter in operation.query:
|
335
|
-
value
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
335
|
+
# Could be absent if value schema can't be negated
|
336
|
+
# I.e. contains just `default` value without any other keywords
|
337
|
+
value = container.get(parameter.name, NOT_SET)
|
338
|
+
if value is not NOT_SET:
|
339
|
+
case = operation.make_case(**{**template, "query": {**container, parameter.name: [value, value]}})
|
340
|
+
case.data_generation_method = DataGenerationMethod.negative
|
341
|
+
case.meta = _make_meta(
|
342
|
+
description=f"Duplicate `{parameter.name}` query parameter",
|
343
|
+
location=None,
|
344
|
+
parameter=parameter.name,
|
345
|
+
parameter_location="query",
|
346
|
+
)
|
347
|
+
yield case
|
345
348
|
# Generate missing required parameters
|
346
349
|
for parameter in operation.iter_parameters():
|
347
350
|
if parameter.is_required and parameter.location != "path":
|
@@ -194,6 +194,10 @@ class CoverageContext:
|
|
194
194
|
re.compile(pattern)
|
195
195
|
except re.error:
|
196
196
|
raise Unsatisfiable from None
|
197
|
+
if "minLength" in schema or "maxLength" in schema:
|
198
|
+
min_length = schema.get("minLength")
|
199
|
+
max_length = schema.get("maxLength")
|
200
|
+
pattern = update_quantifier(pattern, min_length, max_length)
|
197
201
|
return cached_draw(st.from_regex(pattern))
|
198
202
|
if (keys == ["items", "type"] or keys == ["items", "minItems", "type"]) and isinstance(schema["items"], dict):
|
199
203
|
items = schema["items"]
|
@@ -514,11 +518,7 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
514
518
|
# Default positive value
|
515
519
|
yield PositiveValue(ctx.generate_from_schema(schema), description="Valid string")
|
516
520
|
elif "pattern" in schema:
|
517
|
-
# Without merging `maxLength` & `minLength` into a regex it is problematic
|
518
|
-
# to generate a valid value as the unredlying machinery will resort to filtering
|
519
|
-
# and it is unlikely that it will generate a string of that length
|
520
521
|
yield PositiveValue(ctx.generate_from_schema(schema), description="Valid string")
|
521
|
-
return
|
522
522
|
|
523
523
|
seen = set()
|
524
524
|
|
@@ -66,9 +66,177 @@ def _handle_parsed_pattern(parsed: list, pattern: str, min_length: int | None, m
|
|
66
66
|
)
|
67
67
|
+ trailing_anchor
|
68
68
|
)
|
69
|
+
elif (
|
70
|
+
len(parsed) > 3
|
71
|
+
and parsed[0][0] == ANCHOR
|
72
|
+
and parsed[-1][0] == ANCHOR
|
73
|
+
and all(op == LITERAL or op in REPEATS for op, _ in parsed[1:-1])
|
74
|
+
):
|
75
|
+
return _handle_anchored_pattern(parsed, pattern, min_length, max_length)
|
69
76
|
return pattern
|
70
77
|
|
71
78
|
|
79
|
+
def _handle_anchored_pattern(parsed: list, pattern: str, min_length: int | None, max_length: int | None) -> str:
|
80
|
+
"""Update regex pattern with multiple quantified patterns to satisfy length constraints."""
|
81
|
+
# Extract anchors
|
82
|
+
leading_anchor_length = _get_anchor_length(parsed[0][1])
|
83
|
+
trailing_anchor_length = _get_anchor_length(parsed[-1][1])
|
84
|
+
leading_anchor = pattern[:leading_anchor_length]
|
85
|
+
trailing_anchor = pattern[-trailing_anchor_length:]
|
86
|
+
|
87
|
+
pattern_parts = parsed[1:-1]
|
88
|
+
|
89
|
+
# Adjust length constraints by subtracting fixed literals length
|
90
|
+
fixed_length = sum(1 for op, _ in pattern_parts if op == LITERAL)
|
91
|
+
if min_length is not None:
|
92
|
+
min_length -= fixed_length
|
93
|
+
if min_length < 0:
|
94
|
+
return pattern
|
95
|
+
if max_length is not None:
|
96
|
+
max_length -= fixed_length
|
97
|
+
if max_length < 0:
|
98
|
+
return pattern
|
99
|
+
|
100
|
+
# Extract only min/max bounds from quantified parts
|
101
|
+
quantifier_bounds = [value[:2] for op, value in pattern_parts if op in REPEATS]
|
102
|
+
|
103
|
+
if not quantifier_bounds:
|
104
|
+
return pattern
|
105
|
+
|
106
|
+
length_distribution = _distribute_length_constraints(quantifier_bounds, min_length, max_length)
|
107
|
+
if not length_distribution:
|
108
|
+
return pattern
|
109
|
+
|
110
|
+
# Rebuild pattern with updated quantifiers
|
111
|
+
result = leading_anchor
|
112
|
+
current_position = leading_anchor_length
|
113
|
+
distribution_idx = 0
|
114
|
+
|
115
|
+
for op, value in pattern_parts:
|
116
|
+
if op == LITERAL:
|
117
|
+
if pattern[current_position] == "\\":
|
118
|
+
# Escaped value
|
119
|
+
current_position += 2
|
120
|
+
result += "\\"
|
121
|
+
else:
|
122
|
+
current_position += 1
|
123
|
+
result += chr(value)
|
124
|
+
else:
|
125
|
+
new_min, new_max = length_distribution[distribution_idx]
|
126
|
+
next_position = _find_quantified_end(pattern, current_position)
|
127
|
+
quantified_segment = pattern[current_position:next_position]
|
128
|
+
_, _, subpattern = value
|
129
|
+
new_value = (new_min, new_max, subpattern)
|
130
|
+
|
131
|
+
result += _update_quantifier(op, new_value, quantified_segment, new_min, new_max)
|
132
|
+
current_position = next_position
|
133
|
+
distribution_idx += 1
|
134
|
+
|
135
|
+
return result + trailing_anchor
|
136
|
+
|
137
|
+
|
138
|
+
def _find_quantified_end(pattern: str, start: int) -> int:
|
139
|
+
"""Find the end position of current quantified part."""
|
140
|
+
char_class_level = 0
|
141
|
+
group_level = 0
|
142
|
+
|
143
|
+
for i in range(start, len(pattern)):
|
144
|
+
char = pattern[i]
|
145
|
+
|
146
|
+
# Handle character class nesting
|
147
|
+
if char == "[":
|
148
|
+
char_class_level += 1
|
149
|
+
elif char == "]":
|
150
|
+
char_class_level -= 1
|
151
|
+
|
152
|
+
# Handle group nesting
|
153
|
+
elif char == "(":
|
154
|
+
group_level += 1
|
155
|
+
elif char == ")":
|
156
|
+
group_level -= 1
|
157
|
+
|
158
|
+
# Only process quantifiers when we're not inside any nested structure
|
159
|
+
elif char_class_level == 0 and group_level == 0:
|
160
|
+
if char in "*+?":
|
161
|
+
return i + 1
|
162
|
+
elif char == "{":
|
163
|
+
# Find matching }
|
164
|
+
while i < len(pattern) and pattern[i] != "}":
|
165
|
+
i += 1
|
166
|
+
return i + 1
|
167
|
+
|
168
|
+
return len(pattern)
|
169
|
+
|
170
|
+
|
171
|
+
def _distribute_length_constraints(
|
172
|
+
bounds: list[tuple[int, int]], min_length: int | None, max_length: int | None
|
173
|
+
) -> list[tuple[int, int]] | None:
|
174
|
+
"""Distribute length constraints among quantified pattern parts."""
|
175
|
+
# Handle exact length case with dynamic programming
|
176
|
+
if min_length == max_length:
|
177
|
+
assert min_length is not None
|
178
|
+
target = min_length
|
179
|
+
dp: dict[tuple[int, int], list[tuple[int, ...]] | None] = {}
|
180
|
+
|
181
|
+
def find_valid_combination(pos: int, remaining: int) -> list[tuple[int, ...]] | None:
|
182
|
+
if (pos, remaining) in dp:
|
183
|
+
return dp[(pos, remaining)]
|
184
|
+
|
185
|
+
if pos == len(bounds):
|
186
|
+
return [()] if remaining == 0 else None
|
187
|
+
|
188
|
+
max_len: int
|
189
|
+
min_len, max_len = bounds[pos]
|
190
|
+
if max_len == MAXREPEAT:
|
191
|
+
max_len = remaining + 1
|
192
|
+
else:
|
193
|
+
max_len += 1
|
194
|
+
|
195
|
+
# Try each possible length for current quantifier
|
196
|
+
for length in range(min_len, max_len):
|
197
|
+
rest = find_valid_combination(pos + 1, remaining - length)
|
198
|
+
if rest is not None:
|
199
|
+
dp[(pos, remaining)] = [(length,) + r for r in rest]
|
200
|
+
return dp[(pos, remaining)]
|
201
|
+
|
202
|
+
dp[(pos, remaining)] = None
|
203
|
+
return None
|
204
|
+
|
205
|
+
distribution = find_valid_combination(0, target)
|
206
|
+
if distribution:
|
207
|
+
return [(length, length) for length in distribution[0]]
|
208
|
+
return None
|
209
|
+
|
210
|
+
# Handle range case by distributing min/max bounds
|
211
|
+
result = []
|
212
|
+
remaining_min = min_length or 0
|
213
|
+
remaining_max = max_length or MAXREPEAT
|
214
|
+
|
215
|
+
for min_repeat, max_repeat in bounds:
|
216
|
+
if remaining_min > 0:
|
217
|
+
part_min = min(max_repeat, max(min_repeat, remaining_min))
|
218
|
+
else:
|
219
|
+
part_min = min_repeat
|
220
|
+
|
221
|
+
if remaining_max < MAXREPEAT:
|
222
|
+
part_max = min(max_repeat, remaining_max)
|
223
|
+
else:
|
224
|
+
part_max = max_repeat
|
225
|
+
|
226
|
+
if part_min > part_max:
|
227
|
+
return None
|
228
|
+
|
229
|
+
result.append((part_min, part_max))
|
230
|
+
|
231
|
+
remaining_min = max(0, remaining_min - part_min)
|
232
|
+
remaining_max -= part_max if part_max != MAXREPEAT else 0
|
233
|
+
|
234
|
+
if remaining_min > 0 or remaining_max < 0:
|
235
|
+
return None
|
236
|
+
|
237
|
+
return result
|
238
|
+
|
239
|
+
|
72
240
|
def _get_anchor_length(node_type: int) -> int:
|
73
241
|
"""Determine the length of the anchor based on its type."""
|
74
242
|
if node_type in {sre.AT_BEGINNING_STRING, sre.AT_END_STRING, sre.AT_BOUNDARY, sre.AT_NON_BOUNDARY}:
|
@@ -93,13 +261,13 @@ def _handle_repeat_quantifier(
|
|
93
261
|
min_length, max_length = _build_size(min_repeat, max_repeat, min_length, max_length)
|
94
262
|
if min_length > max_length:
|
95
263
|
return pattern
|
96
|
-
return f"({_strip_quantifier(pattern)})" + _build_quantifier(min_length, max_length)
|
264
|
+
return f"({_strip_quantifier(pattern).strip(')(')})" + _build_quantifier(min_length, max_length)
|
97
265
|
|
98
266
|
|
99
267
|
def _handle_literal_or_in_quantifier(pattern: str, min_length: int | None, max_length: int | None) -> str:
|
100
268
|
"""Handle literal or character class quantifiers."""
|
101
269
|
min_length = 1 if min_length is None else max(min_length, 1)
|
102
|
-
return f"({pattern})" + _build_quantifier(min_length, max_length)
|
270
|
+
return f"({pattern.strip(')(')})" + _build_quantifier(min_length, max_length)
|
103
271
|
|
104
272
|
|
105
273
|
def _build_quantifier(minimum: int | None, maximum: int | None) -> str:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: schemathesis
|
3
|
-
Version: 3.39.
|
3
|
+
Version: 3.39.8
|
4
4
|
Summary: Property-based testing framework for Open API and GraphQL based apps
|
5
5
|
Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
|
6
6
|
Project-URL: Changelog, https://schemathesis.readthedocs.io/en/stable/changelog.html
|
@@ -1,7 +1,7 @@
|
|
1
1
|
schemathesis/__init__.py,sha256=UW2Bq8hDDkcBeAAA7PzpBFXkOOxkmHox-mfQwzHDjL0,1914
|
2
2
|
schemathesis/_compat.py,sha256=y4RZd59i2NCnZ91VQhnKeMn_8t3SgvLOk2Xm8nymUHY,1837
|
3
3
|
schemathesis/_dependency_versions.py,sha256=pjEkkGAfOQJYNb-9UOo84V8nj_lKHr_TGDVdFwY2UU0,816
|
4
|
-
schemathesis/_hypothesis.py,sha256=
|
4
|
+
schemathesis/_hypothesis.py,sha256=d9MgjqKSQ_chQ8a8dCBnSQTAboOt2h3914IAmIZTXUI,24660
|
5
5
|
schemathesis/_lazy_import.py,sha256=aMhWYgbU2JOltyWBb32vnWBb6kykOghucEzI_F70yVE,470
|
6
6
|
schemathesis/_override.py,sha256=TAjYB3eJQmlw9K_xiR9ptt9Wj7if4U7UFlUhGjpBAoM,1625
|
7
7
|
schemathesis/_patches.py,sha256=Hsbpn4UVeXUQD2Kllrbq01CSWsTYENWa0VJTyhX5C2k,895
|
@@ -61,7 +61,7 @@ schemathesis/fixups/utf8_bom.py,sha256=lWT9RNmJG8i-l5AXIpaCT3qCPUwRgzXPW3eoOjmZE
|
|
61
61
|
schemathesis/generation/__init__.py,sha256=29Zys_tD6kfngaC4zHeC6TOBZQcmo7CWm7KDSYsHStQ,1581
|
62
62
|
schemathesis/generation/_hypothesis.py,sha256=74fzLPHugZgMQXerWYFAMqCAjtAXz5E4gek7Gnkhli4,1756
|
63
63
|
schemathesis/generation/_methods.py,sha256=r8oVlJ71_gXcnEhU-byw2E0R2RswQQFm8U7yGErSqbw,1204
|
64
|
-
schemathesis/generation/coverage.py,sha256=
|
64
|
+
schemathesis/generation/coverage.py,sha256=YrAvnIywwYR0yjm6rhLZ0foRf4D8CAzT8UKjAFn5eRM,39227
|
65
65
|
schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
|
66
66
|
schemathesis/internal/checks.py,sha256=YBhldvs-oQTrtvTlz3cjaO9Ri2oQeyobFcquO4Y0UJ8,2720
|
67
67
|
schemathesis/internal/copy.py,sha256=DcL56z-d69kKR_5u8mlHvjSL1UTyUKNMAwexrwHFY1s,1031
|
@@ -117,7 +117,7 @@ schemathesis/specs/openapi/links.py,sha256=C4Uir2P_EcpqME8ee_a1vdUM8Tm3ZcKNn2YsG
|
|
117
117
|
schemathesis/specs/openapi/loaders.py,sha256=5B1cgYEBj3h2psPQxzrQ5Xq5owLVGw-u9HsCQIx7yFE,25705
|
118
118
|
schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
|
119
119
|
schemathesis/specs/openapi/parameters.py,sha256=LUahlWKCDSlp94v2IA1Q90pyeECgO6FmrqbzCU-9Z0Y,14658
|
120
|
-
schemathesis/specs/openapi/patterns.py,sha256=
|
120
|
+
schemathesis/specs/openapi/patterns.py,sha256=OxZp31cBEHv8fwoeYJ9JcdWNHFMIGzRISNN3dCBc9Dg,11260
|
121
121
|
schemathesis/specs/openapi/references.py,sha256=euxM02kQGMHh4Ss1jWjOY_gyw_HazafKITIsvOEiAvI,9831
|
122
122
|
schemathesis/specs/openapi/schemas.py,sha256=JA9SiBnwYg75kYnd4_0CWOuQv_XTfYwuDeGmFe4RtVo,53724
|
123
123
|
schemathesis/specs/openapi/security.py,sha256=Z-6pk2Ga1PTUtBe298KunjVHsNh5A-teegeso7zcPIE,7138
|
@@ -153,8 +153,8 @@ schemathesis/transports/auth.py,sha256=urSTO9zgFO1qU69xvnKHPFQV0SlJL3d7_Ojl0tLnZ
|
|
153
153
|
schemathesis/transports/content_types.py,sha256=MiKOm-Hy5i75hrROPdpiBZPOTDzOwlCdnthJD12AJzI,2187
|
154
154
|
schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
|
155
155
|
schemathesis/transports/responses.py,sha256=OFD4ZLqwEFpo7F9vaP_SVgjhxAqatxIj38FS4XVq8Qs,1680
|
156
|
-
schemathesis-3.39.
|
157
|
-
schemathesis-3.39.
|
158
|
-
schemathesis-3.39.
|
159
|
-
schemathesis-3.39.
|
160
|
-
schemathesis-3.39.
|
156
|
+
schemathesis-3.39.8.dist-info/METADATA,sha256=OIpYE4hvAtDJB6GJJi2xRasTiPIOlRQkoMe0ju0EBDY,11976
|
157
|
+
schemathesis-3.39.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
158
|
+
schemathesis-3.39.8.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
|
159
|
+
schemathesis-3.39.8.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
160
|
+
schemathesis-3.39.8.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|