schemathesis 3.37.1__py3-none-any.whl → 3.38.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.
- schemathesis/_hypothesis.py +44 -8
- schemathesis/_patches.py +21 -0
- schemathesis/cli/__init__.py +1 -1
- schemathesis/cli/cassettes.py +18 -0
- schemathesis/extra/pytest_plugin.py +1 -1
- schemathesis/generation/_hypothesis.py +2 -0
- schemathesis/generation/coverage.py +257 -59
- schemathesis/internal/checks.py +5 -3
- schemathesis/internal/diff.py +15 -0
- schemathesis/models.py +76 -4
- schemathesis/runner/impl/context.py +5 -1
- schemathesis/runner/impl/core.py +14 -4
- schemathesis/runner/serialization.py +10 -3
- schemathesis/serializers.py +3 -0
- schemathesis/service/extensions.py +1 -1
- schemathesis/service/metadata.py +3 -3
- schemathesis/specs/openapi/_hypothesis.py +9 -46
- schemathesis/specs/openapi/checks.py +7 -2
- schemathesis/specs/openapi/converter.py +27 -11
- schemathesis/specs/openapi/formats.py +44 -0
- schemathesis/specs/openapi/negative/mutations.py +5 -0
- schemathesis/specs/openapi/parameters.py +16 -14
- schemathesis/specs/openapi/schemas.py +6 -2
- schemathesis/stateful/context.py +1 -1
- schemathesis/stateful/runner.py +6 -2
- schemathesis/utils.py +6 -4
- {schemathesis-3.37.1.dist-info → schemathesis-3.38.1.dist-info}/METADATA +2 -1
- {schemathesis-3.37.1.dist-info → schemathesis-3.38.1.dist-info}/RECORD +31 -29
- {schemathesis-3.37.1.dist-info → schemathesis-3.38.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.37.1.dist-info → schemathesis-3.38.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.37.1.dist-info → schemathesis-3.38.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/_hypothesis.py
CHANGED
|
@@ -6,15 +6,16 @@ import asyncio
|
|
|
6
6
|
import json
|
|
7
7
|
import warnings
|
|
8
8
|
from copy import copy
|
|
9
|
+
from functools import wraps
|
|
9
10
|
from typing import TYPE_CHECKING, Any, Callable, Generator, Mapping
|
|
10
11
|
|
|
11
12
|
import hypothesis
|
|
12
13
|
from hypothesis import Phase
|
|
13
14
|
from hypothesis.errors import HypothesisWarning, Unsatisfiable
|
|
14
15
|
from hypothesis.internal.entropy import deterministic_PRNG
|
|
15
|
-
from hypothesis.internal.reflection import proxies
|
|
16
16
|
from jsonschema.exceptions import SchemaError
|
|
17
17
|
|
|
18
|
+
from . import _patches
|
|
18
19
|
from .auths import get_auth_storage_from_test
|
|
19
20
|
from .constants import DEFAULT_DEADLINE, NOT_SET
|
|
20
21
|
from .exceptions import OperationSchemaError, SerializationNotPossible
|
|
@@ -29,11 +30,13 @@ from .types import NotSet
|
|
|
29
30
|
if TYPE_CHECKING:
|
|
30
31
|
from .utils import GivenInput
|
|
31
32
|
|
|
32
|
-
# Forcefully initializes Hypothesis' global PRNG to avoid races that
|
|
33
|
+
# Forcefully initializes Hypothesis' global PRNG to avoid races that initialize it
|
|
33
34
|
# if e.g. Schemathesis CLI is used with multiple workers
|
|
34
35
|
with deterministic_PRNG():
|
|
35
36
|
pass
|
|
36
37
|
|
|
38
|
+
_patches.install()
|
|
39
|
+
|
|
37
40
|
|
|
38
41
|
def create_test(
|
|
39
42
|
*,
|
|
@@ -72,7 +75,7 @@ def create_test(
|
|
|
72
75
|
# tests in multiple threads because Hypothesis stores some internal attributes on function objects and re-writing
|
|
73
76
|
# them from different threads may lead to unpredictable side-effects.
|
|
74
77
|
|
|
75
|
-
@
|
|
78
|
+
@wraps(test)
|
|
76
79
|
def test_function(*args: Any, **kwargs: Any) -> Any:
|
|
77
80
|
__tracebackhide__ = True
|
|
78
81
|
return test(*args, **kwargs)
|
|
@@ -220,7 +223,6 @@ def _iter_coverage_cases(
|
|
|
220
223
|
from .specs.openapi.constants import LOCATION_TO_CONTAINER
|
|
221
224
|
from .specs.openapi.examples import find_in_responses, find_matching_in_responses
|
|
222
225
|
|
|
223
|
-
ctx = coverage.CoverageContext(data_generation_methods=data_generation_methods)
|
|
224
226
|
meta = GenerationMetadata(
|
|
225
227
|
query=None,
|
|
226
228
|
path_parameters=None,
|
|
@@ -229,15 +231,20 @@ def _iter_coverage_cases(
|
|
|
229
231
|
body=None,
|
|
230
232
|
phase=TestPhase.COVERAGE,
|
|
231
233
|
description=None,
|
|
234
|
+
location=None,
|
|
235
|
+
parameter=None,
|
|
236
|
+
parameter_location=None,
|
|
232
237
|
)
|
|
233
238
|
generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
|
|
234
239
|
template: dict[str, Any] = {}
|
|
235
240
|
responses = find_in_responses(operation)
|
|
236
241
|
for parameter in operation.iter_parameters():
|
|
237
|
-
schema = parameter.as_json_schema(operation)
|
|
242
|
+
schema = parameter.as_json_schema(operation, update_quantifiers=False)
|
|
238
243
|
for value in find_matching_in_responses(responses, parameter.name):
|
|
239
244
|
schema.setdefault("examples", []).append(value)
|
|
240
|
-
gen = coverage.cover_schema_iter(
|
|
245
|
+
gen = coverage.cover_schema_iter(
|
|
246
|
+
coverage.CoverageContext(data_generation_methods=data_generation_methods), schema
|
|
247
|
+
)
|
|
241
248
|
value = next(gen, NOT_SET)
|
|
242
249
|
if isinstance(value, NotSet):
|
|
243
250
|
continue
|
|
@@ -251,13 +258,15 @@ def _iter_coverage_cases(
|
|
|
251
258
|
generators[(location, name)] = gen
|
|
252
259
|
if operation.body:
|
|
253
260
|
for body in operation.body:
|
|
254
|
-
schema = body.as_json_schema(operation)
|
|
261
|
+
schema = body.as_json_schema(operation, update_quantifiers=False)
|
|
255
262
|
# Definition could be a list for Open API 2.0
|
|
256
263
|
definition = body.definition if isinstance(body.definition, dict) else {}
|
|
257
264
|
examples = [example["value"] for example in definition.get("examples", {}).values() if "value" in example]
|
|
258
265
|
if examples:
|
|
259
266
|
schema.setdefault("examples", []).extend(examples)
|
|
260
|
-
gen = coverage.cover_schema_iter(
|
|
267
|
+
gen = coverage.cover_schema_iter(
|
|
268
|
+
coverage.CoverageContext(data_generation_methods=data_generation_methods), schema
|
|
269
|
+
)
|
|
261
270
|
value = next(gen, NOT_SET)
|
|
262
271
|
if isinstance(value, NotSet):
|
|
263
272
|
continue
|
|
@@ -268,12 +277,18 @@ def _iter_coverage_cases(
|
|
|
268
277
|
case.data_generation_method = value.data_generation_method
|
|
269
278
|
case.meta = copy(meta)
|
|
270
279
|
case.meta.description = value.description
|
|
280
|
+
case.meta.location = value.location
|
|
281
|
+
case.meta.parameter = body.media_type
|
|
282
|
+
case.meta.parameter_location = "body"
|
|
271
283
|
yield case
|
|
272
284
|
for next_value in gen:
|
|
273
285
|
case = operation.make_case(**{**template, "body": next_value.value, "media_type": body.media_type})
|
|
274
286
|
case.data_generation_method = next_value.data_generation_method
|
|
275
287
|
case.meta = copy(meta)
|
|
276
288
|
case.meta.description = next_value.description
|
|
289
|
+
case.meta.location = next_value.location
|
|
290
|
+
case.meta.parameter = body.media_type
|
|
291
|
+
case.meta.parameter_location = "body"
|
|
277
292
|
yield case
|
|
278
293
|
elif DataGenerationMethod.positive in data_generation_methods:
|
|
279
294
|
case = operation.make_case(**template)
|
|
@@ -292,7 +307,28 @@ def _iter_coverage_cases(
|
|
|
292
307
|
case.data_generation_method = value.data_generation_method
|
|
293
308
|
case.meta = copy(meta)
|
|
294
309
|
case.meta.description = value.description
|
|
310
|
+
case.meta.location = value.location
|
|
311
|
+
case.meta.parameter = name
|
|
312
|
+
case.meta.parameter_location = location
|
|
295
313
|
yield case
|
|
314
|
+
# Generate missing required parameters
|
|
315
|
+
if DataGenerationMethod.negative in data_generation_methods:
|
|
316
|
+
for parameter in operation.iter_parameters():
|
|
317
|
+
if parameter.is_required:
|
|
318
|
+
name = parameter.name
|
|
319
|
+
location = parameter.location
|
|
320
|
+
container_name = LOCATION_TO_CONTAINER[location]
|
|
321
|
+
container = template[container_name]
|
|
322
|
+
case = operation.make_case(
|
|
323
|
+
**{**template, container_name: {k: v for k, v in container.items() if k != name}}
|
|
324
|
+
)
|
|
325
|
+
case.data_generation_method = DataGenerationMethod.negative
|
|
326
|
+
case.meta = copy(meta)
|
|
327
|
+
case.meta.description = f"Missing `{name}` at {location}"
|
|
328
|
+
case.meta.location = parameter.location
|
|
329
|
+
case.meta.parameter = name
|
|
330
|
+
case.meta.parameter_location = location
|
|
331
|
+
yield case
|
|
296
332
|
|
|
297
333
|
|
|
298
334
|
def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
|
schemathesis/_patches.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""A set of performance-related patches."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def install() -> None:
|
|
7
|
+
from hypothesis.internal.reflection import is_first_param_referenced_in_function
|
|
8
|
+
from hypothesis.strategies._internal import core
|
|
9
|
+
from hypothesis_jsonschema import _from_schema, _resolve
|
|
10
|
+
|
|
11
|
+
from .internal.copy import fast_deepcopy
|
|
12
|
+
|
|
13
|
+
# This one is used a lot, and under the hood it re-parses the AST of the same function
|
|
14
|
+
def _is_first_param_referenced_in_function(f: Any) -> bool:
|
|
15
|
+
if f.__name__ == "from_object_schema" and f.__module__ == "hypothesis_jsonschema._from_schema":
|
|
16
|
+
return True
|
|
17
|
+
return is_first_param_referenced_in_function(f)
|
|
18
|
+
|
|
19
|
+
core.is_first_param_referenced_in_function = _is_first_param_referenced_in_function # type: ignore
|
|
20
|
+
_resolve.deepcopy = fast_deepcopy # type: ignore
|
|
21
|
+
_from_schema.deepcopy = fast_deepcopy # type: ignore
|
schemathesis/cli/__init__.py
CHANGED
|
@@ -36,7 +36,7 @@ from ..filters import FilterSet, expression_to_filter_function, is_deprecated
|
|
|
36
36
|
from ..fixups import ALL_FIXUPS
|
|
37
37
|
from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod
|
|
38
38
|
from ..hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope
|
|
39
|
-
from ..internal.checks import CheckConfig
|
|
39
|
+
from ..internal.checks import CheckConfig
|
|
40
40
|
from ..internal.datetime import current_datetime
|
|
41
41
|
from ..internal.output import OutputConfig
|
|
42
42
|
from ..internal.validation import file_exists
|
schemathesis/cli/cassettes.py
CHANGED
|
@@ -244,6 +244,24 @@ http_interactions:"""
|
|
|
244
244
|
write_double_quoted(stream, interaction.description)
|
|
245
245
|
else:
|
|
246
246
|
stream.write("null")
|
|
247
|
+
|
|
248
|
+
stream.write("\n location: ")
|
|
249
|
+
if interaction.location is not None:
|
|
250
|
+
write_double_quoted(stream, interaction.location)
|
|
251
|
+
else:
|
|
252
|
+
stream.write("null")
|
|
253
|
+
|
|
254
|
+
stream.write("\n parameter: ")
|
|
255
|
+
if interaction.parameter is not None:
|
|
256
|
+
write_double_quoted(stream, interaction.parameter)
|
|
257
|
+
else:
|
|
258
|
+
stream.write("null")
|
|
259
|
+
|
|
260
|
+
stream.write("\n parameter_location: ")
|
|
261
|
+
if interaction.parameter_location is not None:
|
|
262
|
+
write_double_quoted(stream, interaction.parameter_location)
|
|
263
|
+
else:
|
|
264
|
+
stream.write("null")
|
|
247
265
|
stream.write(
|
|
248
266
|
f"""
|
|
249
267
|
phase: {phase}
|
|
@@ -200,7 +200,7 @@ class SchemathesisCase(PyCollector):
|
|
|
200
200
|
kwargs["_ispytest"] = True
|
|
201
201
|
metafunc = Metafunc(definition, fixtureinfo, self.config, **kwargs)
|
|
202
202
|
methods = []
|
|
203
|
-
if hasattr(module, "pytest_generate_tests"):
|
|
203
|
+
if module is not None and hasattr(module, "pytest_generate_tests"):
|
|
204
204
|
methods.append(module.pytest_generate_tests)
|
|
205
205
|
if hasattr(cls, "pytest_generate_tests"):
|
|
206
206
|
cls = cast(Type, cls)
|
|
@@ -43,6 +43,8 @@ def add_single_example(strategy: st.SearchStrategy[T], examples: list[T]) -> Non
|
|
|
43
43
|
def example_generating_inner_function(ex: T) -> None:
|
|
44
44
|
examples.append(ex)
|
|
45
45
|
|
|
46
|
+
example_generating_inner_function._hypothesis_internal_database_key = b"" # type: ignore
|
|
47
|
+
|
|
46
48
|
if SCHEMATHESIS_BENCHMARK_SEED is not None:
|
|
47
49
|
example_generating_inner_function = seed(SCHEMATHESIS_BENCHMARK_SEED)(example_generating_inner_function)
|
|
48
50
|
|