schemathesis 3.21.2__py3-none-any.whl → 3.22.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/__init__.py +1 -1
- schemathesis/_compat.py +2 -18
- schemathesis/_dependency_versions.py +1 -6
- schemathesis/_hypothesis.py +15 -12
- schemathesis/_lazy_import.py +3 -2
- schemathesis/_xml.py +12 -11
- schemathesis/auths.py +88 -81
- schemathesis/checks.py +4 -4
- schemathesis/cli/__init__.py +202 -171
- schemathesis/cli/callbacks.py +29 -32
- schemathesis/cli/cassettes.py +25 -25
- schemathesis/cli/context.py +18 -12
- schemathesis/cli/junitxml.py +2 -2
- schemathesis/cli/options.py +10 -11
- schemathesis/cli/output/default.py +64 -34
- schemathesis/code_samples.py +10 -10
- schemathesis/constants.py +1 -1
- schemathesis/contrib/unique_data.py +2 -2
- schemathesis/exceptions.py +55 -42
- schemathesis/extra/_aiohttp.py +2 -2
- schemathesis/extra/_flask.py +2 -2
- schemathesis/extra/_server.py +3 -2
- schemathesis/extra/pytest_plugin.py +10 -10
- schemathesis/failures.py +16 -16
- schemathesis/filters.py +40 -41
- schemathesis/fixups/__init__.py +4 -3
- schemathesis/fixups/fast_api.py +5 -4
- schemathesis/generation/__init__.py +16 -4
- schemathesis/hooks.py +25 -25
- schemathesis/internal/jsonschema.py +4 -3
- schemathesis/internal/transformation.py +3 -2
- schemathesis/lazy.py +39 -31
- schemathesis/loaders.py +8 -8
- schemathesis/models.py +128 -126
- schemathesis/parameters.py +6 -5
- schemathesis/runner/__init__.py +107 -81
- schemathesis/runner/events.py +37 -26
- schemathesis/runner/impl/core.py +86 -81
- schemathesis/runner/impl/solo.py +19 -15
- schemathesis/runner/impl/threadpool.py +40 -22
- schemathesis/runner/serialization.py +67 -40
- schemathesis/sanitization.py +18 -20
- schemathesis/schemas.py +83 -72
- schemathesis/serializers.py +39 -30
- schemathesis/service/ci.py +20 -21
- schemathesis/service/client.py +29 -9
- schemathesis/service/constants.py +1 -0
- schemathesis/service/events.py +2 -2
- schemathesis/service/hosts.py +8 -7
- schemathesis/service/metadata.py +5 -0
- schemathesis/service/models.py +22 -4
- schemathesis/service/report.py +15 -15
- schemathesis/service/serialization.py +23 -27
- schemathesis/service/usage.py +8 -7
- schemathesis/specs/graphql/loaders.py +31 -24
- schemathesis/specs/graphql/nodes.py +3 -2
- schemathesis/specs/graphql/scalars.py +26 -2
- schemathesis/specs/graphql/schemas.py +38 -34
- schemathesis/specs/openapi/_hypothesis.py +62 -44
- schemathesis/specs/openapi/checks.py +10 -10
- schemathesis/specs/openapi/converter.py +10 -9
- schemathesis/specs/openapi/definitions.py +2 -2
- schemathesis/specs/openapi/examples.py +22 -21
- schemathesis/specs/openapi/expressions/nodes.py +5 -4
- schemathesis/specs/openapi/expressions/parser.py +7 -6
- schemathesis/specs/openapi/filters.py +6 -6
- schemathesis/specs/openapi/formats.py +2 -2
- schemathesis/specs/openapi/links.py +19 -21
- schemathesis/specs/openapi/loaders.py +133 -78
- schemathesis/specs/openapi/negative/__init__.py +16 -11
- schemathesis/specs/openapi/negative/mutations.py +11 -10
- schemathesis/specs/openapi/parameters.py +20 -19
- schemathesis/specs/openapi/references.py +21 -20
- schemathesis/specs/openapi/schemas.py +97 -84
- schemathesis/specs/openapi/security.py +25 -24
- schemathesis/specs/openapi/serialization.py +20 -23
- schemathesis/specs/openapi/stateful/__init__.py +12 -11
- schemathesis/specs/openapi/stateful/links.py +7 -7
- schemathesis/specs/openapi/utils.py +4 -3
- schemathesis/specs/openapi/validation.py +3 -2
- schemathesis/stateful/__init__.py +15 -16
- schemathesis/stateful/state_machine.py +9 -9
- schemathesis/targets.py +3 -3
- schemathesis/throttling.py +2 -2
- schemathesis/transports/auth.py +2 -2
- schemathesis/transports/content_types.py +5 -0
- schemathesis/transports/headers.py +3 -2
- schemathesis/transports/responses.py +1 -1
- schemathesis/utils.py +7 -10
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/METADATA +12 -13
- schemathesis-3.22.1.dist-info/RECORD +130 -0
- schemathesis-3.21.2.dist-info/RECORD +0 -130
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import json
|
|
2
|
-
from typing import Any, Callable, Dict, Generator, List
|
|
3
|
+
from typing import Any, Callable, Dict, Generator, List
|
|
3
4
|
|
|
4
5
|
from ...utils import compose
|
|
5
6
|
|
|
@@ -10,11 +11,11 @@ MapFunction = Callable[[Generated], Generated]
|
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def make_serializer(
|
|
13
|
-
func: Callable[[DefinitionList], Generator[
|
|
14
|
-
) -> Callable[[DefinitionList],
|
|
14
|
+
func: Callable[[DefinitionList], Generator[Callable | None, None, None]]
|
|
15
|
+
) -> Callable[[DefinitionList], Callable | None]:
|
|
15
16
|
"""A maker function to avoid code duplication."""
|
|
16
17
|
|
|
17
|
-
def _wrapper(definitions: DefinitionList) ->
|
|
18
|
+
def _wrapper(definitions: DefinitionList) -> Callable | None:
|
|
18
19
|
conversions = list(func(definitions))
|
|
19
20
|
if conversions:
|
|
20
21
|
return compose(*[conv for conv in conversions if conv is not None])
|
|
@@ -23,7 +24,7 @@ def make_serializer(
|
|
|
23
24
|
return _wrapper
|
|
24
25
|
|
|
25
26
|
|
|
26
|
-
def _serialize_openapi3(definitions: DefinitionList) -> Generator[
|
|
27
|
+
def _serialize_openapi3(definitions: DefinitionList) -> Generator[Callable | None, None, None]:
|
|
27
28
|
"""Different collection styles for Open API 3.0."""
|
|
28
29
|
for definition in definitions:
|
|
29
30
|
name = definition["name"]
|
|
@@ -49,8 +50,8 @@ def _serialize_openapi3(definitions: DefinitionList) -> Generator[Optional[Calla
|
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
def _serialize_path_openapi3(
|
|
52
|
-
name: str, type_: str, style:
|
|
53
|
-
) -> Generator[
|
|
53
|
+
name: str, type_: str, style: str | None, explode: bool | None
|
|
54
|
+
) -> Generator[Callable | None, None, None]:
|
|
54
55
|
if style == "simple":
|
|
55
56
|
if type_ == "object":
|
|
56
57
|
if explode is False:
|
|
@@ -76,8 +77,8 @@ def _serialize_path_openapi3(
|
|
|
76
77
|
|
|
77
78
|
|
|
78
79
|
def _serialize_query_openapi3(
|
|
79
|
-
name: str, type_: str, style:
|
|
80
|
-
) -> Generator[
|
|
80
|
+
name: str, type_: str, style: str | None, explode: bool | None
|
|
81
|
+
) -> Generator[Callable | None, None, None]:
|
|
81
82
|
if type_ == "object":
|
|
82
83
|
if style == "deepObject":
|
|
83
84
|
yield deep_object(name)
|
|
@@ -95,9 +96,7 @@ def _serialize_query_openapi3(
|
|
|
95
96
|
yield delimited(name, delimiter=",")
|
|
96
97
|
|
|
97
98
|
|
|
98
|
-
def _serialize_header_openapi3(
|
|
99
|
-
name: str, type_: str, explode: Optional[bool]
|
|
100
|
-
) -> Generator[Optional[Callable], None, None]:
|
|
99
|
+
def _serialize_header_openapi3(name: str, type_: str, explode: bool | None) -> Generator[Callable | None, None, None]:
|
|
101
100
|
# Headers should be coerced to a string so we can check it for validity later
|
|
102
101
|
yield to_string(name)
|
|
103
102
|
# Header parameters always use the "simple" style, that is, comma-separated values
|
|
@@ -110,9 +109,7 @@ def _serialize_header_openapi3(
|
|
|
110
109
|
yield delimited_object(name)
|
|
111
110
|
|
|
112
111
|
|
|
113
|
-
def _serialize_cookie_openapi3(
|
|
114
|
-
name: str, type_: str, explode: Optional[bool]
|
|
115
|
-
) -> Generator[Optional[Callable], None, None]:
|
|
112
|
+
def _serialize_cookie_openapi3(name: str, type_: str, explode: bool | None) -> Generator[Callable | None, None, None]:
|
|
116
113
|
# Cookies should be coerced to a string so we can check it for validity later
|
|
117
114
|
yield to_string(name)
|
|
118
115
|
# Cookie parameters always use the "form" style
|
|
@@ -129,7 +126,7 @@ def _serialize_cookie_openapi3(
|
|
|
129
126
|
yield comma_delimited_object(name)
|
|
130
127
|
|
|
131
128
|
|
|
132
|
-
def _serialize_swagger2(definitions: DefinitionList) -> Generator[
|
|
129
|
+
def _serialize_swagger2(definitions: DefinitionList) -> Generator[Callable | None, None, None]:
|
|
133
130
|
"""Different collection formats for Open API 2.0."""
|
|
134
131
|
for definition in definitions:
|
|
135
132
|
name = definition["name"]
|
|
@@ -165,11 +162,11 @@ def conversion(func: Callable[..., None]) -> Callable:
|
|
|
165
162
|
return _wrapper
|
|
166
163
|
|
|
167
164
|
|
|
168
|
-
def make_delimited(data:
|
|
165
|
+
def make_delimited(data: dict[str, Any] | None, delimiter: str = ",") -> str:
|
|
169
166
|
return delimiter.join(f"{key}={value}" for key, value in force_dict(data or {}).items())
|
|
170
167
|
|
|
171
168
|
|
|
172
|
-
def force_iterable(value: Any) ->
|
|
169
|
+
def force_iterable(value: Any) -> list | tuple:
|
|
173
170
|
"""Converts the value to a list or a tuple.
|
|
174
171
|
|
|
175
172
|
Only relevant for negative test scenarios where the original types might be changed.
|
|
@@ -179,7 +176,7 @@ def force_iterable(value: Any) -> Union[List, Tuple]:
|
|
|
179
176
|
return [value]
|
|
180
177
|
|
|
181
178
|
|
|
182
|
-
def force_dict(value: Any) ->
|
|
179
|
+
def force_dict(value: Any) -> dict:
|
|
183
180
|
"""Converts the value to a dictionary.
|
|
184
181
|
|
|
185
182
|
Only relevant for negative test scenarios where the original types might be changed.
|
|
@@ -247,7 +244,7 @@ def label_primitive(item: Generated, name: str) -> None:
|
|
|
247
244
|
|
|
248
245
|
|
|
249
246
|
@conversion
|
|
250
|
-
def label_array(item: Generated, name: str, explode:
|
|
247
|
+
def label_array(item: Generated, name: str, explode: bool | None) -> None:
|
|
251
248
|
"""Serialize an array with the `label` style.
|
|
252
249
|
|
|
253
250
|
Explode=True
|
|
@@ -270,7 +267,7 @@ def label_array(item: Generated, name: str, explode: Optional[bool]) -> None:
|
|
|
270
267
|
|
|
271
268
|
|
|
272
269
|
@conversion
|
|
273
|
-
def label_object(item: Generated, name: str, explode:
|
|
270
|
+
def label_object(item: Generated, name: str, explode: bool | None) -> None:
|
|
274
271
|
"""Serialize an object with the `label` style.
|
|
275
272
|
|
|
276
273
|
Explode=True
|
|
@@ -306,7 +303,7 @@ def matrix_primitive(item: Generated, name: str) -> None:
|
|
|
306
303
|
|
|
307
304
|
|
|
308
305
|
@conversion
|
|
309
|
-
def matrix_array(item: Generated, name: str, explode:
|
|
306
|
+
def matrix_array(item: Generated, name: str, explode: bool | None) -> None:
|
|
310
307
|
"""Serialize an array with the `matrix` style.
|
|
311
308
|
|
|
312
309
|
Explode=True
|
|
@@ -328,7 +325,7 @@ def matrix_array(item: Generated, name: str, explode: Optional[bool]) -> None:
|
|
|
328
325
|
|
|
329
326
|
|
|
330
327
|
@conversion
|
|
331
|
-
def matrix_object(item: Generated, name: str, explode:
|
|
328
|
+
def matrix_object(item: Generated, name: str, explode: bool | None) -> None:
|
|
332
329
|
"""Serialize an object with the `matrix` style.
|
|
333
330
|
|
|
334
331
|
Explode=True
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
from collections import defaultdict
|
|
2
|
-
from typing import TYPE_CHECKING, Any,
|
|
3
|
+
from typing import TYPE_CHECKING, Any, List, cast
|
|
3
4
|
|
|
4
5
|
from hypothesis import strategies as st
|
|
5
6
|
from hypothesis.stateful import Bundle, Rule, precondition, rule
|
|
@@ -18,13 +19,13 @@ if TYPE_CHECKING:
|
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class OpenAPIStateMachine(APIStateMachine):
|
|
21
|
-
def transform(self, result: StepResult, direction: Direction, case:
|
|
22
|
+
def transform(self, result: StepResult, direction: Direction, case: Case) -> Case:
|
|
22
23
|
context = expressions.ExpressionContext(case=result.case, response=result.response)
|
|
23
24
|
direction.set_data(case, elapsed=result.elapsed, context=context)
|
|
24
25
|
return case
|
|
25
26
|
|
|
26
27
|
|
|
27
|
-
def create_state_machine(schema:
|
|
28
|
+
def create_state_machine(schema: BaseOpenAPISchema) -> type[APIStateMachine]:
|
|
28
29
|
"""Create a state machine class.
|
|
29
30
|
|
|
30
31
|
It aims to avoid making calls that are not likely to lead to a stateful call later. For example:
|
|
@@ -42,17 +43,17 @@ def create_state_machine(schema: "BaseOpenAPISchema") -> Type[APIStateMachine]:
|
|
|
42
43
|
|
|
43
44
|
rules = make_all_rules(operations, bundles, connections)
|
|
44
45
|
|
|
45
|
-
kwargs:
|
|
46
|
+
kwargs: dict[str, Any] = {"bundles": bundles, "schema": schema}
|
|
46
47
|
return type("APIWorkflow", (OpenAPIStateMachine,), {**kwargs, **rules})
|
|
47
48
|
|
|
48
49
|
|
|
49
|
-
def init_bundles(schema:
|
|
50
|
+
def init_bundles(schema: BaseOpenAPISchema) -> dict[str, CaseInsensitiveDict]:
|
|
50
51
|
"""Create bundles for all operations in the given schema.
|
|
51
52
|
|
|
52
53
|
Each API operation has a bundle that stores all responses from that operation.
|
|
53
54
|
We need to create bundles first, so they can be referred when building connections between operations.
|
|
54
55
|
"""
|
|
55
|
-
output:
|
|
56
|
+
output: dict[str, CaseInsensitiveDict] = {}
|
|
56
57
|
for result in schema.get_all_operations():
|
|
57
58
|
if isinstance(result, Ok):
|
|
58
59
|
operation = result.ok()
|
|
@@ -62,10 +63,10 @@ def init_bundles(schema: "BaseOpenAPISchema") -> Dict[str, CaseInsensitiveDict]:
|
|
|
62
63
|
|
|
63
64
|
|
|
64
65
|
def make_all_rules(
|
|
65
|
-
operations:
|
|
66
|
-
bundles:
|
|
66
|
+
operations: list[APIOperation],
|
|
67
|
+
bundles: dict[str, CaseInsensitiveDict],
|
|
67
68
|
connections: APIOperationConnections,
|
|
68
|
-
) ->
|
|
69
|
+
) -> dict[str, Rule]:
|
|
69
70
|
"""Create rules for all API operations, based on the provided connections."""
|
|
70
71
|
rules = {}
|
|
71
72
|
for operation in operations:
|
|
@@ -76,10 +77,10 @@ def make_all_rules(
|
|
|
76
77
|
|
|
77
78
|
|
|
78
79
|
def make_rule(
|
|
79
|
-
operation:
|
|
80
|
+
operation: APIOperation,
|
|
80
81
|
bundle: Bundle,
|
|
81
82
|
connections: APIOperationConnections,
|
|
82
|
-
) ->
|
|
83
|
+
) -> Rule | None:
|
|
83
84
|
"""Create a rule for an API operation."""
|
|
84
85
|
|
|
85
86
|
def _make_rule(previous: st.SearchStrategy) -> Rule:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from typing import TYPE_CHECKING, Callable, Dict, List
|
|
3
|
+
from typing import TYPE_CHECKING, Callable, Dict, List
|
|
4
4
|
|
|
5
5
|
import hypothesis.strategies as st
|
|
6
6
|
from requests.structures import CaseInsensitiveDict
|
|
@@ -18,15 +18,15 @@ FilterFunction = Callable[["StepResult"], bool]
|
|
|
18
18
|
@dataclass
|
|
19
19
|
class Connection:
|
|
20
20
|
source: str
|
|
21
|
-
strategy: st.SearchStrategy[
|
|
21
|
+
strategy: st.SearchStrategy[tuple[StepResult, OpenAPILink]]
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
APIOperationConnections = Dict[str, List[Connection]]
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def apply(
|
|
28
|
-
operation:
|
|
29
|
-
bundles:
|
|
28
|
+
operation: APIOperation,
|
|
29
|
+
bundles: dict[str, CaseInsensitiveDict],
|
|
30
30
|
connections: APIOperationConnections,
|
|
31
31
|
) -> None:
|
|
32
32
|
"""Gather all connections based on Open API links definitions."""
|
|
@@ -42,12 +42,12 @@ def apply(
|
|
|
42
42
|
|
|
43
43
|
def _convert_strategy(
|
|
44
44
|
strategy: st.SearchStrategy[StepResult], link: OpenAPILink
|
|
45
|
-
) -> st.SearchStrategy[
|
|
45
|
+
) -> st.SearchStrategy[tuple[StepResult, OpenAPILink]]:
|
|
46
46
|
# This function is required to capture values properly (it won't work properly when lambda is defined in a loop)
|
|
47
47
|
return strategy.map(lambda out: (out, link))
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
def make_response_filter(status_code: str, all_status_codes:
|
|
50
|
+
def make_response_filter(status_code: str, all_status_codes: list[str]) -> FilterFunction:
|
|
51
51
|
"""Create a filter for stored responses.
|
|
52
52
|
|
|
53
53
|
This filter will decide whether some response is suitable to use as a source for requesting some API operation.
|
|
@@ -76,7 +76,7 @@ def match_status_code(status_code: str) -> FilterFunction:
|
|
|
76
76
|
return compare
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
def default_status_code(status_codes:
|
|
79
|
+
def default_status_code(status_codes: list[str]) -> FilterFunction:
|
|
80
80
|
"""Create a filter that matches all "default" responses.
|
|
81
81
|
|
|
82
82
|
In Open API, the "default" response is the one that is used if no other options were matched.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import string
|
|
2
3
|
from itertools import product
|
|
3
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, Generator
|
|
4
5
|
|
|
5
6
|
|
|
6
|
-
def expand_status_code(status_code:
|
|
7
|
+
def expand_status_code(status_code: str | int) -> Generator[int, None, None]:
|
|
7
8
|
chars = [list(string.digits) if digit == "X" else [digit] for digit in str(status_code).upper()]
|
|
8
9
|
for expanded in product(*chars):
|
|
9
10
|
yield int("".join(expanded))
|
|
@@ -14,7 +15,7 @@ def is_header_location(location: str) -> bool:
|
|
|
14
15
|
return location in ("header", "cookie")
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
def get_type(schema:
|
|
18
|
+
def get_type(schema: dict[str, Any]) -> list[str]:
|
|
18
19
|
type_ = schema.get("type", ["null", "boolean", "integer", "number", "string", "array", "object"])
|
|
19
20
|
if isinstance(type_, str):
|
|
20
21
|
return [type_]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any
|
|
2
3
|
|
|
3
4
|
from ...constants import HTTP_METHODS
|
|
4
5
|
|
|
@@ -9,7 +10,7 @@ def is_pattern_error(exception: TypeError) -> bool:
|
|
|
9
10
|
return "expected string or bytes-like object" in str(exception)
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
def find_numeric_http_status_codes(schema: Any) ->
|
|
13
|
+
def find_numeric_http_status_codes(schema: Any) -> list[tuple[int, list[str | int]]]:
|
|
13
14
|
if not isinstance(schema, dict):
|
|
14
15
|
return []
|
|
15
16
|
found = []
|
|
@@ -2,9 +2,9 @@ from __future__ import annotations
|
|
|
2
2
|
import enum
|
|
3
3
|
import json
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
|
-
from typing import TYPE_CHECKING, Any, Callable,
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator
|
|
6
6
|
|
|
7
|
-
from ..
|
|
7
|
+
from .. import GenerationConfig
|
|
8
8
|
from ..exceptions import OperationSchemaError
|
|
9
9
|
from ..models import APIOperation, Case
|
|
10
10
|
from ..constants import NOT_SET
|
|
@@ -29,7 +29,7 @@ class ParsedData:
|
|
|
29
29
|
It is used later to create a new version of an API operation that will reuse this data.
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
parameters:
|
|
32
|
+
parameters: dict[str, Any]
|
|
33
33
|
body: Any = NOT_SET
|
|
34
34
|
|
|
35
35
|
def __hash__(self) -> int:
|
|
@@ -54,7 +54,7 @@ class StatefulTest:
|
|
|
54
54
|
def parse(self, case: Case, response: GenericResponse) -> ParsedData:
|
|
55
55
|
raise NotImplementedError
|
|
56
56
|
|
|
57
|
-
def make_operation(self, collected:
|
|
57
|
+
def make_operation(self, collected: list[ParsedData]) -> APIOperation:
|
|
58
58
|
raise NotImplementedError
|
|
59
59
|
|
|
60
60
|
|
|
@@ -63,7 +63,7 @@ class StatefulData:
|
|
|
63
63
|
"""Storage for data that will be used in later tests."""
|
|
64
64
|
|
|
65
65
|
stateful_test: StatefulTest
|
|
66
|
-
container:
|
|
66
|
+
container: list[ParsedData] = field(default_factory=list)
|
|
67
67
|
|
|
68
68
|
def make_operation(self) -> APIOperation:
|
|
69
69
|
return self.stateful_test.make_operation(self.container)
|
|
@@ -81,9 +81,9 @@ class Feedback:
|
|
|
81
81
|
Provides a way to control runner's behavior from tests.
|
|
82
82
|
"""
|
|
83
83
|
|
|
84
|
-
stateful:
|
|
84
|
+
stateful: Stateful | None
|
|
85
85
|
operation: APIOperation = field(repr=False)
|
|
86
|
-
stateful_tests:
|
|
86
|
+
stateful_tests: dict[str, StatefulData] = field(default_factory=dict, repr=False)
|
|
87
87
|
|
|
88
88
|
def add_test_case(self, case: Case, response: GenericResponse) -> None:
|
|
89
89
|
"""Store test data to reuse it in the future additional tests."""
|
|
@@ -94,10 +94,11 @@ class Feedback:
|
|
|
94
94
|
def get_stateful_tests(
|
|
95
95
|
self,
|
|
96
96
|
test: Callable,
|
|
97
|
-
settings:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
settings: hypothesis.settings | None,
|
|
98
|
+
generation_config: GenerationConfig | None,
|
|
99
|
+
seed: int | None,
|
|
100
|
+
as_strategy_kwargs: dict[str, Any] | None,
|
|
101
|
+
) -> Generator[Result[tuple[APIOperation, Callable], OperationSchemaError], None, None]:
|
|
101
102
|
"""Generate additional tests that use data from the previous ones."""
|
|
102
103
|
from .._hypothesis import create_test
|
|
103
104
|
|
|
@@ -109,13 +110,14 @@ class Feedback:
|
|
|
109
110
|
settings=settings,
|
|
110
111
|
seed=seed,
|
|
111
112
|
data_generation_methods=operation.schema.data_generation_methods,
|
|
113
|
+
generation_config=generation_config,
|
|
112
114
|
as_strategy_kwargs=as_strategy_kwargs,
|
|
113
115
|
)
|
|
114
116
|
yield Ok((operation, test_function))
|
|
115
117
|
|
|
116
118
|
|
|
117
119
|
def run_state_machine_as_test(
|
|
118
|
-
state_machine_factory:
|
|
120
|
+
state_machine_factory: type[APIStateMachine], *, settings: hypothesis.settings | None = None
|
|
119
121
|
) -> None:
|
|
120
122
|
"""Run a state machine as a test.
|
|
121
123
|
|
|
@@ -123,7 +125,4 @@ def run_state_machine_as_test(
|
|
|
123
125
|
"""
|
|
124
126
|
from hypothesis.stateful import run_state_machine_as_test as _run_state_machine_as_test
|
|
125
127
|
|
|
126
|
-
|
|
127
|
-
# Newer Hypothesis contains an argument to set the minimum number of steps for a state machine execution
|
|
128
|
-
return _run_state_machine_as_test(state_machine_factory, settings=settings, _min_steps=2)
|
|
129
|
-
return _run_state_machine_as_test(state_machine_factory, settings=settings)
|
|
128
|
+
return _run_state_machine_as_test(state_machine_factory, settings=settings, _min_steps=2)
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from typing import TYPE_CHECKING, Any,
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, ClassVar
|
|
6
6
|
|
|
7
7
|
from hypothesis.stateful import RuleBasedStateMachine
|
|
8
8
|
|
|
@@ -34,8 +34,8 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
|
34
34
|
# This is a convenience attribute, which happened to clash with `RuleBasedStateMachine` instance level attribute
|
|
35
35
|
# They don't interfere, since it is properly overridden on the Hypothesis side, but it is likely that this
|
|
36
36
|
# attribute will be renamed in the future
|
|
37
|
-
bundles: ClassVar[
|
|
38
|
-
schema:
|
|
37
|
+
bundles: ClassVar[dict[str, CaseInsensitiveDict]] # type: ignore
|
|
38
|
+
schema: BaseSchema
|
|
39
39
|
|
|
40
40
|
def __init__(self) -> None:
|
|
41
41
|
super().__init__() # type: ignore
|
|
@@ -53,7 +53,7 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
|
53
53
|
return super()._pretty_print(value) # type: ignore
|
|
54
54
|
|
|
55
55
|
@classmethod
|
|
56
|
-
def run(cls, *, settings:
|
|
56
|
+
def run(cls, *, settings: hypothesis.settings | None = None) -> None:
|
|
57
57
|
"""Run state machine as a test."""
|
|
58
58
|
from . import run_state_machine_as_test
|
|
59
59
|
|
|
@@ -74,14 +74,14 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
|
74
74
|
def transform(self, result: StepResult, direction: Direction, case: Case) -> Case:
|
|
75
75
|
raise NotImplementedError
|
|
76
76
|
|
|
77
|
-
def _step(self, case: Case, previous:
|
|
77
|
+
def _step(self, case: Case, previous: tuple[StepResult, Direction] | None = None) -> StepResult:
|
|
78
78
|
# This method is a proxy that is used under the hood during the state machine initialization.
|
|
79
79
|
# The whole point of having it is to make it possible to override `step`; otherwise, custom "step" is ignored.
|
|
80
80
|
# It happens because, at the point of initialization, the final class is not yet created.
|
|
81
81
|
__tracebackhide__ = True
|
|
82
82
|
return self.step(case, previous)
|
|
83
83
|
|
|
84
|
-
def step(self, case: Case, previous:
|
|
84
|
+
def step(self, case: Case, previous: tuple[StepResult, Direction] | None = None) -> StepResult:
|
|
85
85
|
"""A single state machine step.
|
|
86
86
|
|
|
87
87
|
:param Case case: Generated test case data that should be sent in an API call to the tested API operation.
|
|
@@ -177,7 +177,7 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
|
177
177
|
method = self._get_call_method(case)
|
|
178
178
|
return method(**kwargs)
|
|
179
179
|
|
|
180
|
-
def get_call_kwargs(self, case: Case) ->
|
|
180
|
+
def get_call_kwargs(self, case: Case) -> dict[str, Any]:
|
|
181
181
|
"""Create custom keyword arguments that will be passed to the :meth:`Case.call` method.
|
|
182
182
|
|
|
183
183
|
Mostly they are proxied to the :func:`requests.request` call.
|
|
@@ -204,7 +204,7 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
|
204
204
|
return case.call
|
|
205
205
|
|
|
206
206
|
def validate_response(
|
|
207
|
-
self, response: GenericResponse, case: Case, additional_checks:
|
|
207
|
+
self, response: GenericResponse, case: Case, additional_checks: tuple[CheckFunction, ...] = ()
|
|
208
208
|
) -> None:
|
|
209
209
|
"""Validate an API response.
|
|
210
210
|
|
|
@@ -242,7 +242,7 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
|
242
242
|
return StepResult(response, case, elapsed)
|
|
243
243
|
|
|
244
244
|
|
|
245
|
-
def _print_case(case: Case, kwargs:
|
|
245
|
+
def _print_case(case: Case, kwargs: dict[str, Any]) -> str:
|
|
246
246
|
from requests.structures import CaseInsensitiveDict
|
|
247
247
|
|
|
248
248
|
operation = f"state.schema['{case.operation.path}']['{case.operation.method.upper()}']"
|
schemathesis/targets.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from typing import TYPE_CHECKING, Callable
|
|
3
|
+
from typing import TYPE_CHECKING, Callable
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from .models import Case
|
|
@@ -16,7 +16,7 @@ class TargetContext:
|
|
|
16
16
|
:ivar float response_time: API response time.
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
case:
|
|
19
|
+
case: Case
|
|
20
20
|
response: GenericResponse
|
|
21
21
|
response_time: float
|
|
22
22
|
|
|
@@ -28,7 +28,7 @@ def response_time(context: TargetContext) -> float:
|
|
|
28
28
|
Target = Callable[[TargetContext], float]
|
|
29
29
|
DEFAULT_TARGETS = ()
|
|
30
30
|
OPTIONAL_TARGETS = (response_time,)
|
|
31
|
-
ALL_TARGETS:
|
|
31
|
+
ALL_TARGETS: tuple[Target, ...] = DEFAULT_TARGETS + OPTIONAL_TARGETS
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def register(target: Target) -> Target:
|
schemathesis/throttling.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
3
|
|
|
4
4
|
from .exceptions import UsageError
|
|
5
5
|
|
|
@@ -8,7 +8,7 @@ if TYPE_CHECKING:
|
|
|
8
8
|
from pyrate_limiter import Limiter
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def parse_units(rate: str) ->
|
|
11
|
+
def parse_units(rate: str) -> tuple[int, int]:
|
|
12
12
|
from pyrate_limiter import Duration
|
|
13
13
|
|
|
14
14
|
try:
|
schemathesis/transports/auth.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
3
|
|
|
4
4
|
from ..types import RawAuth
|
|
5
5
|
|
|
@@ -7,7 +7,7 @@ if TYPE_CHECKING:
|
|
|
7
7
|
from requests.auth import HTTPDigestAuth
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def get_requests_auth(auth:
|
|
10
|
+
def get_requests_auth(auth: RawAuth | None, auth_type: str | None) -> HTTPDigestAuth | RawAuth | None:
|
|
11
11
|
from requests.auth import HTTPDigestAuth
|
|
12
12
|
|
|
13
13
|
if auth and auth_type == "digest":
|
|
@@ -21,6 +21,11 @@ def is_json_media_type(value: str) -> bool:
|
|
|
21
21
|
return main == "application" and (sub == "json" or sub.endswith("+json"))
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def is_yaml_media_type(value: str) -> bool:
|
|
25
|
+
"""Detect whether the content type is YAML-compatible."""
|
|
26
|
+
return value in ("text/yaml", "text/x-yaml", "application/x-yaml", "text/vnd.yaml")
|
|
27
|
+
|
|
28
|
+
|
|
24
29
|
def is_plain_text_media_type(value: str) -> bool:
|
|
25
30
|
"""Detect variations of the ``text/plain`` media type."""
|
|
26
31
|
return parse_content_type(value) == ("text", "plain")
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import re
|
|
2
|
-
from typing import
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
5
|
from ..constants import USER_AGENT
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
def setup_default_headers(kwargs:
|
|
8
|
+
def setup_default_headers(kwargs: dict[str, Any]) -> None:
|
|
8
9
|
headers = kwargs.setdefault("headers", {})
|
|
9
10
|
if "user-agent" not in {header.lower() for header in headers}:
|
|
10
11
|
kwargs["headers"]["User-Agent"] = USER_AGENT
|
|
@@ -58,7 +58,7 @@ def copy_response(response: GenericResponse) -> GenericResponse:
|
|
|
58
58
|
|
|
59
59
|
def get_reason(status_code: int) -> str:
|
|
60
60
|
if sys.version_info < (3, 9) and status_code == 418:
|
|
61
|
-
# Python 3.
|
|
61
|
+
# Python 3.8 does not have 418 status in the `HTTPStatus` enum
|
|
62
62
|
return "I'm a Teapot"
|
|
63
63
|
|
|
64
64
|
import http.client
|
schemathesis/utils.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import functools
|
|
2
3
|
import operator
|
|
3
4
|
from contextlib import contextmanager
|
|
@@ -5,12 +6,8 @@ from inspect import getfullargspec
|
|
|
5
6
|
from typing import (
|
|
6
7
|
Any,
|
|
7
8
|
Callable,
|
|
8
|
-
Dict,
|
|
9
9
|
Generator,
|
|
10
|
-
List,
|
|
11
10
|
NoReturn,
|
|
12
|
-
Optional,
|
|
13
|
-
Tuple,
|
|
14
11
|
Union,
|
|
15
12
|
)
|
|
16
13
|
|
|
@@ -57,7 +54,7 @@ IGNORED_PATTERNS = (
|
|
|
57
54
|
|
|
58
55
|
|
|
59
56
|
@contextmanager
|
|
60
|
-
def capture_hypothesis_output() -> Generator[
|
|
57
|
+
def capture_hypothesis_output() -> Generator[list[str], None, None]:
|
|
61
58
|
"""Capture all output of Hypothesis into a list of strings.
|
|
62
59
|
|
|
63
60
|
It allows us to have more granular control over Schemathesis output.
|
|
@@ -91,11 +88,11 @@ GIVEN_ARGS_MARKER = "_schemathesis_given_args"
|
|
|
91
88
|
GIVEN_KWARGS_MARKER = "_schemathesis_given_kwargs"
|
|
92
89
|
|
|
93
90
|
|
|
94
|
-
def get_given_args(func: GenericTest) ->
|
|
91
|
+
def get_given_args(func: GenericTest) -> tuple:
|
|
95
92
|
return getattr(func, GIVEN_ARGS_MARKER, ())
|
|
96
93
|
|
|
97
94
|
|
|
98
|
-
def get_given_kwargs(func: GenericTest) ->
|
|
95
|
+
def get_given_kwargs(func: GenericTest) -> dict[str, Any]:
|
|
99
96
|
return getattr(func, GIVEN_KWARGS_MARKER, {})
|
|
100
97
|
|
|
101
98
|
|
|
@@ -124,7 +121,7 @@ def given_proxy(*args: GivenInput, **kwargs: GivenInput) -> Callable[[GenericTes
|
|
|
124
121
|
return wrapper
|
|
125
122
|
|
|
126
123
|
|
|
127
|
-
def merge_given_args(func: GenericTest, args:
|
|
124
|
+
def merge_given_args(func: GenericTest, args: tuple, kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
128
125
|
"""Merge positional arguments to ``@schema.given`` into a dictionary with keyword arguments.
|
|
129
126
|
|
|
130
127
|
Kwargs are modified inplace.
|
|
@@ -136,7 +133,7 @@ def merge_given_args(func: GenericTest, args: Tuple, kwargs: Dict[str, Any]) ->
|
|
|
136
133
|
return kwargs
|
|
137
134
|
|
|
138
135
|
|
|
139
|
-
def validate_given_args(func: GenericTest, args:
|
|
136
|
+
def validate_given_args(func: GenericTest, args: tuple, kwargs: dict[str, Any]) -> Callable | None:
|
|
140
137
|
signature = get_signature(func)
|
|
141
138
|
return is_invalid_test(func, signature, args, kwargs) # type: ignore
|
|
142
139
|
|
|
@@ -150,7 +147,7 @@ def compose(*functions: Callable) -> Callable:
|
|
|
150
147
|
return functools.reduce(lambda f, g: lambda x: f(g(x)), functions, noop)
|
|
151
148
|
|
|
152
149
|
|
|
153
|
-
def combine_strategies(strategies:
|
|
150
|
+
def combine_strategies(strategies: list[st.SearchStrategy]) -> st.SearchStrategy:
|
|
154
151
|
"""Combine a list of strategies into a single one.
|
|
155
152
|
|
|
156
153
|
If the input is `[a, b, c]`, then the result is equivalent to `a | b | c`.
|