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
schemathesis/extra/_aiohttp.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import asyncio
|
|
2
|
-
from typing import Optional
|
|
3
3
|
|
|
4
4
|
from aiohttp import web
|
|
5
5
|
|
|
@@ -22,6 +22,6 @@ def _run_server(app: web.Application, port: int) -> None:
|
|
|
22
22
|
loop.run_forever()
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
def run_server(app: web.Application, port:
|
|
25
|
+
def run_server(app: web.Application, port: int | None = None, timeout: float = 0.05) -> int:
|
|
26
26
|
"""Start a thread with the given aiohttp application."""
|
|
27
27
|
return _server.run(_run_server, app=app, port=port, timeout=timeout)
|
schemathesis/extra/_flask.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from flask import Flask
|
|
4
4
|
|
|
5
5
|
from . import _server
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def run_server(app: Flask, port:
|
|
8
|
+
def run_server(app: Flask, port: int | None = None, timeout: float = 0.05) -> int:
|
|
9
9
|
"""Start a thread with the given aiohttp application."""
|
|
10
10
|
return _server.run(app.run, port=port, timeout=timeout)
|
schemathesis/extra/_server.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import threading
|
|
2
3
|
from time import sleep
|
|
3
|
-
from typing import Any, Callable
|
|
4
|
+
from typing import Any, Callable
|
|
4
5
|
|
|
5
6
|
from aiohttp.test_utils import unused_port
|
|
6
7
|
|
|
7
8
|
|
|
8
|
-
def run(target: Callable, port:
|
|
9
|
+
def run(target: Callable, port: int | None = None, timeout: float = 0.05, **kwargs: Any) -> int:
|
|
9
10
|
"""Start a thread with the given aiohttp application."""
|
|
10
11
|
if port is None:
|
|
11
12
|
port = unused_port()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
from contextlib import contextmanager
|
|
2
3
|
from functools import partial
|
|
3
|
-
from typing import Any, Callable,
|
|
4
|
+
from typing import Any, Callable, Generator, Type, TypeVar, cast
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
6
7
|
from _pytest import fixtures, nodes
|
|
@@ -32,7 +33,7 @@ from ..utils import (
|
|
|
32
33
|
T = TypeVar("T", bound=Node)
|
|
33
34
|
|
|
34
35
|
|
|
35
|
-
def create(cls:
|
|
36
|
+
def create(cls: type[T], *args: Any, **kwargs: Any) -> T:
|
|
36
37
|
if IS_PYTEST_ABOVE_54:
|
|
37
38
|
return cls.from_parent(*args, **kwargs) # type: ignore
|
|
38
39
|
return cls(*args, **kwargs)
|
|
@@ -43,7 +44,7 @@ class SchemathesisFunction(Function):
|
|
|
43
44
|
self,
|
|
44
45
|
*args: Any,
|
|
45
46
|
test_func: Callable,
|
|
46
|
-
test_name:
|
|
47
|
+
test_name: str | None = None,
|
|
47
48
|
**kwargs: Any,
|
|
48
49
|
) -> None:
|
|
49
50
|
super().__init__(*args, **kwargs)
|
|
@@ -62,11 +63,11 @@ class SchemathesisFunction(Function):
|
|
|
62
63
|
|
|
63
64
|
class SchemathesisCase(PyCollector):
|
|
64
65
|
def __init__(self, test_function: Callable, *args: Any, **kwargs: Any) -> None:
|
|
65
|
-
self.given_kwargs:
|
|
66
|
+
self.given_kwargs: dict[str, Any] | None
|
|
66
67
|
given_args = get_given_args(test_function)
|
|
67
68
|
given_kwargs = get_given_kwargs(test_function)
|
|
68
69
|
|
|
69
|
-
def _init_with_valid_test(_test_function: Callable, _args:
|
|
70
|
+
def _init_with_valid_test(_test_function: Callable, _args: tuple, _kwargs: dict[str, Any]) -> None:
|
|
70
71
|
self.test_function = _test_function
|
|
71
72
|
self.is_invalid_test = False
|
|
72
73
|
self.given_kwargs = merge_given_args(test_function, _args, _kwargs)
|
|
@@ -108,6 +109,7 @@ class SchemathesisCase(PyCollector):
|
|
|
108
109
|
test=self.test_function,
|
|
109
110
|
_given_kwargs=self.given_kwargs,
|
|
110
111
|
data_generation_methods=self.schemathesis_case.data_generation_methods,
|
|
112
|
+
generation_config=self.schemathesis_case.generation_config,
|
|
111
113
|
)
|
|
112
114
|
name = self._get_test_name(operation)
|
|
113
115
|
else:
|
|
@@ -158,13 +160,11 @@ class SchemathesisCase(PyCollector):
|
|
|
158
160
|
test_func=self.test_function,
|
|
159
161
|
)
|
|
160
162
|
|
|
161
|
-
def _get_class_parent(self) ->
|
|
163
|
+
def _get_class_parent(self) -> type | None:
|
|
162
164
|
clscol = self.getparent(Class)
|
|
163
165
|
return clscol.obj if clscol else None
|
|
164
166
|
|
|
165
|
-
def _parametrize(
|
|
166
|
-
self, cls: Optional[Type], definition: FunctionDefinition, fixtureinfo: FuncFixtureInfo
|
|
167
|
-
) -> Metafunc:
|
|
167
|
+
def _parametrize(self, cls: type | None, definition: FunctionDefinition, fixtureinfo: FuncFixtureInfo) -> Metafunc:
|
|
168
168
|
parent = self.getparent(Module)
|
|
169
169
|
module = parent.obj if parent is not None else parent
|
|
170
170
|
kwargs = {"cls": cls, "module": module}
|
|
@@ -181,7 +181,7 @@ class SchemathesisCase(PyCollector):
|
|
|
181
181
|
self.ihook.pytest_generate_tests.call_extra(methods, {"metafunc": metafunc})
|
|
182
182
|
return metafunc
|
|
183
183
|
|
|
184
|
-
def collect(self) ->
|
|
184
|
+
def collect(self) -> list[Function]: # type: ignore
|
|
185
185
|
"""Generate different test items for all API operations available in the given schema."""
|
|
186
186
|
try:
|
|
187
187
|
items = [
|
schemathesis/failures.py
CHANGED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
import textwrap
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from json import JSONDecodeError
|
|
5
|
-
from typing import Any,
|
|
5
|
+
from typing import Any, TYPE_CHECKING
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
8
|
from jsonschema import ValidationError
|
|
@@ -17,7 +17,7 @@ class FailureContext:
|
|
|
17
17
|
message: str
|
|
18
18
|
type: str
|
|
19
19
|
|
|
20
|
-
def unique_by_key(self, check_message:
|
|
20
|
+
def unique_by_key(self, check_message: str | None) -> tuple[str, ...]:
|
|
21
21
|
"""A key to distinguish different failure contexts."""
|
|
22
22
|
return (check_message or self.message,)
|
|
23
23
|
|
|
@@ -27,20 +27,20 @@ class ValidationErrorContext(FailureContext):
|
|
|
27
27
|
"""Additional information about JSON Schema validation errors."""
|
|
28
28
|
|
|
29
29
|
validation_message: str
|
|
30
|
-
schema_path:
|
|
31
|
-
schema:
|
|
32
|
-
instance_path:
|
|
33
|
-
instance:
|
|
30
|
+
schema_path: list[str | int]
|
|
31
|
+
schema: dict[str, Any] | bool
|
|
32
|
+
instance_path: list[str | int]
|
|
33
|
+
instance: None | bool | float | str | list | dict[str, Any]
|
|
34
34
|
message: str
|
|
35
35
|
title: str = "Response violates schema"
|
|
36
36
|
type: str = "json_schema"
|
|
37
37
|
|
|
38
|
-
def unique_by_key(self, check_message:
|
|
38
|
+
def unique_by_key(self, check_message: str | None) -> tuple[str, ...]:
|
|
39
39
|
# Deduplicate by JSON Schema path. All errors that happened on this sub-schema will be deduplicated
|
|
40
40
|
return ("/".join(map(str, self.schema_path)),)
|
|
41
41
|
|
|
42
42
|
@classmethod
|
|
43
|
-
def from_exception(cls, exc:
|
|
43
|
+
def from_exception(cls, exc: ValidationError) -> ValidationErrorContext:
|
|
44
44
|
from .exceptions import truncated_json
|
|
45
45
|
|
|
46
46
|
schema = textwrap.indent(truncated_json(exc.schema, max_lines=20), prefix=" ")
|
|
@@ -69,14 +69,14 @@ class JSONDecodeErrorContext(FailureContext):
|
|
|
69
69
|
title: str = "JSON deserialization error"
|
|
70
70
|
type: str = "json_decode"
|
|
71
71
|
|
|
72
|
-
def unique_by_key(self, check_message:
|
|
72
|
+
def unique_by_key(self, check_message: str | None) -> tuple[str, ...]:
|
|
73
73
|
# Treat different JSON decoding failures as the same issue
|
|
74
74
|
# Payloads often contain dynamic data and distinguishing it by the error location still would not be sufficient
|
|
75
75
|
# as it may be different on different dynamic payloads
|
|
76
76
|
return (self.title,)
|
|
77
77
|
|
|
78
78
|
@classmethod
|
|
79
|
-
def from_exception(cls, exc: JSONDecodeError) ->
|
|
79
|
+
def from_exception(cls, exc: JSONDecodeError) -> JSONDecodeErrorContext:
|
|
80
80
|
return cls(
|
|
81
81
|
message=str(exc),
|
|
82
82
|
validation_message=exc.msg,
|
|
@@ -99,7 +99,7 @@ class ServerError(FailureContext):
|
|
|
99
99
|
class MissingContentType(FailureContext):
|
|
100
100
|
"""Content type header is missing."""
|
|
101
101
|
|
|
102
|
-
media_types:
|
|
102
|
+
media_types: list[str]
|
|
103
103
|
message: str
|
|
104
104
|
title: str = "Missing Content-Type header"
|
|
105
105
|
type: str = "missing_content_type"
|
|
@@ -110,7 +110,7 @@ class UndefinedContentType(FailureContext):
|
|
|
110
110
|
"""Response has Content-Type that is not documented in the schema."""
|
|
111
111
|
|
|
112
112
|
content_type: str
|
|
113
|
-
defined_content_types:
|
|
113
|
+
defined_content_types: list[str]
|
|
114
114
|
message: str
|
|
115
115
|
title: str = "Undocumented Content-Type"
|
|
116
116
|
type: str = "undefined_content_type"
|
|
@@ -123,9 +123,9 @@ class UndefinedStatusCode(FailureContext):
|
|
|
123
123
|
# Response's status code
|
|
124
124
|
status_code: int
|
|
125
125
|
# Status codes as defined in schema
|
|
126
|
-
defined_status_codes:
|
|
126
|
+
defined_status_codes: list[str]
|
|
127
127
|
# Defined status code with expanded wildcards
|
|
128
|
-
allowed_status_codes:
|
|
128
|
+
allowed_status_codes: list[int]
|
|
129
129
|
message: str
|
|
130
130
|
title: str = "Undocumented HTTP status code"
|
|
131
131
|
type: str = "undefined_status_code"
|
|
@@ -135,7 +135,7 @@ class UndefinedStatusCode(FailureContext):
|
|
|
135
135
|
class MissingHeaders(FailureContext):
|
|
136
136
|
"""Some required headers are missing."""
|
|
137
137
|
|
|
138
|
-
missing_headers:
|
|
138
|
+
missing_headers: list[str]
|
|
139
139
|
message: str
|
|
140
140
|
title: str = "Missing required headers"
|
|
141
141
|
type: str = "missing_headers"
|
|
@@ -165,7 +165,7 @@ class ResponseTimeExceeded(FailureContext):
|
|
|
165
165
|
title: str = "Response time limit exceeded"
|
|
166
166
|
type: str = "response_time_exceeded"
|
|
167
167
|
|
|
168
|
-
def unique_by_key(self, check_message:
|
|
168
|
+
def unique_by_key(self, check_message: str | None) -> tuple[str, ...]:
|
|
169
169
|
return (self.title,)
|
|
170
170
|
|
|
171
171
|
|
schemathesis/filters.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"""Filtering system that allows users to filter API operations based on certain criteria."""
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
import re
|
|
3
4
|
from dataclasses import dataclass, field
|
|
4
5
|
from functools import partial
|
|
5
6
|
from types import SimpleNamespace
|
|
6
|
-
from typing import TYPE_CHECKING, Callable, List,
|
|
7
|
-
|
|
8
|
-
from typing_extensions import Protocol
|
|
7
|
+
from typing import TYPE_CHECKING, Callable, List, Union, Protocol
|
|
9
8
|
|
|
10
9
|
from .exceptions import UsageError
|
|
11
10
|
|
|
@@ -14,7 +13,7 @@ if TYPE_CHECKING:
|
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
class HasAPIOperation(Protocol):
|
|
17
|
-
operation:
|
|
16
|
+
operation: APIOperation
|
|
18
17
|
|
|
19
18
|
|
|
20
19
|
MatcherFunc = Callable[[HasAPIOperation], bool]
|
|
@@ -39,12 +38,12 @@ class Matcher:
|
|
|
39
38
|
return f"<{self.__class__.__name__}: {self.label}>"
|
|
40
39
|
|
|
41
40
|
@classmethod
|
|
42
|
-
def for_function(cls, func: MatcherFunc) ->
|
|
41
|
+
def for_function(cls, func: MatcherFunc) -> Matcher:
|
|
43
42
|
"""Matcher that uses the given function for matching operations."""
|
|
44
43
|
return cls(func, label=func.__name__, _hash=hash(func))
|
|
45
44
|
|
|
46
45
|
@classmethod
|
|
47
|
-
def for_value(cls, attribute: str, expected: FilterValue) ->
|
|
46
|
+
def for_value(cls, attribute: str, expected: FilterValue) -> Matcher:
|
|
48
47
|
"""Matcher that checks whether the specified attribute has the expected value."""
|
|
49
48
|
if isinstance(expected, list):
|
|
50
49
|
func = partial(by_value_list, attribute=attribute, expected=expected)
|
|
@@ -54,7 +53,7 @@ class Matcher:
|
|
|
54
53
|
return cls(func, label=label, _hash=hash(label))
|
|
55
54
|
|
|
56
55
|
@classmethod
|
|
57
|
-
def for_regex(cls, attribute: str, regex: RegexValue) ->
|
|
56
|
+
def for_regex(cls, attribute: str, regex: RegexValue) -> Matcher:
|
|
58
57
|
"""Matcher that checks whether the specified attribute has the provided regex."""
|
|
59
58
|
if isinstance(regex, str):
|
|
60
59
|
regex = re.compile(regex)
|
|
@@ -67,7 +66,7 @@ class Matcher:
|
|
|
67
66
|
return self.func(ctx)
|
|
68
67
|
|
|
69
68
|
|
|
70
|
-
def get_operation_attribute(operation:
|
|
69
|
+
def get_operation_attribute(operation: APIOperation, attribute: str) -> str:
|
|
71
70
|
# Just uppercase `method`
|
|
72
71
|
value = getattr(operation, attribute)
|
|
73
72
|
if attribute == "method":
|
|
@@ -79,7 +78,7 @@ def by_value(ctx: HasAPIOperation, attribute: str, expected: str) -> bool:
|
|
|
79
78
|
return get_operation_attribute(ctx.operation, attribute) == expected
|
|
80
79
|
|
|
81
80
|
|
|
82
|
-
def by_value_list(ctx: HasAPIOperation, attribute: str, expected:
|
|
81
|
+
def by_value_list(ctx: HasAPIOperation, attribute: str, expected: list[str]) -> bool:
|
|
83
82
|
return get_operation_attribute(ctx.operation, attribute) in expected
|
|
84
83
|
|
|
85
84
|
|
|
@@ -92,7 +91,7 @@ def by_regex(ctx: HasAPIOperation, attribute: str, regex: re.Pattern) -> bool:
|
|
|
92
91
|
class Filter:
|
|
93
92
|
"""Match API operations against a list of matchers."""
|
|
94
93
|
|
|
95
|
-
matchers:
|
|
94
|
+
matchers: tuple[Matcher, ...]
|
|
96
95
|
|
|
97
96
|
def __repr__(self) -> str:
|
|
98
97
|
inner = " && ".join(matcher.label for matcher in self.matchers)
|
|
@@ -110,10 +109,10 @@ class Filter:
|
|
|
110
109
|
class FilterSet:
|
|
111
110
|
"""Combines multiple filters to apply inclusion and exclusion rules on API operations."""
|
|
112
111
|
|
|
113
|
-
_includes:
|
|
114
|
-
_excludes:
|
|
112
|
+
_includes: set[Filter] = field(default_factory=set)
|
|
113
|
+
_excludes: set[Filter] = field(default_factory=set)
|
|
115
114
|
|
|
116
|
-
def apply_to(self, operations:
|
|
115
|
+
def apply_to(self, operations: list[APIOperation]) -> list[APIOperation]:
|
|
117
116
|
"""Get a filtered list of the given operations that match the filters."""
|
|
118
117
|
return [operation for operation in operations if self.match(SimpleNamespace(operation=operation))]
|
|
119
118
|
|
|
@@ -141,14 +140,14 @@ class FilterSet:
|
|
|
141
140
|
|
|
142
141
|
def include(
|
|
143
142
|
self,
|
|
144
|
-
func:
|
|
143
|
+
func: MatcherFunc | None = None,
|
|
145
144
|
*,
|
|
146
|
-
name:
|
|
147
|
-
name_regex:
|
|
148
|
-
method:
|
|
149
|
-
method_regex:
|
|
150
|
-
path:
|
|
151
|
-
path_regex:
|
|
145
|
+
name: FilterValue | None = None,
|
|
146
|
+
name_regex: RegexValue | None = None,
|
|
147
|
+
method: FilterValue | None = None,
|
|
148
|
+
method_regex: RegexValue | None = None,
|
|
149
|
+
path: FilterValue | None = None,
|
|
150
|
+
path_regex: RegexValue | None = None,
|
|
152
151
|
) -> None:
|
|
153
152
|
"""Add a new INCLUDE filter."""
|
|
154
153
|
self._add_filter(
|
|
@@ -164,14 +163,14 @@ class FilterSet:
|
|
|
164
163
|
|
|
165
164
|
def exclude(
|
|
166
165
|
self,
|
|
167
|
-
func:
|
|
166
|
+
func: MatcherFunc | None = None,
|
|
168
167
|
*,
|
|
169
|
-
name:
|
|
170
|
-
name_regex:
|
|
171
|
-
method:
|
|
172
|
-
method_regex:
|
|
173
|
-
path:
|
|
174
|
-
path_regex:
|
|
168
|
+
name: FilterValue | None = None,
|
|
169
|
+
name_regex: RegexValue | None = None,
|
|
170
|
+
method: FilterValue | None = None,
|
|
171
|
+
method_regex: RegexValue | None = None,
|
|
172
|
+
path: FilterValue | None = None,
|
|
173
|
+
path_regex: RegexValue | None = None,
|
|
175
174
|
) -> None:
|
|
176
175
|
"""Add a new EXCLUDE filter."""
|
|
177
176
|
self._add_filter(
|
|
@@ -189,13 +188,13 @@ class FilterSet:
|
|
|
189
188
|
self,
|
|
190
189
|
include: bool,
|
|
191
190
|
*,
|
|
192
|
-
func:
|
|
193
|
-
name:
|
|
194
|
-
name_regex:
|
|
195
|
-
method:
|
|
196
|
-
method_regex:
|
|
197
|
-
path:
|
|
198
|
-
path_regex:
|
|
191
|
+
func: MatcherFunc | None = None,
|
|
192
|
+
name: FilterValue | None = None,
|
|
193
|
+
name_regex: RegexValue | None = None,
|
|
194
|
+
method: FilterValue | None = None,
|
|
195
|
+
method_regex: RegexValue | None = None,
|
|
196
|
+
path: FilterValue | None = None,
|
|
197
|
+
path_regex: RegexValue | None = None,
|
|
199
198
|
) -> None:
|
|
200
199
|
matchers = []
|
|
201
200
|
if func is not None:
|
|
@@ -242,14 +241,14 @@ def attach_filter_chain(
|
|
|
242
241
|
"""
|
|
243
242
|
|
|
244
243
|
def proxy(
|
|
245
|
-
func:
|
|
244
|
+
func: MatcherFunc | None = None,
|
|
246
245
|
*,
|
|
247
|
-
name:
|
|
248
|
-
name_regex:
|
|
249
|
-
method:
|
|
250
|
-
method_regex:
|
|
251
|
-
path:
|
|
252
|
-
path_regex:
|
|
246
|
+
name: FilterValue | None = None,
|
|
247
|
+
name_regex: str | None = None,
|
|
248
|
+
method: FilterValue | None = None,
|
|
249
|
+
method_regex: str | None = None,
|
|
250
|
+
path: FilterValue | None = None,
|
|
251
|
+
path_regex: str | None = None,
|
|
253
252
|
) -> Callable:
|
|
254
253
|
__tracebackhide__ = True
|
|
255
254
|
filter_func(
|
schemathesis/fixups/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Iterable
|
|
2
3
|
|
|
3
4
|
from . import fast_api, utf8_bom
|
|
4
5
|
|
|
@@ -6,7 +7,7 @@ ALL_FIXUPS = {"fast_api": fast_api, "utf8_bom": utf8_bom}
|
|
|
6
7
|
ALL_FIXUP_NAMES = list(ALL_FIXUPS.keys())
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
def install(fixups:
|
|
10
|
+
def install(fixups: Iterable[str] | None = None) -> None:
|
|
10
11
|
"""Install fixups.
|
|
11
12
|
|
|
12
13
|
Without the first argument installs all available fixups.
|
|
@@ -18,7 +19,7 @@ def install(fixups: Optional[Iterable[str]] = None) -> None:
|
|
|
18
19
|
ALL_FIXUPS[name].install() # type: ignore
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
def uninstall(fixups:
|
|
22
|
+
def uninstall(fixups: Iterable[str] | None = None) -> None:
|
|
22
23
|
"""Uninstall fixups.
|
|
23
24
|
|
|
24
25
|
Without the first argument uninstalls all available fixups.
|
schemathesis/fixups/fast_api.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any
|
|
2
3
|
|
|
3
4
|
from ..hooks import HookContext
|
|
4
5
|
from ..hooks import is_installed as global_is_installed
|
|
@@ -18,15 +19,15 @@ def is_installed() -> bool:
|
|
|
18
19
|
return global_is_installed("before_load_schema", before_load_schema)
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
def before_load_schema(context: HookContext, schema:
|
|
22
|
+
def before_load_schema(context: HookContext, schema: dict[str, Any]) -> None:
|
|
22
23
|
adjust_schema(schema)
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
def adjust_schema(schema:
|
|
26
|
+
def adjust_schema(schema: dict[str, Any]) -> None:
|
|
26
27
|
traverse_schema(schema, _handle_boundaries)
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
def _handle_boundaries(schema:
|
|
30
|
+
def _handle_boundaries(schema: dict[str, Any]) -> dict[str, Any]:
|
|
30
31
|
"""Convert Draft 7 keywords to Draft 4 compatible versions.
|
|
31
32
|
|
|
32
33
|
FastAPI uses ``pydantic``, which generates Draft 7 compatible schemas.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import random
|
|
3
|
+
from dataclasses import dataclass
|
|
2
4
|
from enum import Enum
|
|
3
|
-
from typing import
|
|
5
|
+
from typing import Union, Iterable
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class DataGenerationMethod(str, Enum):
|
|
@@ -12,11 +14,11 @@ class DataGenerationMethod(str, Enum):
|
|
|
12
14
|
negative = "negative"
|
|
13
15
|
|
|
14
16
|
@classmethod
|
|
15
|
-
def default(cls) ->
|
|
17
|
+
def default(cls) -> DataGenerationMethod:
|
|
16
18
|
return cls.positive
|
|
17
19
|
|
|
18
20
|
@classmethod
|
|
19
|
-
def all(cls) ->
|
|
21
|
+
def all(cls) -> list[DataGenerationMethod]:
|
|
20
22
|
return list(DataGenerationMethod)
|
|
21
23
|
|
|
22
24
|
def as_short_name(self) -> str:
|
|
@@ -30,7 +32,7 @@ class DataGenerationMethod(str, Enum):
|
|
|
30
32
|
return self == DataGenerationMethod.negative
|
|
31
33
|
|
|
32
34
|
@classmethod
|
|
33
|
-
def ensure_list(cls, value:
|
|
35
|
+
def ensure_list(cls, value: DataGenerationMethodInput) -> list[DataGenerationMethod]:
|
|
34
36
|
if isinstance(value, DataGenerationMethod):
|
|
35
37
|
return [value]
|
|
36
38
|
return list(value)
|
|
@@ -54,3 +56,13 @@ def generate_random_case_id(length: int = 6) -> str:
|
|
|
54
56
|
number, rem = divmod(number, BASE)
|
|
55
57
|
output += CASE_ID_ALPHABET[rem]
|
|
56
58
|
return output
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class GenerationConfig:
|
|
63
|
+
"""Holds various configuration options relevant for data generation."""
|
|
64
|
+
|
|
65
|
+
# Allow generating `\x00` bytes in strings
|
|
66
|
+
allow_x00: bool = True
|
|
67
|
+
# Generate strings using the given codec
|
|
68
|
+
codec: str | None = "utf-8"
|
schemathesis/hooks.py
CHANGED
|
@@ -5,7 +5,7 @@ from copy import deepcopy
|
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from enum import Enum, unique
|
|
7
7
|
from functools import partial
|
|
8
|
-
from typing import TYPE_CHECKING, Any, Callable, ClassVar, DefaultDict,
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Callable, ClassVar, DefaultDict, cast
|
|
9
9
|
|
|
10
10
|
from .types import GenericTest
|
|
11
11
|
from .internal.deprecation import deprecated_property
|
|
@@ -27,7 +27,7 @@ class HookScope(Enum):
|
|
|
27
27
|
@dataclass
|
|
28
28
|
class RegisteredHook:
|
|
29
29
|
signature: inspect.Signature
|
|
30
|
-
scopes:
|
|
30
|
+
scopes: list[HookScope]
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
@dataclass
|
|
@@ -38,10 +38,10 @@ class HookContext:
|
|
|
38
38
|
Might be absent in some cases.
|
|
39
39
|
"""
|
|
40
40
|
|
|
41
|
-
operation:
|
|
41
|
+
operation: APIOperation | None = None
|
|
42
42
|
|
|
43
43
|
@deprecated_property(removed_in="4.0", replacement="operation")
|
|
44
|
-
def endpoint(self) ->
|
|
44
|
+
def endpoint(self) -> APIOperation | None:
|
|
45
45
|
return self.operation
|
|
46
46
|
|
|
47
47
|
|
|
@@ -53,10 +53,10 @@ class HookDispatcher:
|
|
|
53
53
|
"""
|
|
54
54
|
|
|
55
55
|
scope: HookScope
|
|
56
|
-
_hooks: DefaultDict[str,
|
|
57
|
-
_specs: ClassVar[
|
|
56
|
+
_hooks: DefaultDict[str, list[Callable]] = field(default_factory=lambda: defaultdict(list))
|
|
57
|
+
_specs: ClassVar[dict[str, RegisteredHook]] = {}
|
|
58
58
|
|
|
59
|
-
def register(self, hook:
|
|
59
|
+
def register(self, hook: str | Callable) -> Callable:
|
|
60
60
|
"""Register a new hook.
|
|
61
61
|
|
|
62
62
|
:param hook: Either a hook function or a string.
|
|
@@ -87,7 +87,7 @@ class HookDispatcher:
|
|
|
87
87
|
return decorator
|
|
88
88
|
return self.register_hook_with_name(hook, hook.__name__)
|
|
89
89
|
|
|
90
|
-
def merge(self, other:
|
|
90
|
+
def merge(self, other: HookDispatcher) -> HookDispatcher:
|
|
91
91
|
"""Merge two dispatches together.
|
|
92
92
|
|
|
93
93
|
The resulting dispatcher will call the `self` hooks first.
|
|
@@ -99,7 +99,7 @@ class HookDispatcher:
|
|
|
99
99
|
instance._hooks = all_hooks
|
|
100
100
|
return instance
|
|
101
101
|
|
|
102
|
-
def apply(self, hook: Callable, *, name:
|
|
102
|
+
def apply(self, hook: Callable, *, name: str | None = None) -> Callable[[Callable], Callable]:
|
|
103
103
|
"""Register hook to run only on one test function.
|
|
104
104
|
|
|
105
105
|
:param hook: A hook function.
|
|
@@ -130,7 +130,7 @@ class HookDispatcher:
|
|
|
130
130
|
return decorator
|
|
131
131
|
|
|
132
132
|
@classmethod
|
|
133
|
-
def add_dispatcher(cls, func: GenericTest) ->
|
|
133
|
+
def add_dispatcher(cls, func: GenericTest) -> HookDispatcher:
|
|
134
134
|
"""Attach a new dispatcher instance to the test if it is not already present."""
|
|
135
135
|
if not hasattr(func, "_schemathesis_hooks"):
|
|
136
136
|
func._schemathesis_hooks = cls(scope=HookScope.TEST) # type: ignore
|
|
@@ -143,7 +143,7 @@ class HookDispatcher:
|
|
|
143
143
|
return hook
|
|
144
144
|
|
|
145
145
|
@classmethod
|
|
146
|
-
def register_spec(cls, scopes:
|
|
146
|
+
def register_spec(cls, scopes: list[HookScope]) -> Callable:
|
|
147
147
|
"""Register hook specification.
|
|
148
148
|
|
|
149
149
|
All hooks, registered with `register` should comply with corresponding registered specs.
|
|
@@ -173,10 +173,10 @@ class HookDispatcher:
|
|
|
173
173
|
f"Hook '{name}' takes {len(spec.signature.parameters)} arguments but {len(signature.parameters)} is defined"
|
|
174
174
|
)
|
|
175
175
|
|
|
176
|
-
def collect_statistic(self) ->
|
|
176
|
+
def collect_statistic(self) -> dict[str, int]:
|
|
177
177
|
return {name: len(hooks) for name, hooks in self._hooks.items()}
|
|
178
178
|
|
|
179
|
-
def get_all_by_name(self, name: str) ->
|
|
179
|
+
def get_all_by_name(self, name: str) -> list[Callable]:
|
|
180
180
|
"""Get a list of hooks registered for a name."""
|
|
181
181
|
return self._hooks.get(name, [])
|
|
182
182
|
|
|
@@ -225,9 +225,9 @@ class HookDispatcher:
|
|
|
225
225
|
|
|
226
226
|
|
|
227
227
|
def apply_to_all_dispatchers(
|
|
228
|
-
operation:
|
|
228
|
+
operation: APIOperation,
|
|
229
229
|
context: HookContext,
|
|
230
|
-
hooks:
|
|
230
|
+
hooks: HookDispatcher | None,
|
|
231
231
|
strategy: st.SearchStrategy,
|
|
232
232
|
container: str,
|
|
233
233
|
) -> st.SearchStrategy:
|
|
@@ -287,32 +287,32 @@ def before_generate_body(context: HookContext, strategy: st.SearchStrategy) -> s
|
|
|
287
287
|
|
|
288
288
|
|
|
289
289
|
@all_scopes
|
|
290
|
-
def before_generate_case(context: HookContext, strategy: st.SearchStrategy[
|
|
290
|
+
def before_generate_case(context: HookContext, strategy: st.SearchStrategy[Case]) -> st.SearchStrategy[Case]:
|
|
291
291
|
"""Called on a strategy that generates ``Case`` instances."""
|
|
292
292
|
|
|
293
293
|
|
|
294
294
|
@all_scopes
|
|
295
|
-
def before_process_path(context: HookContext, path: str, methods:
|
|
295
|
+
def before_process_path(context: HookContext, path: str, methods: dict[str, Any]) -> None:
|
|
296
296
|
"""Called before API path is processed."""
|
|
297
297
|
|
|
298
298
|
|
|
299
299
|
@all_scopes
|
|
300
|
-
def filter_operations(context: HookContext) ->
|
|
300
|
+
def filter_operations(context: HookContext) -> bool | None:
|
|
301
301
|
"""Decide whether testing of this particular API operation should be skipped or not."""
|
|
302
302
|
|
|
303
303
|
|
|
304
304
|
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
305
|
-
def before_load_schema(context: HookContext, raw_schema:
|
|
305
|
+
def before_load_schema(context: HookContext, raw_schema: dict[str, Any]) -> None:
|
|
306
306
|
"""Called before schema instance is created."""
|
|
307
307
|
|
|
308
308
|
|
|
309
309
|
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
310
|
-
def after_load_schema(context: HookContext, schema:
|
|
310
|
+
def after_load_schema(context: HookContext, schema: BaseSchema) -> None:
|
|
311
311
|
"""Called after schema instance is created."""
|
|
312
312
|
|
|
313
313
|
|
|
314
314
|
@all_scopes
|
|
315
|
-
def before_add_examples(context: HookContext, examples:
|
|
315
|
+
def before_add_examples(context: HookContext, examples: list[Case]) -> None:
|
|
316
316
|
"""Called before explicit examples are added to a test via `@example` decorator.
|
|
317
317
|
|
|
318
318
|
`examples` is a list that could be extended with examples provided by the user.
|
|
@@ -320,12 +320,12 @@ def before_add_examples(context: HookContext, examples: List["Case"]) -> None:
|
|
|
320
320
|
|
|
321
321
|
|
|
322
322
|
@all_scopes
|
|
323
|
-
def before_init_operation(context: HookContext, operation:
|
|
323
|
+
def before_init_operation(context: HookContext, operation: APIOperation) -> None:
|
|
324
324
|
"""Allows you to customize a newly created API operation."""
|
|
325
325
|
|
|
326
326
|
|
|
327
327
|
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
328
|
-
def add_case(context: HookContext, case:
|
|
328
|
+
def add_case(context: HookContext, case: Case, response: GenericResponse) -> Case | None:
|
|
329
329
|
"""Creates an additional test per API operation. If this hook returns None, no additional test created.
|
|
330
330
|
|
|
331
331
|
Called with a copy of the original case object and the server's response to the original case.
|
|
@@ -333,7 +333,7 @@ def add_case(context: HookContext, case: "Case", response: GenericResponse) -> O
|
|
|
333
333
|
|
|
334
334
|
|
|
335
335
|
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
336
|
-
def before_call(context: HookContext, case:
|
|
336
|
+
def before_call(context: HookContext, case: Case) -> None:
|
|
337
337
|
"""Called before every network call in CLI tests.
|
|
338
338
|
|
|
339
339
|
Use cases:
|
|
@@ -343,7 +343,7 @@ def before_call(context: HookContext, case: "Case") -> None:
|
|
|
343
343
|
|
|
344
344
|
|
|
345
345
|
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
346
|
-
def after_call(context: HookContext, case:
|
|
346
|
+
def after_call(context: HookContext, case: Case, response: GenericResponse) -> None:
|
|
347
347
|
"""Called after every network call in CLI tests.
|
|
348
348
|
|
|
349
349
|
Note that you need to modify the response in-place.
|