schemathesis 4.1.4__py3-none-any.whl → 4.2.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.
- schemathesis/cli/commands/run/executor.py +1 -1
- schemathesis/cli/commands/run/handlers/base.py +28 -1
- schemathesis/cli/commands/run/handlers/cassettes.py +10 -12
- schemathesis/cli/commands/run/handlers/junitxml.py +5 -6
- schemathesis/cli/commands/run/handlers/output.py +7 -1
- schemathesis/cli/ext/fs.py +1 -1
- schemathesis/config/_diff_base.py +3 -1
- schemathesis/config/_operations.py +2 -0
- schemathesis/config/_phases.py +21 -4
- schemathesis/config/_projects.py +10 -2
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/errors.py +29 -5
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +163 -0
- schemathesis/{specs/openapi/constants.py → core/jsonschema/keywords.py} +0 -8
- schemathesis/core/jsonschema/references.py +122 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/media_types.py +6 -4
- schemathesis/core/parameters.py +37 -0
- schemathesis/core/transforms.py +25 -2
- schemathesis/core/validation.py +19 -0
- schemathesis/engine/context.py +1 -1
- schemathesis/engine/errors.py +11 -18
- schemathesis/engine/phases/stateful/_executor.py +1 -1
- schemathesis/engine/phases/unit/_executor.py +30 -13
- schemathesis/errors.py +4 -0
- schemathesis/filters.py +2 -2
- schemathesis/generation/coverage.py +87 -11
- schemathesis/generation/hypothesis/__init__.py +4 -1
- schemathesis/generation/hypothesis/builder.py +108 -70
- schemathesis/generation/meta.py +5 -14
- schemathesis/generation/overrides.py +17 -17
- schemathesis/pytest/lazy.py +1 -1
- schemathesis/pytest/plugin.py +1 -6
- schemathesis/schemas.py +22 -72
- schemathesis/specs/graphql/schemas.py +27 -16
- schemathesis/specs/openapi/_hypothesis.py +83 -68
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +504 -0
- schemathesis/specs/openapi/adapter/protocol.py +57 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +329 -0
- schemathesis/specs/openapi/adapter/security.py +141 -0
- schemathesis/specs/openapi/adapter/v2.py +28 -0
- schemathesis/specs/openapi/adapter/v3_0.py +28 -0
- schemathesis/specs/openapi/adapter/v3_1.py +28 -0
- schemathesis/specs/openapi/checks.py +99 -90
- schemathesis/specs/openapi/converter.py +114 -27
- schemathesis/specs/openapi/examples.py +210 -168
- schemathesis/specs/openapi/negative/__init__.py +12 -7
- schemathesis/specs/openapi/negative/mutations.py +68 -40
- schemathesis/specs/openapi/references.py +2 -175
- schemathesis/specs/openapi/schemas.py +142 -490
- schemathesis/specs/openapi/serialization.py +15 -7
- schemathesis/specs/openapi/stateful/__init__.py +17 -12
- schemathesis/specs/openapi/stateful/inference.py +13 -11
- schemathesis/specs/openapi/stateful/links.py +5 -20
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/v3.py +68 -0
- schemathesis/specs/openapi/utils.py +1 -13
- schemathesis/transport/requests.py +3 -11
- schemathesis/transport/serialization.py +63 -27
- schemathesis/transport/wsgi.py +1 -8
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/METADATA +2 -2
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/RECORD +68 -53
- schemathesis/specs/openapi/parameters.py +0 -405
- schemathesis/specs/openapi/security.py +0 -162
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/WHEEL +0 -0
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -29,7 +29,8 @@ from hypothesis_jsonschema._canonicalise import canonicalish
|
|
29
29
|
from hypothesis_jsonschema._from_schema import STRING_FORMATS as BUILT_IN_STRING_FORMATS
|
30
30
|
|
31
31
|
from schemathesis.core import INTERNAL_BUFFER_SIZE, NOT_SET
|
32
|
-
from schemathesis.core.compat import RefResolutionError
|
32
|
+
from schemathesis.core.compat import RefResolutionError, RefResolver
|
33
|
+
from schemathesis.core.parameters import ParameterLocation
|
33
34
|
from schemathesis.core.transforms import deepclone
|
34
35
|
from schemathesis.core.validation import contains_unicode_surrogate_pair, has_invalid_characters, is_latin_1_encodable
|
35
36
|
from schemathesis.generation import GenerationMode
|
@@ -121,31 +122,62 @@ def cached_draw(strategy: st.SearchStrategy) -> Any:
|
|
121
122
|
|
122
123
|
@dataclass
|
123
124
|
class CoverageContext:
|
125
|
+
root_schema: dict[str, Any]
|
124
126
|
generation_modes: list[GenerationMode]
|
125
|
-
location:
|
127
|
+
location: ParameterLocation
|
128
|
+
media_type: tuple[str, str] | None
|
126
129
|
is_required: bool
|
127
130
|
path: list[str | int]
|
128
131
|
custom_formats: dict[str, st.SearchStrategy]
|
129
132
|
validator_cls: type[jsonschema.protocols.Validator]
|
130
|
-
|
131
|
-
|
133
|
+
_resolver: RefResolver | None
|
134
|
+
|
135
|
+
__slots__ = (
|
136
|
+
"root_schema",
|
137
|
+
"location",
|
138
|
+
"media_type",
|
139
|
+
"generation_modes",
|
140
|
+
"is_required",
|
141
|
+
"path",
|
142
|
+
"custom_formats",
|
143
|
+
"validator_cls",
|
144
|
+
"_resolver",
|
145
|
+
)
|
132
146
|
|
133
147
|
def __init__(
|
134
148
|
self,
|
135
149
|
*,
|
136
|
-
|
150
|
+
root_schema: dict[str, Any],
|
151
|
+
location: ParameterLocation,
|
152
|
+
media_type: tuple[str, str] | None,
|
137
153
|
generation_modes: list[GenerationMode] | None = None,
|
138
154
|
is_required: bool,
|
139
155
|
path: list[str | int] | None = None,
|
140
156
|
custom_formats: dict[str, st.SearchStrategy],
|
141
157
|
validator_cls: type[jsonschema.protocols.Validator],
|
158
|
+
_resolver: RefResolver | None = None,
|
142
159
|
) -> None:
|
160
|
+
self.root_schema = root_schema
|
143
161
|
self.location = location
|
162
|
+
self.media_type = media_type
|
144
163
|
self.generation_modes = generation_modes if generation_modes is not None else list(GenerationMode)
|
145
164
|
self.is_required = is_required
|
146
165
|
self.path = path or []
|
147
166
|
self.custom_formats = custom_formats
|
148
167
|
self.validator_cls = validator_cls
|
168
|
+
self._resolver = _resolver
|
169
|
+
|
170
|
+
@property
|
171
|
+
def resolver(self) -> RefResolver:
|
172
|
+
"""Lazy-initialized cached resolver."""
|
173
|
+
if self._resolver is None:
|
174
|
+
self._resolver = RefResolver.from_schema(self.root_schema)
|
175
|
+
return cast(RefResolver, self._resolver)
|
176
|
+
|
177
|
+
def resolve_ref(self, ref: str) -> dict | bool:
|
178
|
+
"""Resolve a $ref to its schema definition."""
|
179
|
+
_, resolved = self.resolver.resolve(ref)
|
180
|
+
return resolved
|
149
181
|
|
150
182
|
@contextmanager
|
151
183
|
def at(self, key: str | int) -> Generator[None, None, None]:
|
@@ -161,22 +193,28 @@ class CoverageContext:
|
|
161
193
|
|
162
194
|
def with_positive(self) -> CoverageContext:
|
163
195
|
return CoverageContext(
|
196
|
+
root_schema=self.root_schema,
|
164
197
|
location=self.location,
|
198
|
+
media_type=self.media_type,
|
165
199
|
generation_modes=[GenerationMode.POSITIVE],
|
166
200
|
is_required=self.is_required,
|
167
201
|
path=self.path,
|
168
202
|
custom_formats=self.custom_formats,
|
169
203
|
validator_cls=self.validator_cls,
|
204
|
+
_resolver=self._resolver,
|
170
205
|
)
|
171
206
|
|
172
207
|
def with_negative(self) -> CoverageContext:
|
173
208
|
return CoverageContext(
|
209
|
+
root_schema=self.root_schema,
|
174
210
|
location=self.location,
|
211
|
+
media_type=self.media_type,
|
175
212
|
generation_modes=[GenerationMode.NEGATIVE],
|
176
213
|
is_required=self.is_required,
|
177
214
|
path=self.path,
|
178
215
|
custom_formats=self.custom_formats,
|
179
216
|
validator_cls=self.validator_cls,
|
217
|
+
_resolver=self._resolver,
|
180
218
|
)
|
181
219
|
|
182
220
|
def is_valid_for_location(self, value: Any) -> bool:
|
@@ -195,7 +233,16 @@ class CoverageContext:
|
|
195
233
|
return True
|
196
234
|
|
197
235
|
def will_be_serialized_to_string(self) -> bool:
|
198
|
-
return self.location in ("query", "path", "header", "cookie")
|
236
|
+
return self.location in ("query", "path", "header", "cookie") or (
|
237
|
+
self.location == "body"
|
238
|
+
and self.media_type
|
239
|
+
in frozenset(
|
240
|
+
[
|
241
|
+
("multipart", "form-data"),
|
242
|
+
("application", "x-www-form-urlencoded"),
|
243
|
+
]
|
244
|
+
)
|
245
|
+
)
|
199
246
|
|
200
247
|
def can_be_negated(self, schema: dict[str, Any]) -> bool:
|
201
248
|
# Path, query, header, and cookie parameters will be stringified anyway
|
@@ -213,6 +260,10 @@ class CoverageContext:
|
|
213
260
|
return cached_draw(strategy)
|
214
261
|
|
215
262
|
def generate_from_schema(self, schema: dict | bool) -> Any:
|
263
|
+
if isinstance(schema, dict) and "$ref" in schema:
|
264
|
+
reference = schema["$ref"]
|
265
|
+
# Deep clone to avoid circular references in Python objects
|
266
|
+
schema = deepclone(self.resolve_ref(reference))
|
216
267
|
if isinstance(schema, bool):
|
217
268
|
return 0
|
218
269
|
keys = sorted([k for k in schema if not k.startswith("x-") and k not in ["description", "example", "examples"]])
|
@@ -284,10 +335,17 @@ class CoverageContext:
|
|
284
335
|
)
|
285
336
|
|
286
337
|
if keys == ["allOf"]:
|
338
|
+
for idx, sub_schema in enumerate(schema["allOf"]):
|
339
|
+
if "$ref" in sub_schema:
|
340
|
+
schema["allOf"][idx] = self.resolve_ref(sub_schema["$ref"])
|
341
|
+
|
287
342
|
schema = canonicalish(schema)
|
288
343
|
if isinstance(schema, dict) and "allOf" not in schema:
|
289
344
|
return self.generate_from_schema(schema)
|
290
345
|
|
346
|
+
if isinstance(schema, dict) and "examples" in schema:
|
347
|
+
# Examples may contain binary data which will fail the canonicalisation process in `hypothesis-jsonschema`
|
348
|
+
schema = {key: value for key, value in schema.items() if key != "examples"}
|
291
349
|
return self.generate_from(from_schema(schema, custom_formats=self.custom_formats))
|
292
350
|
|
293
351
|
|
@@ -306,7 +364,7 @@ else:
|
|
306
364
|
|
307
365
|
|
308
366
|
def _encode(o: Any) -> str:
|
309
|
-
return "".join(_iterencode(o,
|
367
|
+
return "".join(_iterencode(o, False))
|
310
368
|
|
311
369
|
|
312
370
|
def _to_hashable_key(value: T, _encode: Callable = _encode) -> tuple[type, str | T]:
|
@@ -360,6 +418,9 @@ def _cover_positive_for_type(
|
|
360
418
|
yield from cover_schema_iter(ctx, all_of[0])
|
361
419
|
else:
|
362
420
|
with suppress(jsonschema.SchemaError):
|
421
|
+
for idx, sub_schema in enumerate(all_of):
|
422
|
+
if "$ref" in sub_schema:
|
423
|
+
all_of[idx] = ctx.resolve_ref(sub_schema["$ref"])
|
363
424
|
canonical = canonicalish(schema)
|
364
425
|
yield from cover_schema_iter(ctx, canonical)
|
365
426
|
if enum is not NOT_SET:
|
@@ -414,6 +475,21 @@ def cover_schema_iter(
|
|
414
475
|
) -> Generator[GeneratedValue, None, None]:
|
415
476
|
if seen is None:
|
416
477
|
seen = HashSet()
|
478
|
+
|
479
|
+
if isinstance(schema, dict) and "$ref" in schema:
|
480
|
+
reference = schema["$ref"]
|
481
|
+
try:
|
482
|
+
resolved = ctx.resolve_ref(reference)
|
483
|
+
if isinstance(resolved, dict):
|
484
|
+
schema = {**resolved, **{k: v for k, v in schema.items() if k != "$ref"}}
|
485
|
+
yield from cover_schema_iter(ctx, schema, seen)
|
486
|
+
else:
|
487
|
+
yield from cover_schema_iter(ctx, resolved, seen)
|
488
|
+
return
|
489
|
+
except RefResolutionError:
|
490
|
+
# Can't resolve a reference - at this point, we can't generate anything useful as `$ref` is in the current schema root
|
491
|
+
return
|
492
|
+
|
417
493
|
if schema == {} or schema is True:
|
418
494
|
types = ["null", "boolean", "string", "number", "array", "object"]
|
419
495
|
schema = {}
|
@@ -881,7 +957,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
881
957
|
smaller = largest - multiple_of
|
882
958
|
else:
|
883
959
|
smaller = maximum - 1
|
884
|
-
if (
|
960
|
+
if (minimum is None or smaller >= minimum) and seen.insert(smaller):
|
885
961
|
yield PositiveValue(smaller, description="Near-boundary number")
|
886
962
|
|
887
963
|
|
@@ -1232,7 +1308,7 @@ def _negative_type(
|
|
1232
1308
|
del strategies["integer"]
|
1233
1309
|
if "integer" in types:
|
1234
1310
|
strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
|
1235
|
-
if ctx.location ==
|
1311
|
+
if ctx.location == ParameterLocation.QUERY:
|
1236
1312
|
strategies.pop("object", None)
|
1237
1313
|
if filter_func is not None:
|
1238
1314
|
for ty, strategy in strategies.items():
|
@@ -1264,10 +1340,10 @@ def _negative_type(
|
|
1264
1340
|
def _does_not_match_the_original_schema(value: Any) -> bool:
|
1265
1341
|
return not is_valid(str(value))
|
1266
1342
|
|
1267
|
-
if ctx.location ==
|
1343
|
+
if ctx.location == ParameterLocation.PATH:
|
1268
1344
|
for ty, strategy in strategies.items():
|
1269
1345
|
strategies[ty] = strategy.map(jsonify).map(quote_path_parameter)
|
1270
|
-
elif ctx.location ==
|
1346
|
+
elif ctx.location == ParameterLocation.QUERY:
|
1271
1347
|
for ty, strategy in strategies.items():
|
1272
1348
|
strategies[ty] = strategy.map(jsonify)
|
1273
1349
|
|
@@ -8,9 +8,10 @@ def setup() -> None:
|
|
8
8
|
from hypothesis.internal.reflection import is_first_param_referenced_in_function
|
9
9
|
from hypothesis.strategies._internal import collections, core
|
10
10
|
from hypothesis.vendor import pretty
|
11
|
-
from hypothesis_jsonschema import _from_schema, _resolve
|
11
|
+
from hypothesis_jsonschema import _canonicalise, _from_schema, _resolve
|
12
12
|
|
13
13
|
from schemathesis.core import INTERNAL_BUFFER_SIZE
|
14
|
+
from schemathesis.core.jsonschema.types import _get_type
|
14
15
|
from schemathesis.core.transforms import deepclone
|
15
16
|
|
16
17
|
# Forcefully initializes Hypothesis' global PRNG to avoid races that initialize it
|
@@ -37,6 +38,8 @@ def setup() -> None:
|
|
37
38
|
root_core.RepresentationPrinter = RepresentationPrinter # type: ignore
|
38
39
|
_resolve.deepcopy = deepclone # type: ignore
|
39
40
|
_from_schema.deepcopy = deepclone # type: ignore
|
41
|
+
_from_schema.get_type = _get_type # type: ignore
|
42
|
+
_canonicalise.get_type = _get_type # type: ignore
|
40
43
|
root_core.BUFFER_SIZE = INTERNAL_BUFFER_SIZE # type: ignore
|
41
44
|
engine.BUFFER_SIZE = INTERNAL_BUFFER_SIZE
|
42
45
|
collections.BUFFER_SIZE = INTERNAL_BUFFER_SIZE # type: ignore
|