schemathesis 3.29.1__py3-none-any.whl → 3.30.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/__init__.py +3 -3
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +1 -3
- schemathesis/_hypothesis.py +6 -0
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +1 -0
- schemathesis/_rate_limiter.py +2 -1
- schemathesis/_xml.py +1 -0
- schemathesis/auths.py +4 -2
- schemathesis/checks.py +8 -5
- schemathesis/cli/__init__.py +8 -1
- schemathesis/cli/callbacks.py +3 -4
- schemathesis/cli/cassettes.py +6 -4
- schemathesis/cli/constants.py +2 -0
- schemathesis/cli/context.py +3 -0
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +1 -1
- schemathesis/cli/options.py +1 -0
- schemathesis/cli/output/default.py +50 -22
- schemathesis/cli/output/short.py +21 -10
- schemathesis/cli/sanitization.py +1 -0
- schemathesis/code_samples.py +1 -0
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +2 -0
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +2 -1
- schemathesis/exceptions.py +40 -26
- schemathesis/experimental/__init__.py +14 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +13 -24
- schemathesis/failures.py +32 -3
- schemathesis/filters.py +2 -1
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +2 -1
- schemathesis/hooks.py +3 -1
- schemathesis/internal/copy.py +19 -3
- schemathesis/internal/deprecation.py +1 -1
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +1 -0
- schemathesis/lazy.py +3 -2
- schemathesis/loaders.py +4 -2
- schemathesis/models.py +20 -5
- schemathesis/parameters.py +1 -0
- schemathesis/runner/__init__.py +1 -1
- schemathesis/runner/events.py +21 -4
- schemathesis/runner/impl/core.py +61 -33
- schemathesis/runner/impl/solo.py +2 -1
- schemathesis/runner/impl/threadpool.py +4 -0
- schemathesis/runner/probes.py +1 -1
- schemathesis/runner/serialization.py +1 -1
- schemathesis/sanitization.py +2 -0
- schemathesis/schemas.py +1 -4
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +7 -7
- schemathesis/service/events.py +2 -1
- schemathesis/service/extensions.py +5 -5
- schemathesis/service/hosts.py +1 -0
- schemathesis/service/metadata.py +2 -1
- schemathesis/service/models.py +2 -1
- schemathesis/service/report.py +3 -3
- schemathesis/service/serialization.py +54 -23
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +1 -1
- schemathesis/specs/graphql/loaders.py +1 -1
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +7 -7
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +17 -11
- schemathesis/specs/openapi/checks.py +102 -9
- schemathesis/specs/openapi/converter.py +2 -1
- schemathesis/specs/openapi/definitions.py +2 -1
- schemathesis/specs/openapi/examples.py +7 -9
- schemathesis/specs/openapi/expressions/__init__.py +29 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +19 -18
- schemathesis/specs/openapi/expressions/nodes.py +24 -4
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/filters.py +1 -0
- schemathesis/specs/openapi/links.py +35 -7
- schemathesis/specs/openapi/loaders.py +13 -11
- schemathesis/specs/openapi/negative/__init__.py +2 -1
- schemathesis/specs/openapi/negative/mutations.py +1 -0
- schemathesis/specs/openapi/parameters.py +1 -0
- schemathesis/specs/openapi/schemas.py +27 -38
- schemathesis/specs/openapi/security.py +1 -0
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +159 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +13 -0
- schemathesis/specs/openapi/utils.py +1 -0
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +4 -2
- schemathesis/stateful/config.py +66 -0
- schemathesis/stateful/context.py +93 -0
- schemathesis/stateful/events.py +209 -0
- schemathesis/stateful/runner.py +233 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +39 -22
- schemathesis/stateful/statistic.py +20 -0
- schemathesis/stateful/validation.py +66 -0
- schemathesis/targets.py +1 -0
- schemathesis/throttling.py +23 -3
- schemathesis/transports/__init__.py +28 -10
- schemathesis/transports/auth.py +1 -0
- schemathesis/transports/content_types.py +1 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +6 -4
- schemathesis/types.py +1 -0
- schemathesis/utils.py +1 -0
- {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/METADATA +1 -1
- schemathesis-3.30.0.dist-info/RECORD +150 -0
- schemathesis/specs/openapi/stateful/links.py +0 -94
- schemathesis-3.29.1.dist-info/RECORD +0 -141
- {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/WHEEL +0 -0
- {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/licenses/LICENSE +0 -0
schemathesis/exceptions.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import enum
|
|
3
4
|
import json
|
|
4
5
|
import re
|
|
@@ -14,11 +15,13 @@ from .failures import FailureContext
|
|
|
14
15
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
16
17
|
import hypothesis.errors
|
|
17
|
-
from jsonschema import RefResolutionError, ValidationError, SchemaError as JsonSchemaError
|
|
18
|
-
from .transports.responses import GenericResponse
|
|
19
18
|
from graphql.error import GraphQLFormattedError
|
|
19
|
+
from jsonschema import RefResolutionError, ValidationError
|
|
20
|
+
from jsonschema import SchemaError as JsonSchemaError
|
|
20
21
|
from requests import RequestException
|
|
21
22
|
|
|
23
|
+
from .transports.responses import GenericResponse
|
|
24
|
+
|
|
22
25
|
|
|
23
26
|
class CheckFailed(AssertionError):
|
|
24
27
|
"""Custom error type to distinguish from arbitrary AssertionError that may happen in the dependent libraries."""
|
|
@@ -97,52 +100,60 @@ def get_grouped_exception(prefix: str, *exceptions: AssertionError) -> type[Chec
|
|
|
97
100
|
return _get_hashed_exception("GroupedException", f"{prefix}{message}")
|
|
98
101
|
|
|
99
102
|
|
|
100
|
-
def get_server_error(status_code: int) -> type[CheckFailed]:
|
|
103
|
+
def get_server_error(prefix: str, status_code: int) -> type[CheckFailed]:
|
|
101
104
|
"""Return new exception for the Internal Server Error cases."""
|
|
102
|
-
name = f"ServerError{status_code}"
|
|
105
|
+
name = f"ServerError{prefix}{status_code}"
|
|
103
106
|
return get_exception(name)
|
|
104
107
|
|
|
105
108
|
|
|
106
|
-
def get_status_code_error(status_code: int) -> type[CheckFailed]:
|
|
109
|
+
def get_status_code_error(prefix: str, status_code: int) -> type[CheckFailed]:
|
|
107
110
|
"""Return new exception for an unexpected status code."""
|
|
108
|
-
name = f"StatusCodeError{status_code}"
|
|
111
|
+
name = f"StatusCodeError{prefix}{status_code}"
|
|
109
112
|
return get_exception(name)
|
|
110
113
|
|
|
111
114
|
|
|
112
|
-
def get_response_type_error(expected: str, received: str) -> type[CheckFailed]:
|
|
115
|
+
def get_response_type_error(prefix: str, expected: str, received: str) -> type[CheckFailed]:
|
|
113
116
|
"""Return new exception for an unexpected response type."""
|
|
114
|
-
name = f"SchemaValidationError{expected}_{received}"
|
|
117
|
+
name = f"SchemaValidationError{prefix}{expected}_{received}"
|
|
115
118
|
return get_exception(name)
|
|
116
119
|
|
|
117
120
|
|
|
118
|
-
def get_malformed_media_type_error(media_type: str) -> type[CheckFailed]:
|
|
119
|
-
name = f"MalformedMediaType{media_type}"
|
|
121
|
+
def get_malformed_media_type_error(prefix: str, media_type: str) -> type[CheckFailed]:
|
|
122
|
+
name = f"MalformedMediaType{prefix}{media_type}"
|
|
120
123
|
return get_exception(name)
|
|
121
124
|
|
|
122
125
|
|
|
123
|
-
def get_missing_content_type_error() -> type[CheckFailed]:
|
|
126
|
+
def get_missing_content_type_error(prefix: str) -> type[CheckFailed]:
|
|
124
127
|
"""Return new exception for a missing Content-Type header."""
|
|
125
|
-
return get_exception("MissingContentTypeError")
|
|
128
|
+
return get_exception(f"MissingContentTypeError{prefix}")
|
|
126
129
|
|
|
127
130
|
|
|
128
|
-
def get_schema_validation_error(exception: ValidationError) -> type[CheckFailed]:
|
|
131
|
+
def get_schema_validation_error(prefix: str, exception: ValidationError) -> type[CheckFailed]:
|
|
129
132
|
"""Return new exception for schema validation error."""
|
|
130
|
-
return _get_hashed_exception("SchemaValidationError", str(exception))
|
|
133
|
+
return _get_hashed_exception(f"SchemaValidationError{prefix}", str(exception))
|
|
131
134
|
|
|
132
135
|
|
|
133
|
-
def get_response_parsing_error(exception: JSONDecodeError) -> type[CheckFailed]:
|
|
136
|
+
def get_response_parsing_error(prefix: str, exception: JSONDecodeError) -> type[CheckFailed]:
|
|
134
137
|
"""Return new exception for response parsing error."""
|
|
135
|
-
return _get_hashed_exception("ResponseParsingError", str(exception))
|
|
138
|
+
return _get_hashed_exception(f"ResponseParsingError{prefix}", str(exception))
|
|
136
139
|
|
|
137
140
|
|
|
138
|
-
def get_headers_error(message: str) -> type[CheckFailed]:
|
|
141
|
+
def get_headers_error(prefix: str, message: str) -> type[CheckFailed]:
|
|
139
142
|
"""Return new exception for missing headers."""
|
|
140
|
-
return _get_hashed_exception("MissingHeadersError", message)
|
|
143
|
+
return _get_hashed_exception(f"MissingHeadersError{prefix}", message)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_negative_rejection_error(prefix: str, status: int) -> type[CheckFailed]:
|
|
147
|
+
return _get_hashed_exception(f"AcceptedNegativeDataError{prefix}", str(status))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_use_after_free_error(free: str) -> type[CheckFailed]:
|
|
151
|
+
return _get_hashed_exception("UseAfterFreeError", free)
|
|
141
152
|
|
|
142
153
|
|
|
143
|
-
def get_timeout_error(deadline: float | int) -> type[CheckFailed]:
|
|
154
|
+
def get_timeout_error(prefix: str, deadline: float | int) -> type[CheckFailed]:
|
|
144
155
|
"""Request took too long."""
|
|
145
|
-
return _get_hashed_exception("TimeoutError", str(deadline))
|
|
156
|
+
return _get_hashed_exception(f"TimeoutError{prefix}", str(deadline))
|
|
146
157
|
|
|
147
158
|
|
|
148
159
|
def get_unexpected_graphql_response_error(type_: type) -> type[CheckFailed]:
|
|
@@ -158,7 +169,7 @@ def get_grouped_graphql_error(errors: list[GraphQLFormattedError]) -> type[Check
|
|
|
158
169
|
if "locations" in error:
|
|
159
170
|
message += ";locations:"
|
|
160
171
|
for location in sorted(error["locations"]):
|
|
161
|
-
message += f"({location['line'],location['column']})"
|
|
172
|
+
message += f"({location['line'], location['column']})"
|
|
162
173
|
if "path" in error:
|
|
163
174
|
message += ";path:"
|
|
164
175
|
for chunk in error["path"]:
|
|
@@ -541,7 +552,7 @@ def remove_ssl_line_number(text: str) -> str:
|
|
|
541
552
|
|
|
542
553
|
|
|
543
554
|
def extract_requests_exception_details(exc: RequestException) -> tuple[str, list[str]]:
|
|
544
|
-
from requests.exceptions import
|
|
555
|
+
from requests.exceptions import ChunkedEncodingError, ConnectionError, SSLError
|
|
545
556
|
from urllib3.exceptions import MaxRetryError
|
|
546
557
|
|
|
547
558
|
if isinstance(exc, SSLError):
|
|
@@ -552,11 +563,14 @@ def extract_requests_exception_details(exc: RequestException) -> tuple[str, list
|
|
|
552
563
|
message = "Connection failed"
|
|
553
564
|
inner = exc.args[0]
|
|
554
565
|
if isinstance(inner, MaxRetryError) and inner.reason is not None:
|
|
555
|
-
arg =
|
|
556
|
-
if
|
|
557
|
-
|
|
566
|
+
arg = inner.reason.args[0]
|
|
567
|
+
if isinstance(arg, str):
|
|
568
|
+
if ":" not in arg:
|
|
569
|
+
reason = arg
|
|
570
|
+
else:
|
|
571
|
+
_, reason = arg.split(":", maxsplit=1)
|
|
558
572
|
else:
|
|
559
|
-
|
|
573
|
+
reason = f"Max retries exceeded with url: {inner.url}"
|
|
560
574
|
extra = [reason.strip()]
|
|
561
575
|
else:
|
|
562
576
|
extra = [" ".join(map(str, inner.args))]
|
|
@@ -79,3 +79,17 @@ SCHEMA_ANALYSIS = GLOBAL_EXPERIMENTS.create_experiment(
|
|
|
79
79
|
description="Analyzing API schemas via Schemathesis.io",
|
|
80
80
|
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2056",
|
|
81
81
|
)
|
|
82
|
+
STATEFUL_TEST_RUNNER = GLOBAL_EXPERIMENTS.create_experiment(
|
|
83
|
+
name="stateful-test-runner",
|
|
84
|
+
verbose_name="New Stateful Test Runner",
|
|
85
|
+
env_var="STATEFUL_TEST_RUNNER",
|
|
86
|
+
description="State machine-based runner for stateful tests in CLI",
|
|
87
|
+
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2262",
|
|
88
|
+
)
|
|
89
|
+
STATEFUL_ONLY = GLOBAL_EXPERIMENTS.create_experiment(
|
|
90
|
+
name="stateful-only",
|
|
91
|
+
verbose_name="Stateful Only",
|
|
92
|
+
env_var="STATEFUL_ONLY",
|
|
93
|
+
description="Run only stateful tests",
|
|
94
|
+
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2262",
|
|
95
|
+
)
|
schemathesis/extra/_aiohttp.py
CHANGED
schemathesis/extra/_server.py
CHANGED
|
@@ -3,19 +3,18 @@ from __future__ import annotations
|
|
|
3
3
|
import unittest
|
|
4
4
|
from contextlib import contextmanager
|
|
5
5
|
from functools import partial
|
|
6
|
-
from typing import Any, Callable, Generator, Type,
|
|
6
|
+
from typing import Any, Callable, Generator, Type, cast
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
from _pytest import fixtures, nodes
|
|
10
10
|
from _pytest.config import hookimpl
|
|
11
11
|
from _pytest.fixtures import FuncFixtureInfo
|
|
12
|
-
from _pytest.nodes import Node
|
|
13
12
|
from _pytest.python import Class, Function, FunctionDefinition, Metafunc, Module, PyCollector
|
|
14
13
|
from hypothesis import reporting
|
|
15
14
|
from hypothesis.errors import InvalidArgument, Unsatisfiable
|
|
16
15
|
from jsonschema.exceptions import SchemaError
|
|
17
16
|
|
|
18
|
-
from .._dependency_versions import IS_PYTEST_ABOVE_7, IS_PYTEST_ABOVE_8
|
|
17
|
+
from .._dependency_versions import IS_PYTEST_ABOVE_7, IS_PYTEST_ABOVE_8
|
|
19
18
|
from .._override import get_override_from_mark
|
|
20
19
|
from ..constants import (
|
|
21
20
|
GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE,
|
|
@@ -43,14 +42,6 @@ from ..utils import (
|
|
|
43
42
|
validate_given_args,
|
|
44
43
|
)
|
|
45
44
|
|
|
46
|
-
T = TypeVar("T", bound=Node)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def create(cls: type[T], *args: Any, **kwargs: Any) -> T:
|
|
50
|
-
if IS_PYTEST_ABOVE_54:
|
|
51
|
-
return cls.from_parent(*args, **kwargs) # type: ignore
|
|
52
|
-
return cls(*args, **kwargs)
|
|
53
|
-
|
|
54
45
|
|
|
55
46
|
class SchemathesisFunction(Function):
|
|
56
47
|
def __init__(
|
|
@@ -155,7 +146,9 @@ class SchemathesisCase(PyCollector):
|
|
|
155
146
|
name += f"[{error.full_path}]"
|
|
156
147
|
|
|
157
148
|
cls = self._get_class_parent()
|
|
158
|
-
definition: FunctionDefinition =
|
|
149
|
+
definition: FunctionDefinition = FunctionDefinition.from_parent(
|
|
150
|
+
name=self.name, parent=self.parent, callobj=funcobj
|
|
151
|
+
)
|
|
159
152
|
fixturemanager = self.session._fixturemanager
|
|
160
153
|
fixtureinfo = fixturemanager.getfixtureinfo(definition, funcobj, cls)
|
|
161
154
|
|
|
@@ -166,8 +159,7 @@ class SchemathesisCase(PyCollector):
|
|
|
166
159
|
funcobj = partial(funcobj, self.parent.obj)
|
|
167
160
|
|
|
168
161
|
if not metafunc._calls:
|
|
169
|
-
yield
|
|
170
|
-
SchemathesisFunction,
|
|
162
|
+
yield SchemathesisFunction.from_parent(
|
|
171
163
|
name=name,
|
|
172
164
|
parent=self.parent,
|
|
173
165
|
callobj=funcobj,
|
|
@@ -181,10 +173,9 @@ class SchemathesisCase(PyCollector):
|
|
|
181
173
|
fixtureinfo.prune_dependency_tree()
|
|
182
174
|
for callspec in metafunc._calls:
|
|
183
175
|
subname = f"{name}[{callspec.id}]"
|
|
184
|
-
yield
|
|
185
|
-
|
|
176
|
+
yield SchemathesisFunction.from_parent(
|
|
177
|
+
self.parent,
|
|
186
178
|
name=subname,
|
|
187
|
-
parent=self.parent,
|
|
188
179
|
callspec=callspec,
|
|
189
180
|
callobj=funcobj,
|
|
190
181
|
fixtureinfo=fixtureinfo,
|
|
@@ -236,7 +227,7 @@ def pytest_pycollect_makeitem(collector: nodes.Collector, name: str, obj: Any) -
|
|
|
236
227
|
"""Switch to a different collector if the test is parametrized marked by schemathesis."""
|
|
237
228
|
outcome = yield
|
|
238
229
|
if is_schemathesis_test(obj):
|
|
239
|
-
outcome.force_result(
|
|
230
|
+
outcome.force_result(SchemathesisCase.from_parent(collector, test_function=obj, name=name))
|
|
240
231
|
else:
|
|
241
232
|
outcome.get_result()
|
|
242
233
|
|
|
@@ -261,7 +252,7 @@ def skip_unnecessary_hypothesis_output() -> Generator:
|
|
|
261
252
|
yield
|
|
262
253
|
|
|
263
254
|
|
|
264
|
-
@hookimpl(
|
|
255
|
+
@hookimpl(wrapper=True)
|
|
265
256
|
def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
|
266
257
|
"""It is possible to have a Hypothesis exception in runtime.
|
|
267
258
|
|
|
@@ -278,10 +269,9 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
|
|
278
269
|
|
|
279
270
|
__tracebackhide__ = True
|
|
280
271
|
if isinstance(pyfuncitem, SchemathesisFunction):
|
|
281
|
-
with skip_unnecessary_hypothesis_output():
|
|
282
|
-
outcome = yield
|
|
283
272
|
try:
|
|
284
|
-
|
|
273
|
+
with skip_unnecessary_hypothesis_output():
|
|
274
|
+
yield
|
|
285
275
|
except InvalidArgument as exc:
|
|
286
276
|
if "Inconsistent args" in str(exc) and "@example()" in str(exc):
|
|
287
277
|
raise UsageError(GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE) from None
|
|
@@ -316,5 +306,4 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
|
|
316
306
|
if invalid_headers is not None:
|
|
317
307
|
raise InvalidHeadersExample.from_headers(invalid_headers) from None
|
|
318
308
|
else:
|
|
319
|
-
|
|
320
|
-
outcome.get_result()
|
|
309
|
+
yield
|
schemathesis/failures.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import textwrap
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from json import JSONDecodeError
|
|
5
|
-
from typing import
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
6
7
|
|
|
7
8
|
if TYPE_CHECKING:
|
|
8
9
|
from graphql.error import GraphQLFormattedError
|
|
@@ -46,11 +47,19 @@ class ValidationErrorContext(FailureContext):
|
|
|
46
47
|
|
|
47
48
|
schema = textwrap.indent(truncated_json(exc.schema, max_lines=20), prefix=" ")
|
|
48
49
|
value = textwrap.indent(truncated_json(exc.instance, max_lines=20), prefix=" ")
|
|
49
|
-
|
|
50
|
+
schema_path = list(exc.absolute_schema_path)
|
|
51
|
+
if len(schema_path) > 1:
|
|
52
|
+
# Exclude the last segment, which is already in the schema
|
|
53
|
+
schema_title = "Schema at "
|
|
54
|
+
for segment in schema_path[:-1]:
|
|
55
|
+
schema_title += f"/{segment}"
|
|
56
|
+
else:
|
|
57
|
+
schema_title = "Schema"
|
|
58
|
+
message = f"{exc.message}\n\n{schema_title}:\n\n{schema}\n\nValue:\n\n{value}"
|
|
50
59
|
return cls(
|
|
51
60
|
message=message,
|
|
52
61
|
validation_message=exc.message,
|
|
53
|
-
schema_path=
|
|
62
|
+
schema_path=schema_path,
|
|
54
63
|
schema=exc.schema,
|
|
55
64
|
instance_path=list(exc.absolute_path),
|
|
56
65
|
instance=exc.instance,
|
|
@@ -117,6 +126,26 @@ class UndefinedContentType(FailureContext):
|
|
|
117
126
|
type: str = "undefined_content_type"
|
|
118
127
|
|
|
119
128
|
|
|
129
|
+
@dataclass(repr=False)
|
|
130
|
+
class AcceptedNegativeData(FailureContext):
|
|
131
|
+
"""Response with negative data was accepted."""
|
|
132
|
+
|
|
133
|
+
message: str
|
|
134
|
+
title: str = "Accepted negative data"
|
|
135
|
+
type: str = "accepted_negative_data"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass(repr=False)
|
|
139
|
+
class UseAfterFree(FailureContext):
|
|
140
|
+
"""Resource was used after a successful DELETE operation on it."""
|
|
141
|
+
|
|
142
|
+
message: str
|
|
143
|
+
free: str
|
|
144
|
+
usage: str
|
|
145
|
+
title: str = "Use after free"
|
|
146
|
+
type: str = "use_after_free"
|
|
147
|
+
|
|
148
|
+
|
|
120
149
|
@dataclass(repr=False)
|
|
121
150
|
class UndefinedStatusCode(FailureContext):
|
|
122
151
|
"""Response has a status code that is not defined in the schema."""
|
schemathesis/filters.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"""Filtering system that allows users to filter API operations based on certain criteria."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
|
|
4
5
|
import re
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
from functools import partial
|
|
7
8
|
from types import SimpleNamespace
|
|
8
|
-
from typing import TYPE_CHECKING, Callable, List,
|
|
9
|
+
from typing import TYPE_CHECKING, Callable, List, Protocol, Union
|
|
9
10
|
|
|
10
11
|
from .exceptions import UsageError
|
|
11
12
|
|
schemathesis/fixups/__init__.py
CHANGED
schemathesis/fixups/fast_api.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from typing import Any
|
|
3
4
|
|
|
4
|
-
from ..hooks import HookContext
|
|
5
|
+
from ..hooks import HookContext, register, unregister
|
|
5
6
|
from ..hooks import is_installed as global_is_installed
|
|
6
|
-
from ..hooks import register, unregister
|
|
7
7
|
from ..internal.jsonschema import traverse_schema
|
|
8
8
|
|
|
9
9
|
|
schemathesis/fixups/utf8_bom.py
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING
|
|
2
2
|
|
|
3
3
|
from ..constants import BOM_MARK
|
|
4
|
-
from ..hooks import HookContext
|
|
4
|
+
from ..hooks import HookContext, register, unregister
|
|
5
5
|
from ..hooks import is_installed as global_is_installed
|
|
6
|
-
from ..hooks import register, unregister
|
|
7
6
|
|
|
8
7
|
if TYPE_CHECKING:
|
|
9
8
|
from ..models import Case
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import random
|
|
3
4
|
from dataclasses import dataclass, field
|
|
4
5
|
from enum import Enum
|
|
5
|
-
from typing import
|
|
6
|
+
from typing import TYPE_CHECKING, Iterable, Union
|
|
6
7
|
|
|
7
8
|
if TYPE_CHECKING:
|
|
8
9
|
from hypothesis.strategies import SearchStrategy
|
schemathesis/hooks.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import inspect
|
|
3
4
|
from collections import defaultdict
|
|
4
5
|
from copy import deepcopy
|
|
@@ -7,11 +8,12 @@ from enum import Enum, unique
|
|
|
7
8
|
from functools import partial
|
|
8
9
|
from typing import TYPE_CHECKING, Any, Callable, ClassVar, DefaultDict, cast
|
|
9
10
|
|
|
10
|
-
from .types import GenericTest
|
|
11
11
|
from .internal.deprecation import deprecated_property
|
|
12
|
+
from .types import GenericTest
|
|
12
13
|
|
|
13
14
|
if TYPE_CHECKING:
|
|
14
15
|
from hypothesis import strategies as st
|
|
16
|
+
|
|
15
17
|
from .models import APIOperation, Case
|
|
16
18
|
from .schemas import BaseSchema
|
|
17
19
|
from .transports.responses import GenericResponse
|
schemathesis/internal/copy.py
CHANGED
|
@@ -2,12 +2,28 @@ from typing import Any
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
def fast_deepcopy(value: Any) -> Any:
|
|
5
|
-
"""A specialized version of `deepcopy` that copies only `dict` and `list
|
|
5
|
+
"""A specialized version of `deepcopy` that copies only `dict` and `list` and does unrolling.
|
|
6
6
|
|
|
7
7
|
It is on average 3x faster than `deepcopy` and given the amount of calls, it is an important optimization.
|
|
8
8
|
"""
|
|
9
9
|
if isinstance(value, dict):
|
|
10
|
-
return {
|
|
10
|
+
return {
|
|
11
|
+
k1: (
|
|
12
|
+
{k2: fast_deepcopy(v2) for k2, v2 in v1.items()}
|
|
13
|
+
if isinstance(v1, dict)
|
|
14
|
+
else [fast_deepcopy(v2) for v2 in v1]
|
|
15
|
+
if isinstance(v1, list)
|
|
16
|
+
else v1
|
|
17
|
+
)
|
|
18
|
+
for k1, v1 in value.items()
|
|
19
|
+
}
|
|
11
20
|
if isinstance(value, list):
|
|
12
|
-
return [
|
|
21
|
+
return [
|
|
22
|
+
{k2: fast_deepcopy(v2) for k2, v2 in v1.items()}
|
|
23
|
+
if isinstance(v1, dict)
|
|
24
|
+
else [fast_deepcopy(v2) for v2 in v1]
|
|
25
|
+
if isinstance(v1, list)
|
|
26
|
+
else v1
|
|
27
|
+
for v1 in value
|
|
28
|
+
]
|
|
13
29
|
return value
|
schemathesis/internal/result.py
CHANGED
schemathesis/lazy.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from dataclasses import dataclass, field
|
|
3
4
|
from inspect import signature
|
|
4
5
|
from typing import Any, Callable, Generator
|
|
@@ -13,12 +14,12 @@ from pyrate_limiter import Limiter
|
|
|
13
14
|
from pytest_subtests import SubTests, nullcontext
|
|
14
15
|
|
|
15
16
|
from ._compat import MultipleFailures, get_interesting_origin
|
|
16
|
-
from ._override import check_no_override_mark,
|
|
17
|
+
from ._override import CaseOverride, check_no_override_mark, get_override_from_mark, set_override_mark
|
|
17
18
|
from .auths import AuthStorage
|
|
18
19
|
from .code_samples import CodeSampleStyle
|
|
19
20
|
from .constants import FLAKY_FAILURE_MESSAGE, NOT_SET
|
|
20
|
-
from .generation import DataGenerationMethodInput, GenerationConfig
|
|
21
21
|
from .exceptions import CheckFailed, OperationSchemaError, SkipTest, get_grouped_exception
|
|
22
|
+
from .generation import DataGenerationMethodInput, GenerationConfig
|
|
22
23
|
from .hooks import HookDispatcher, HookScope
|
|
23
24
|
from .internal.result import Ok
|
|
24
25
|
from .models import APIOperation
|
schemathesis/loaders.py
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import re
|
|
3
4
|
import sys
|
|
4
5
|
from functools import lru_cache
|
|
5
|
-
from typing import
|
|
6
|
+
from typing import TYPE_CHECKING, Any, BinaryIO, Callable, TextIO, TypeVar
|
|
6
7
|
|
|
7
8
|
from .exceptions import SchemaError, SchemaErrorType, extract_requests_exception_details
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
10
|
-
from .transports.responses import GenericResponse
|
|
11
11
|
import yaml
|
|
12
12
|
|
|
13
|
+
from .transports.responses import GenericResponse
|
|
14
|
+
|
|
13
15
|
R = TypeVar("R", bound="GenericResponse")
|
|
14
16
|
|
|
15
17
|
|
schemathesis/models.py
CHANGED
|
@@ -42,6 +42,7 @@ from .exceptions import (
|
|
|
42
42
|
OperationSchemaError,
|
|
43
43
|
SerializationNotPossible,
|
|
44
44
|
SkipTest,
|
|
45
|
+
UsageError,
|
|
45
46
|
deduplicate_failed_checks,
|
|
46
47
|
get_grouped_exception,
|
|
47
48
|
maybe_set_assertion_message,
|
|
@@ -54,7 +55,7 @@ from .internal.deprecation import deprecated_function, deprecated_property
|
|
|
54
55
|
from .parameters import Parameter, ParameterSet, PayloadAlternatives
|
|
55
56
|
from .sanitization import sanitize_request, sanitize_response
|
|
56
57
|
from .serializers import Serializer
|
|
57
|
-
from .transports import ASGITransport, RequestsTransport, WSGITransport,
|
|
58
|
+
from .transports import ASGITransport, RequestsTransport, WSGITransport, deserialize_payload, serialize_payload
|
|
58
59
|
from .types import Body, Cookies, FormData, Headers, NotSet, PathParameters, Query
|
|
59
60
|
|
|
60
61
|
if TYPE_CHECKING:
|
|
@@ -271,13 +272,14 @@ class Case:
|
|
|
271
272
|
final_headers.setdefault(SCHEMATHESIS_TEST_CASE_HEADER, self.id)
|
|
272
273
|
return final_headers
|
|
273
274
|
|
|
274
|
-
def _get_serializer(self) -> Serializer | None:
|
|
275
|
+
def _get_serializer(self, media_type: str | None = None) -> Serializer | None:
|
|
275
276
|
"""Get a serializer for the payload, if there is any."""
|
|
276
|
-
|
|
277
|
-
|
|
277
|
+
input_media_type = media_type or self.media_type
|
|
278
|
+
if input_media_type is not None:
|
|
279
|
+
media_type = serializers.get_first_matching_media_type(input_media_type)
|
|
278
280
|
if media_type is None:
|
|
279
281
|
# This media type is set manually. Otherwise, it should have been rejected during the data generation
|
|
280
|
-
raise SerializationNotPossible.for_media_type(
|
|
282
|
+
raise SerializationNotPossible.for_media_type(input_media_type)
|
|
281
283
|
# SAFETY: It is safe to assume that serializer will be found, because `media_type` returned above
|
|
282
284
|
# is registered. This intentionally ignores cases with concurrent serializers registry modification.
|
|
283
285
|
cls = cast(Type[serializers.Serializer], serializers.get(media_type))
|
|
@@ -694,6 +696,19 @@ class APIOperation(Generic[P, C]):
|
|
|
694
696
|
def get_request_payload_content_types(self) -> list[str]:
|
|
695
697
|
return self.schema.get_request_payload_content_types(self)
|
|
696
698
|
|
|
699
|
+
def _get_default_media_type(self) -> str:
|
|
700
|
+
# If the user wants to send payload, then there should be a media type, otherwise the payload is ignored
|
|
701
|
+
media_types = self.get_request_payload_content_types()
|
|
702
|
+
if len(media_types) == 1:
|
|
703
|
+
# The only available option
|
|
704
|
+
return media_types[0]
|
|
705
|
+
media_types_repr = ", ".join(media_types)
|
|
706
|
+
raise UsageError(
|
|
707
|
+
"Can not detect appropriate media type. "
|
|
708
|
+
"You can either specify one of the defined media types "
|
|
709
|
+
f"or pass any other media type available for serialization. Defined media types: {media_types_repr}"
|
|
710
|
+
)
|
|
711
|
+
|
|
697
712
|
def partial_deepcopy(self) -> APIOperation:
|
|
698
713
|
return self.__class__(
|
|
699
714
|
path=self.path, # string, immutable
|
schemathesis/parameters.py
CHANGED
schemathesis/runner/__init__.py
CHANGED
|
@@ -28,10 +28,10 @@ if TYPE_CHECKING:
|
|
|
28
28
|
|
|
29
29
|
from ..models import CheckFunction
|
|
30
30
|
from ..schemas import BaseSchema
|
|
31
|
+
from ..service.client import ServiceClient
|
|
31
32
|
from ..stateful import Stateful
|
|
32
33
|
from . import events
|
|
33
34
|
from .impl import BaseRunner
|
|
34
|
-
from ..service.client import ServiceClient
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
@deprecated_function(removed_in="4.0", replacement="schemathesis.runner.from_schema")
|
schemathesis/runner/events.py
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import enum
|
|
3
4
|
import threading
|
|
4
5
|
import time
|
|
5
6
|
from dataclasses import asdict, dataclass, field
|
|
6
|
-
from typing import
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
7
8
|
|
|
9
|
+
from ..exceptions import RuntimeErrorType, SchemaError, SchemaErrorType, format_exception
|
|
10
|
+
from ..generation import DataGenerationMethod
|
|
8
11
|
from ..internal.datetime import current_datetime
|
|
9
12
|
from ..internal.result import Result
|
|
10
|
-
from ..generation import DataGenerationMethod
|
|
11
|
-
from ..exceptions import SchemaError, SchemaErrorType, format_exception, RuntimeErrorType
|
|
12
13
|
from .serialization import SerializedError, SerializedTestResult
|
|
13
14
|
|
|
14
|
-
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
from ..models import APIOperation, Status, TestResult, TestResultSet
|
|
17
17
|
from ..schemas import BaseSchema
|
|
18
18
|
from ..service.models import AnalysisResult
|
|
19
|
+
from ..stateful import events
|
|
19
20
|
from . import probes
|
|
20
21
|
|
|
21
22
|
|
|
@@ -287,6 +288,22 @@ class InternalError(ExecutionEvent):
|
|
|
287
288
|
)
|
|
288
289
|
|
|
289
290
|
|
|
291
|
+
@dataclass
|
|
292
|
+
class StatefulEvent(ExecutionEvent):
|
|
293
|
+
"""Represents an event originating from the state machine runner."""
|
|
294
|
+
|
|
295
|
+
data: events.StatefulEvent
|
|
296
|
+
|
|
297
|
+
__slots__ = ("data",)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@dataclass
|
|
301
|
+
class AfterStatefulExecution(ExecutionEvent):
|
|
302
|
+
"""Happens after the stateful test run."""
|
|
303
|
+
|
|
304
|
+
result: SerializedTestResult
|
|
305
|
+
|
|
306
|
+
|
|
290
307
|
@dataclass
|
|
291
308
|
class Finished(ExecutionEvent):
|
|
292
309
|
"""The final event of the run.
|