schemathesis 3.36.1__py3-none-any.whl → 3.36.2__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.
@@ -15,7 +15,7 @@ from hypothesis_jsonschema._canonicalise import canonicalish
15
15
 
16
16
  from schemathesis.constants import NOT_SET
17
17
 
18
- from ._hypothesis import combine_strategies, get_single_example
18
+ from ._hypothesis import get_single_example
19
19
  from ._methods import DataGenerationMethod
20
20
 
21
21
  BUFFER_SIZE = 8 * 1024
@@ -157,7 +157,11 @@ def _ignore_unfixable(
157
157
  raise
158
158
 
159
159
 
160
- def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[GeneratedValue, None, None]:
160
+ def cover_schema_iter(
161
+ ctx: CoverageContext, schema: dict | bool, seen: set[Any | tuple[type, str]] | None = None
162
+ ) -> Generator[GeneratedValue, None, None]:
163
+ if seen is None:
164
+ seen = set()
161
165
  if isinstance(schema, bool):
162
166
  types = ["null", "boolean", "string", "number", "array", "object"]
163
167
  schema = {}
@@ -174,13 +178,16 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[Ge
174
178
  yield from _cover_positive_for_type(ctx, schema, ty)
175
179
  if DataGenerationMethod.negative in ctx.data_generation_methods:
176
180
  template = None
177
- seen: set[Any | tuple[type, str]] = set()
178
181
  for key, value in schema.items():
179
182
  with _ignore_unfixable():
180
183
  if key == "enum":
181
184
  yield from _negative_enum(ctx, value)
182
185
  elif key == "const":
183
- yield from _negative_enum(ctx, [value])
186
+ for value_ in _negative_enum(ctx, [value]):
187
+ k = _to_hashable_key(value_.value)
188
+ if k not in seen:
189
+ yield value_
190
+ seen.add(k)
184
191
  elif key == "type":
185
192
  yield from _negative_type(ctx, seen, value)
186
193
  elif key == "properties":
@@ -192,27 +199,37 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[Ge
192
199
  yield from _negative_format(ctx, schema, value)
193
200
  elif key == "maximum":
194
201
  next = value + 1
195
- yield NegativeValue(next)
196
- seen.add(next)
202
+ if next not in seen:
203
+ yield NegativeValue(next)
204
+ seen.add(next)
197
205
  elif key == "minimum":
198
206
  next = value - 1
199
- yield NegativeValue(next)
200
- seen.add(next)
207
+ if next not in seen:
208
+ yield NegativeValue(next)
209
+ seen.add(next)
201
210
  elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and value not in seen:
202
211
  yield NegativeValue(value)
203
212
  seen.add(value)
204
213
  elif key == "multipleOf":
205
- yield from _negative_multiple_of(ctx, schema, value)
214
+ for value_ in _negative_multiple_of(ctx, schema, value):
215
+ k = _to_hashable_key(value_.value)
216
+ if k not in seen:
217
+ yield value_
218
+ seen.add(k)
206
219
  elif key == "minLength" and 0 < value < BUFFER_SIZE:
207
220
  with suppress(InvalidArgument):
208
- yield NegativeValue(
209
- ctx.generate_from_schema({**schema, "minLength": value - 1, "maxLength": value - 1})
210
- )
221
+ value = ctx.generate_from_schema({**schema, "minLength": value - 1, "maxLength": value - 1})
222
+ k = _to_hashable_key(value)
223
+ if k not in seen:
224
+ yield NegativeValue(value)
225
+ seen.add(k)
211
226
  elif key == "maxLength" and value < BUFFER_SIZE:
212
227
  with suppress(InvalidArgument):
213
- yield NegativeValue(
214
- ctx.generate_from_schema({**schema, "minLength": value + 1, "maxLength": value + 1})
215
- )
228
+ value = ctx.generate_from_schema({**schema, "minLength": value + 1, "maxLength": value + 1})
229
+ k = _to_hashable_key(value)
230
+ if k not in seen:
231
+ yield NegativeValue(value)
232
+ seen.add(k)
216
233
  elif key == "uniqueItems" and value:
217
234
  yield from _negative_unique_items(ctx, schema)
218
235
  elif key == "required":
@@ -224,16 +241,16 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[Ge
224
241
  elif key == "allOf":
225
242
  nctx = ctx.with_negative()
226
243
  if len(value) == 1:
227
- yield from cover_schema_iter(nctx, value[0])
244
+ yield from cover_schema_iter(nctx, value[0], seen)
228
245
  else:
229
246
  with _ignore_unfixable():
230
247
  canonical = canonicalish(schema)
231
- yield from cover_schema_iter(nctx, canonical)
248
+ yield from cover_schema_iter(nctx, canonical, seen)
232
249
  elif key == "anyOf" or key == "oneOf":
233
250
  nctx = ctx.with_negative()
234
251
  # NOTE: Other sub-schemas are not filtered out
235
252
  for sub_schema in value:
236
- yield from cover_schema_iter(nctx, sub_schema)
253
+ yield from cover_schema_iter(nctx, sub_schema, seen)
237
254
 
238
255
 
239
256
  def _get_properties(schema: dict | bool) -> dict | bool:
@@ -584,10 +601,13 @@ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Gene
584
601
  del strategies["integer"]
585
602
  if "integer" in types:
586
603
  strategies["number"] = FLOAT_STRATEGY.filter(lambda x: x != int(x))
587
- negative_strategy = combine_strategies(tuple(strategies.values())).filter(lambda x: _to_hashable_key(x) not in seen)
588
- value = ctx.generate_from(negative_strategy, cached=True)
589
- yield NegativeValue(value)
590
- seen.add(_to_hashable_key(value))
604
+ for strat in strategies.values():
605
+ value = ctx.generate_from(strat, cached=True)
606
+ hashed = _to_hashable_key(value)
607
+ if hashed in seen:
608
+ continue
609
+ yield NegativeValue(value)
610
+ seen.add(hashed)
591
611
 
592
612
 
593
613
  def push_examples_to_properties(schema: dict[str, Any]) -> None:
@@ -595,9 +615,10 @@ def push_examples_to_properties(schema: dict[str, Any]) -> None:
595
615
  if "examples" in schema and "properties" in schema:
596
616
  properties = schema["properties"]
597
617
  for example in schema["examples"]:
598
- for prop, value in example.items():
599
- if prop in properties:
600
- if "examples" not in properties[prop]:
601
- properties[prop]["examples"] = []
602
- if value not in schema["properties"][prop]["examples"]:
603
- properties[prop]["examples"].append(value)
618
+ if isinstance(example, dict):
619
+ for prop, value in example.items():
620
+ if prop in properties:
621
+ if "examples" not in properties[prop]:
622
+ properties[prop]["examples"] = []
623
+ if value not in schema["properties"][prop]["examples"]:
624
+ properties[prop]["examples"].append(value)
@@ -6,10 +6,11 @@ from dataclasses import dataclass
6
6
  from typing import TYPE_CHECKING, Callable, Optional
7
7
 
8
8
  if TYPE_CHECKING:
9
- from ..types import RawAuth
9
+ from requests.structures import CaseInsensitiveDict
10
+
10
11
  from ..models import Case
11
12
  from ..transports.responses import GenericResponse
12
- from requests.structures import CaseInsensitiveDict
13
+ from ..types import RawAuth
13
14
 
14
15
 
15
16
  CheckFunction = Callable[["CheckContext", "GenericResponse", "Case"], Optional[bool]]
@@ -527,7 +527,12 @@ def is_valid_path(parameters: dict[str, Any]) -> bool:
527
527
  disallowed_values = (SLASH, "")
528
528
 
529
529
  return not any(
530
- (value in disallowed_values or is_illegal_surrogate(value) or isinstance(value, str) and SLASH in value)
530
+ (
531
+ value in disallowed_values
532
+ or is_illegal_surrogate(value)
533
+ or isinstance(value, str)
534
+ and (SLASH in value or "}" in value or "{" in value)
535
+ )
531
536
  for value in parameters.values()
532
537
  )
533
538
 
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass
4
3
  import enum
4
+ from dataclasses import dataclass
5
5
  from http.cookies import SimpleCookie
6
6
  from typing import TYPE_CHECKING, Any, Dict, Generator, NoReturn, cast
7
7
  from urllib.parse import parse_qs, urlparse
@@ -5,6 +5,7 @@ from typing import Any, Callable
5
5
 
6
6
  from ...internal.copy import fast_deepcopy
7
7
  from ...internal.jsonschema import traverse_schema
8
+ from .patterns import update_quantifier
8
9
 
9
10
 
10
11
  def to_json_schema(
@@ -24,6 +25,15 @@ def to_json_schema(
24
25
  if schema_type == "file":
25
26
  schema["type"] = "string"
26
27
  schema["format"] = "binary"
28
+ pattern = schema.get("pattern")
29
+ min_length = schema.get("minLength")
30
+ max_length = schema.get("maxLength")
31
+ if pattern and (min_length or max_length):
32
+ new_pattern = update_quantifier(pattern, min_length, max_length)
33
+ if new_pattern != pattern:
34
+ schema.pop("minLength", None)
35
+ schema.pop("maxLength", None)
36
+ schema["pattern"] = new_pattern
27
37
  if schema_type == "object":
28
38
  if is_response_schema:
29
39
  # Write-only properties should not occur in responses
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from functools import lru_cache
5
+
6
+ try: # pragma: no cover
7
+ import re._constants as sre
8
+ import re._parser as sre_parse
9
+ except ImportError:
10
+ import sre_constants as sre
11
+ import sre_parse
12
+
13
+ ANCHOR = sre.AT
14
+ REPEATS: tuple
15
+ if hasattr(sre, "POSSESSIVE_REPEAT"):
16
+ REPEATS = (sre.MIN_REPEAT, sre.MAX_REPEAT, sre.POSSESSIVE_REPEAT)
17
+ else:
18
+ REPEATS = (sre.MIN_REPEAT, sre.MAX_REPEAT)
19
+ LITERAL = sre.LITERAL
20
+ IN = sre.IN
21
+ MAXREPEAT = sre_parse.MAXREPEAT
22
+
23
+
24
+ @lru_cache()
25
+ def update_quantifier(pattern: str, min_length: int | None, max_length: int | None) -> str:
26
+ """Update the quantifier of a regular expression based on given min and max lengths."""
27
+ if not pattern or (min_length in (None, 0) and max_length is None):
28
+ return pattern
29
+
30
+ try:
31
+ parsed = sre_parse.parse(pattern)
32
+ return _handle_parsed_pattern(parsed, pattern, min_length, max_length)
33
+ except re.error:
34
+ # Invalid pattern
35
+ return pattern
36
+
37
+
38
+ def _handle_parsed_pattern(parsed: list, pattern: str, min_length: int | None, max_length: int | None) -> str:
39
+ """Handle the parsed pattern and update quantifiers based on different cases."""
40
+ if len(parsed) == 1:
41
+ op, value = parsed[0]
42
+ return _update_quantifier(op, value, pattern, min_length, max_length)
43
+ elif len(parsed) == 2:
44
+ if parsed[0][0] == ANCHOR:
45
+ # Starts with an anchor
46
+ op, value = parsed[1]
47
+ leading_anchor = pattern[0]
48
+ return leading_anchor + _update_quantifier(op, value, pattern[1:], min_length, max_length)
49
+ if parsed[1][0] == ANCHOR:
50
+ # Ends with an anchor
51
+ op, value = parsed[0]
52
+ trailing_anchor = pattern[-1]
53
+ return _update_quantifier(op, value, pattern[:-1], min_length, max_length) + trailing_anchor
54
+ elif len(parsed) == 3 and parsed[0][0] == ANCHOR and parsed[2][0] == ANCHOR:
55
+ op, value = parsed[1]
56
+ leading_anchor = pattern[0]
57
+ trailing_anchor = pattern[-1]
58
+ return leading_anchor + _update_quantifier(op, value, pattern[1:-1], min_length, max_length) + trailing_anchor
59
+ return pattern
60
+
61
+
62
+ def _update_quantifier(op: int, value: tuple, pattern: str, min_length: int | None, max_length: int | None) -> str:
63
+ """Update the quantifier based on the operation type and given constraints."""
64
+ if op in REPEATS:
65
+ return _handle_repeat_quantifier(value, pattern, min_length, max_length)
66
+ if op in (LITERAL, IN) and max_length != 0:
67
+ return _handle_literal_or_in_quantifier(pattern, min_length, max_length)
68
+ return pattern
69
+
70
+
71
+ def _handle_repeat_quantifier(
72
+ value: tuple[int, int, tuple], pattern: str, min_length: int | None, max_length: int | None
73
+ ) -> str:
74
+ """Handle repeat quantifiers (e.g., '+', '*', '?')."""
75
+ min_repeat, max_repeat, _ = value
76
+ min_length, max_length = _build_size(min_repeat, max_repeat, min_length, max_length)
77
+ if min_length > max_length:
78
+ return pattern
79
+ return f"({_strip_quantifier(pattern)})" + _build_quantifier(min_length, max_length)
80
+
81
+
82
+ def _handle_literal_or_in_quantifier(pattern: str, min_length: int | None, max_length: int | None) -> str:
83
+ """Handle literal or character class quantifiers."""
84
+ min_length = 1 if min_length is None else max(min_length, 1)
85
+ return f"({pattern})" + _build_quantifier(min_length, max_length)
86
+
87
+
88
+ def _build_quantifier(minimum: int | None, maximum: int | None) -> str:
89
+ """Construct a quantifier string based on min and max values."""
90
+ if maximum == MAXREPEAT or maximum is None:
91
+ return f"{{{minimum or 0},}}"
92
+ if minimum == maximum:
93
+ return f"{{{minimum}}}"
94
+ return f"{{{minimum or 0},{maximum}}}"
95
+
96
+
97
+ def _build_size(min_repeat: int, max_repeat: int, min_length: int | None, max_length: int | None) -> tuple[int, int]:
98
+ """Merge the current repetition constraints with the provided min and max lengths."""
99
+ if min_length is not None:
100
+ min_repeat = max(min_repeat, min_length)
101
+ if max_length is not None:
102
+ if max_repeat == MAXREPEAT:
103
+ max_repeat = max_length
104
+ else:
105
+ max_repeat = min(max_repeat, max_length)
106
+ return min_repeat, max_repeat
107
+
108
+
109
+ def _strip_quantifier(pattern: str) -> str:
110
+ """Remove quantifier from the pattern."""
111
+ # Lazy & posessive quantifiers
112
+ if pattern.endswith(("*?", "+?", "??", "*+", "?+", "++")):
113
+ return pattern[:-2]
114
+ if pattern.endswith(("?", "*", "+")):
115
+ pattern = pattern[:-1]
116
+ if pattern.endswith("}"):
117
+ # Find the start of the exact quantifier and drop everything since that index
118
+ idx = pattern.rfind("{")
119
+ pattern = pattern[:idx]
120
+ return pattern
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: schemathesis
3
- Version: 3.36.1
3
+ Version: 3.36.2
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
@@ -60,9 +60,9 @@ schemathesis/fixups/utf8_bom.py,sha256=lWT9RNmJG8i-l5AXIpaCT3qCPUwRgzXPW3eoOjmZE
60
60
  schemathesis/generation/__init__.py,sha256=29Zys_tD6kfngaC4zHeC6TOBZQcmo7CWm7KDSYsHStQ,1581
61
61
  schemathesis/generation/_hypothesis.py,sha256=QDBzpcM9eXPgLGGdCPdGlxCtfMXD4YBN9_6Oz73lofI,1406
62
62
  schemathesis/generation/_methods.py,sha256=jCK09f4sedDfePrS-6BIiE-CcEE8fJ4ZHxq1BHoTltQ,1101
63
- schemathesis/generation/coverage.py,sha256=gEB8J9qJTSVRdINVYUw0twCxPIwmuWF2FPJRIwSiy5A,23944
63
+ schemathesis/generation/coverage.py,sha256=F_ABsBsQ7k4dR_sGI2ZNTUNRCjxpAPh5V80MYXx8F20,24758
64
64
  schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
65
- schemathesis/internal/checks.py,sha256=_DO0SlpfgiwEu5vkfS3hyXyC7BsGKKIPuAPgLhfGZ1M,1667
65
+ schemathesis/internal/checks.py,sha256=m6lY6x2Pkz6AjU8Hs-UMSDJZEOq8EcgZOCp6BQA7p_g,1668
66
66
  schemathesis/internal/copy.py,sha256=DcL56z-d69kKR_5u8mlHvjSL1UTyUKNMAwexrwHFY1s,1031
67
67
  schemathesis/internal/datetime.py,sha256=zPLBL0XXLNfP-KYel3H2m8pnsxjsA_4d-zTOhJg2EPQ,136
68
68
  schemathesis/internal/deprecation.py,sha256=Ty5VBFBlufkITpP0WWTPIPbnB7biDi0kQgXVYWZp820,1273
@@ -104,10 +104,10 @@ schemathesis/specs/graphql/schemas.py,sha256=b7QwglKbcYQCMjuYmqDsVoFu2o4xaA_kduU
104
104
  schemathesis/specs/graphql/validation.py,sha256=uINIOt-2E7ZuQV2CxKzwez-7L9tDtqzMSpnVoRWvxy0,1635
105
105
  schemathesis/specs/openapi/__init__.py,sha256=HDcx3bqpa6qWPpyMrxAbM3uTo0Lqpg-BUNZhDJSJKnw,279
106
106
  schemathesis/specs/openapi/_cache.py,sha256=PAiAu4X_a2PQgD2lG5H3iisXdyg4SaHpU46bRZvfNkM,4320
107
- schemathesis/specs/openapi/_hypothesis.py,sha256=XgKq36ONJIWM-8ASnDpzOgcCcVz-uUQw74bOxcUC3n8,24201
108
- schemathesis/specs/openapi/checks.py,sha256=sOfnEoeu7aGTM7RXplhPy1iUaibxIzytjWtBp4at8S4,22288
107
+ schemathesis/specs/openapi/_hypothesis.py,sha256=ZbLo8hDf4WVmRKmKF6rhK47xetkVJmE59Ghu_p7kLCw,24293
108
+ schemathesis/specs/openapi/checks.py,sha256=-4qOzkova0e4QSqdgsoUiOv2bg57HZmzbpAiAeotc3Q,22288
109
109
  schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
110
- schemathesis/specs/openapi/converter.py,sha256=TaYgc5BBHPdkN-n0lqpbeVgLu3eL3L8Wu3y_Vo3TJaQ,2800
110
+ schemathesis/specs/openapi/converter.py,sha256=NkrzBNjtmVwQTeE73NOtwB_puvQTjxxqqrc7gD_yscc,3241
111
111
  schemathesis/specs/openapi/definitions.py,sha256=nEsCKn_LgqYjZ9nNWp-8KUIrB4S94pT3GsV5A8UIzDw,94043
112
112
  schemathesis/specs/openapi/examples.py,sha256=FwhPWca7bpdHpUp_LRoK09DVgusojO3aXXhXYrK373I,20354
113
113
  schemathesis/specs/openapi/formats.py,sha256=JmmkQWNAj5XreXb7Edgj4LADAf4m86YulR_Ec8evpJ4,1220
@@ -115,6 +115,7 @@ schemathesis/specs/openapi/links.py,sha256=a8JmWM9aZhrR5CfyIh6t2SkfonMLfYKOScXY2
115
115
  schemathesis/specs/openapi/loaders.py,sha256=5B1cgYEBj3h2psPQxzrQ5Xq5owLVGw-u9HsCQIx7yFE,25705
116
116
  schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
117
117
  schemathesis/specs/openapi/parameters.py,sha256=CqJdS4d14l25_yEbqkLCnfIdDTlodRhJpxD8EXdaFwM,14059
118
+ schemathesis/specs/openapi/patterns.py,sha256=IK2BkXI1xByEz5if6jvydFE07nq5rDa4k_-2xX7ifG8,4715
118
119
  schemathesis/specs/openapi/references.py,sha256=euxM02kQGMHh4Ss1jWjOY_gyw_HazafKITIsvOEiAvI,9831
119
120
  schemathesis/specs/openapi/schemas.py,sha256=t3Gz2q-d9b8Oy-hDhz0rNfjYT3Nx-uOeLOmjO9hpRM0,53741
120
121
  schemathesis/specs/openapi/security.py,sha256=Z-6pk2Ga1PTUtBe298KunjVHsNh5A-teegeso7zcPIE,7138
@@ -150,8 +151,8 @@ schemathesis/transports/auth.py,sha256=urSTO9zgFO1qU69xvnKHPFQV0SlJL3d7_Ojl0tLnZ
150
151
  schemathesis/transports/content_types.py,sha256=MiKOm-Hy5i75hrROPdpiBZPOTDzOwlCdnthJD12AJzI,2187
151
152
  schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
152
153
  schemathesis/transports/responses.py,sha256=OFD4ZLqwEFpo7F9vaP_SVgjhxAqatxIj38FS4XVq8Qs,1680
153
- schemathesis-3.36.1.dist-info/METADATA,sha256=ysyAwX1ruHhcAtdGT2Ty2lpc4UpM0zyotuwzDprQd08,12904
154
- schemathesis-3.36.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
155
- schemathesis-3.36.1.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
156
- schemathesis-3.36.1.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
157
- schemathesis-3.36.1.dist-info/RECORD,,
154
+ schemathesis-3.36.2.dist-info/METADATA,sha256=eoWwbh8dfwwyl3EyHoJsV_MUm4zvKYGTMMehbfOJyyQ,12904
155
+ schemathesis-3.36.2.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
156
+ schemathesis-3.36.2.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
157
+ schemathesis-3.36.2.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
158
+ schemathesis-3.36.2.dist-info/RECORD,,