schemathesis 3.15.4__py3-none-any.whl → 4.4.2__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 +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1219
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +682 -257
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +26 -2
- schemathesis/specs/graphql/scalars.py +77 -12
- schemathesis/specs/graphql/schemas.py +367 -148
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +555 -318
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +748 -82
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +93 -73
- schemathesis/specs/openapi/negative/mutations.py +294 -103
- schemathesis/specs/openapi/negative/utils.py +0 -9
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +647 -666
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +403 -68
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -57
- schemathesis/_hypothesis.py +0 -123
- schemathesis/auth.py +0 -214
- schemathesis/cli/callbacks.py +0 -240
- schemathesis/cli/cassettes.py +0 -351
- schemathesis/cli/context.py +0 -38
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -70
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -521
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -88
- schemathesis/exceptions.py +0 -257
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -251
- schemathesis/failures.py +0 -145
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/graphql.py +0 -5
- schemathesis/internal.py +0 -6
- schemathesis/lazy.py +0 -301
- schemathesis/models.py +0 -1113
- schemathesis/parameters.py +0 -91
- schemathesis/runner/__init__.py +0 -470
- schemathesis/runner/events.py +0 -242
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -791
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -206
- schemathesis/serializers.py +0 -253
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -10
- schemathesis/service/client.py +0 -62
- schemathesis/service/constants.py +0 -25
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -46
- schemathesis/service/hosts.py +0 -74
- schemathesis/service/metadata.py +0 -42
- schemathesis/service/models.py +0 -21
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/worker.py +0 -39
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -303
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -430
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -358
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -475
- schemathesis-3.15.4.dist-info/METADATA +0 -202
- schemathesis-3.15.4.dist-info/RECORD +0 -99
- schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
schemathesis/hooks.py
CHANGED
|
@@ -1,126 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import inspect
|
|
2
4
|
from collections import defaultdict
|
|
3
|
-
from
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from dataclasses import dataclass, field
|
|
4
7
|
from enum import Enum, unique
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
import attr
|
|
8
|
-
from hypothesis import strategies as st
|
|
8
|
+
from functools import lru_cache, partial
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generator, cast
|
|
9
10
|
|
|
10
|
-
from .
|
|
11
|
-
from .
|
|
11
|
+
from schemathesis.core.marks import Mark
|
|
12
|
+
from schemathesis.core.transport import Response
|
|
13
|
+
from schemathesis.filters import FilterSet, attach_filter_chain
|
|
12
14
|
|
|
13
15
|
if TYPE_CHECKING:
|
|
14
|
-
from
|
|
16
|
+
from hypothesis import strategies as st
|
|
17
|
+
|
|
18
|
+
from schemathesis.generation.case import Case
|
|
19
|
+
from schemathesis.schemas import APIOperation, BaseSchema
|
|
20
|
+
|
|
21
|
+
HookDispatcherMark = Mark["HookDispatcher"](attr_name="hook_dispatcher")
|
|
15
22
|
|
|
16
23
|
|
|
17
24
|
@unique
|
|
18
|
-
class HookScope(Enum):
|
|
25
|
+
class HookScope(int, Enum):
|
|
19
26
|
GLOBAL = 1
|
|
20
27
|
SCHEMA = 2
|
|
21
28
|
TEST = 3
|
|
22
29
|
|
|
23
30
|
|
|
24
|
-
@
|
|
31
|
+
@dataclass
|
|
25
32
|
class RegisteredHook:
|
|
26
|
-
signature: inspect.Signature
|
|
27
|
-
scopes:
|
|
33
|
+
signature: inspect.Signature
|
|
34
|
+
scopes: list[HookScope]
|
|
35
|
+
|
|
36
|
+
__slots__ = ("signature", "scopes")
|
|
28
37
|
|
|
38
|
+
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
|
29
39
|
|
|
30
|
-
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
31
42
|
class HookContext:
|
|
32
|
-
"""A context that is passed to some hook functions.
|
|
43
|
+
"""A context that is passed to some hook functions."""
|
|
33
44
|
|
|
34
|
-
:
|
|
35
|
-
|
|
36
|
-
"""
|
|
45
|
+
operation: APIOperation | None
|
|
46
|
+
"""API operation that is currently being processed."""
|
|
37
47
|
|
|
38
|
-
|
|
48
|
+
__slots__ = ("operation",)
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return self.operation
|
|
50
|
+
def __init__(self, *, operation: APIOperation | None = None) -> None:
|
|
51
|
+
self.operation = operation
|
|
43
52
|
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
def to_filterable_hook(dispatcher: HookDispatcher) -> Callable:
|
|
55
|
+
filter_used = False
|
|
56
|
+
filter_set = FilterSet()
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
@contextmanager
|
|
59
|
+
def _reset_on_error() -> Generator:
|
|
60
|
+
try:
|
|
61
|
+
yield
|
|
62
|
+
except Exception:
|
|
63
|
+
filter_set.clear()
|
|
64
|
+
raise
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
_specs: Dict[str, RegisteredHook] = {} # pragma: no mutate
|
|
66
|
+
def register(hook: str | Callable) -> Callable:
|
|
67
|
+
nonlocal filter_set
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
if filter_used:
|
|
70
|
+
with _reset_on_error():
|
|
71
|
+
validate_filterable_hook(hook)
|
|
58
72
|
|
|
59
|
-
|
|
73
|
+
if isinstance(hook, str):
|
|
60
74
|
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
def decorator(func: Callable) -> Callable:
|
|
76
|
+
hook_name = cast(str, hook)
|
|
77
|
+
if filter_used:
|
|
78
|
+
with _reset_on_error():
|
|
79
|
+
validate_filterable_hook(hook)
|
|
80
|
+
func.filter_set = filter_set # type: ignore[attr-defined]
|
|
81
|
+
return dispatcher.register_hook_with_name(func, hook_name)
|
|
63
82
|
|
|
64
|
-
|
|
83
|
+
init_filter_set(decorator)
|
|
84
|
+
return decorator
|
|
65
85
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
hook.filter_set = filter_set # type: ignore[attr-defined]
|
|
87
|
+
init_filter_set(register)
|
|
88
|
+
return dispatcher.register_hook_with_name(hook, hook.__name__)
|
|
69
89
|
|
|
70
|
-
|
|
90
|
+
def init_filter_set(target: Callable) -> FilterSet:
|
|
91
|
+
nonlocal filter_used
|
|
71
92
|
|
|
72
|
-
|
|
93
|
+
filter_used = False
|
|
94
|
+
filter_set = FilterSet()
|
|
73
95
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
...
|
|
77
|
-
"""
|
|
78
|
-
if isinstance(hook, str):
|
|
96
|
+
def include(*args: Any, **kwargs: Any) -> None:
|
|
97
|
+
nonlocal filter_used
|
|
79
98
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
filter_used = True
|
|
100
|
+
with _reset_on_error():
|
|
101
|
+
filter_set.include(*args, **kwargs)
|
|
83
102
|
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
def exclude(*args: Any, **kwargs: Any) -> None:
|
|
104
|
+
nonlocal filter_used
|
|
86
105
|
|
|
87
|
-
|
|
88
|
-
|
|
106
|
+
filter_used = True
|
|
107
|
+
with _reset_on_error():
|
|
108
|
+
filter_set.exclude(*args, **kwargs)
|
|
89
109
|
|
|
90
|
-
|
|
91
|
-
""
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
attach_filter_chain(target, "apply_to", include)
|
|
111
|
+
attach_filter_chain(target, "skip_for", exclude)
|
|
112
|
+
return filter_set
|
|
113
|
+
|
|
114
|
+
filter_set = init_filter_set(register)
|
|
115
|
+
return register
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class HookDispatcher:
|
|
120
|
+
"""Generic hook dispatcher.
|
|
121
|
+
|
|
122
|
+
Provides a mechanism to extend Schemathesis in registered hook points.
|
|
123
|
+
"""
|
|
101
124
|
|
|
102
|
-
|
|
103
|
-
|
|
125
|
+
scope: HookScope
|
|
126
|
+
_hooks: defaultdict[str, list[Callable]] = field(default_factory=lambda: defaultdict(list))
|
|
127
|
+
_specs: ClassVar[dict[str, RegisteredHook]] = {}
|
|
104
128
|
|
|
105
|
-
|
|
129
|
+
@property
|
|
130
|
+
def hook(self) -> Callable:
|
|
131
|
+
return to_filterable_hook(self)
|
|
106
132
|
|
|
107
|
-
|
|
133
|
+
def apply(self, hook: Callable, *, name: str | None = None) -> Callable[[Callable], Callable]:
|
|
134
|
+
"""Register hook to run only on one test function.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
hook: A hook function.
|
|
138
|
+
name: A hook name.
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
```python
|
|
142
|
+
def filter_query(ctx, value):
|
|
108
143
|
...
|
|
109
144
|
|
|
110
145
|
|
|
111
|
-
@schema.hooks.apply(
|
|
146
|
+
@schema.hooks.apply(filter_query)
|
|
112
147
|
@schema.parametrize()
|
|
113
148
|
def test_api(case):
|
|
114
149
|
...
|
|
150
|
+
```
|
|
115
151
|
|
|
116
152
|
"""
|
|
117
|
-
|
|
118
153
|
if name is None:
|
|
119
154
|
hook_name = hook.__name__
|
|
120
155
|
else:
|
|
121
156
|
hook_name = name
|
|
122
157
|
|
|
123
|
-
def decorator(func:
|
|
158
|
+
def decorator(func: Callable) -> Callable:
|
|
124
159
|
dispatcher = self.add_dispatcher(func)
|
|
125
160
|
dispatcher.register_hook_with_name(hook, hook_name)
|
|
126
161
|
return func
|
|
@@ -128,11 +163,13 @@ class HookDispatcher:
|
|
|
128
163
|
return decorator
|
|
129
164
|
|
|
130
165
|
@classmethod
|
|
131
|
-
def add_dispatcher(cls, func:
|
|
166
|
+
def add_dispatcher(cls, func: Callable) -> HookDispatcher:
|
|
132
167
|
"""Attach a new dispatcher instance to the test if it is not already present."""
|
|
133
|
-
if not
|
|
134
|
-
func
|
|
135
|
-
|
|
168
|
+
if not HookDispatcherMark.is_set(func):
|
|
169
|
+
HookDispatcherMark.set(func, cls(scope=HookScope.TEST))
|
|
170
|
+
dispatcher = HookDispatcherMark.get(func)
|
|
171
|
+
assert dispatcher is not None
|
|
172
|
+
return dispatcher
|
|
136
173
|
|
|
137
174
|
def register_hook_with_name(self, hook: Callable, name: str) -> Callable:
|
|
138
175
|
"""A helper for hooks registration."""
|
|
@@ -141,7 +178,7 @@ class HookDispatcher:
|
|
|
141
178
|
return hook
|
|
142
179
|
|
|
143
180
|
@classmethod
|
|
144
|
-
def register_spec(cls, scopes:
|
|
181
|
+
def register_spec(cls, scopes: list[HookScope]) -> Callable:
|
|
145
182
|
"""Register hook specification.
|
|
146
183
|
|
|
147
184
|
All hooks, registered with `register` should comply with corresponding registered specs.
|
|
@@ -171,20 +208,52 @@ class HookDispatcher:
|
|
|
171
208
|
f"Hook '{name}' takes {len(spec.signature.parameters)} arguments but {len(signature.parameters)} is defined"
|
|
172
209
|
)
|
|
173
210
|
|
|
174
|
-
def get_all_by_name(self, name: str) ->
|
|
211
|
+
def get_all_by_name(self, name: str) -> list[Callable]:
|
|
175
212
|
"""Get a list of hooks registered for a name."""
|
|
176
213
|
return self._hooks.get(name, [])
|
|
177
214
|
|
|
178
|
-
def
|
|
215
|
+
def get_all(self) -> dict[str, list[Callable]]:
|
|
216
|
+
return self._hooks
|
|
217
|
+
|
|
218
|
+
def apply_to_container(
|
|
219
|
+
self, strategy: st.SearchStrategy, container: str, context: HookContext
|
|
220
|
+
) -> st.SearchStrategy:
|
|
221
|
+
for hook in self.get_all_by_name(f"before_generate_{container}"):
|
|
222
|
+
if _should_skip_hook(hook, context):
|
|
223
|
+
continue
|
|
224
|
+
strategy = hook(context, strategy)
|
|
225
|
+
for hook in self.get_all_by_name(f"filter_{container}"):
|
|
226
|
+
if _should_skip_hook(hook, context):
|
|
227
|
+
continue
|
|
228
|
+
hook = partial(hook, context)
|
|
229
|
+
strategy = strategy.filter(hook)
|
|
230
|
+
for hook in self.get_all_by_name(f"map_{container}"):
|
|
231
|
+
if _should_skip_hook(hook, context):
|
|
232
|
+
continue
|
|
233
|
+
hook = partial(hook, context)
|
|
234
|
+
strategy = strategy.map(hook)
|
|
235
|
+
for hook in self.get_all_by_name(f"flatmap_{container}"):
|
|
236
|
+
if _should_skip_hook(hook, context):
|
|
237
|
+
continue
|
|
238
|
+
hook = partial(hook, context)
|
|
239
|
+
strategy = strategy.flatmap(hook)
|
|
240
|
+
return strategy
|
|
241
|
+
|
|
242
|
+
def dispatch(
|
|
243
|
+
self, name: str, context: HookContext, *args: Any, _with_dual_style_kwargs: bool = False, **kwargs: Any
|
|
244
|
+
) -> None:
|
|
179
245
|
"""Run all hooks for the given name."""
|
|
180
246
|
for hook in self.get_all_by_name(name):
|
|
181
|
-
hook
|
|
247
|
+
if _should_skip_hook(hook, context):
|
|
248
|
+
continue
|
|
249
|
+
# NOTE: It is a backward-compat shim to support calling `before_call` with `**kwargs` OR with `kwargs`.
|
|
250
|
+
if _with_dual_style_kwargs and not has_var_keyword(hook):
|
|
251
|
+
hook(context, *args, kwargs)
|
|
252
|
+
else:
|
|
253
|
+
hook(context, *args, **kwargs)
|
|
182
254
|
|
|
183
255
|
def unregister(self, hook: Callable) -> None:
|
|
184
|
-
"""Unregister a specific hook.
|
|
185
|
-
|
|
186
|
-
:param hook: A hook function to unregister.
|
|
187
|
-
"""
|
|
256
|
+
"""Unregister a specific hook."""
|
|
188
257
|
# It removes this function from all places
|
|
189
258
|
for hooks in self._hooks.values():
|
|
190
259
|
hooks[:] = [item for item in hooks if item is not hook]
|
|
@@ -197,9 +266,56 @@ class HookDispatcher:
|
|
|
197
266
|
self._hooks = defaultdict(list)
|
|
198
267
|
|
|
199
268
|
|
|
269
|
+
@lru_cache(maxsize=16)
|
|
270
|
+
def has_var_keyword(hook: Callable) -> bool:
|
|
271
|
+
"""Check if hook function accepts **kwargs."""
|
|
272
|
+
return any(p.kind == inspect.Parameter.VAR_KEYWORD for p in inspect.signature(hook).parameters.values())
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _should_skip_hook(hook: Callable, ctx: HookContext) -> bool:
|
|
276
|
+
filter_set = getattr(hook, "filter_set", None)
|
|
277
|
+
return filter_set is not None and ctx.operation is not None and not filter_set.match(ctx)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def apply_to_all_dispatchers(
|
|
281
|
+
operation: APIOperation,
|
|
282
|
+
context: HookContext,
|
|
283
|
+
hooks: HookDispatcher | None,
|
|
284
|
+
strategy: st.SearchStrategy,
|
|
285
|
+
container: str,
|
|
286
|
+
) -> st.SearchStrategy:
|
|
287
|
+
"""Apply all hooks related to the given location."""
|
|
288
|
+
strategy = GLOBAL_HOOK_DISPATCHER.apply_to_container(strategy, container, context)
|
|
289
|
+
strategy = operation.schema.hooks.apply_to_container(strategy, container, context)
|
|
290
|
+
if hooks is not None:
|
|
291
|
+
strategy = hooks.apply_to_container(strategy, container, context)
|
|
292
|
+
return strategy
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def validate_filterable_hook(hook: str | Callable) -> None:
|
|
296
|
+
if callable(hook):
|
|
297
|
+
name = hook.__name__
|
|
298
|
+
else:
|
|
299
|
+
name = hook
|
|
300
|
+
if name in ("before_process_path", "before_load_schema", "after_load_schema"):
|
|
301
|
+
raise ValueError(f"Filters are not applicable to this hook: `{name}`")
|
|
302
|
+
|
|
303
|
+
|
|
200
304
|
all_scopes = HookDispatcher.register_spec(list(HookScope))
|
|
201
305
|
|
|
202
306
|
|
|
307
|
+
for action in ("filter", "map", "flatmap"):
|
|
308
|
+
for target in ("path_parameters", "query", "headers", "cookies", "body", "case"):
|
|
309
|
+
exec(
|
|
310
|
+
f"""
|
|
311
|
+
@all_scopes
|
|
312
|
+
def {action}_{target}(context: HookContext, {target}: Any) -> Any:
|
|
313
|
+
pass
|
|
314
|
+
""",
|
|
315
|
+
globals(),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
203
319
|
@all_scopes
|
|
204
320
|
def before_generate_path_parameters(context: HookContext, strategy: st.SearchStrategy) -> st.SearchStrategy:
|
|
205
321
|
"""Called on a strategy that generates values for ``path_parameters``."""
|
|
@@ -226,22 +342,27 @@ def before_generate_body(context: HookContext, strategy: st.SearchStrategy) -> s
|
|
|
226
342
|
|
|
227
343
|
|
|
228
344
|
@all_scopes
|
|
229
|
-
def before_generate_case(context: HookContext, strategy: st.SearchStrategy[
|
|
345
|
+
def before_generate_case(context: HookContext, strategy: st.SearchStrategy[Case]) -> st.SearchStrategy[Case]:
|
|
230
346
|
"""Called on a strategy that generates ``Case`` instances."""
|
|
231
347
|
|
|
232
348
|
|
|
233
349
|
@all_scopes
|
|
234
|
-
def before_process_path(context: HookContext, path: str, methods:
|
|
350
|
+
def before_process_path(context: HookContext, path: str, methods: dict[str, Any]) -> None:
|
|
235
351
|
"""Called before API path is processed."""
|
|
236
352
|
|
|
237
353
|
|
|
238
354
|
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
239
|
-
def before_load_schema(context: HookContext, raw_schema:
|
|
355
|
+
def before_load_schema(context: HookContext, raw_schema: dict[str, Any]) -> None:
|
|
240
356
|
"""Called before schema instance is created."""
|
|
241
357
|
|
|
242
358
|
|
|
359
|
+
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
360
|
+
def after_load_schema(context: HookContext, schema: BaseSchema) -> None:
|
|
361
|
+
"""Called after schema instance is created."""
|
|
362
|
+
|
|
363
|
+
|
|
243
364
|
@all_scopes
|
|
244
|
-
def before_add_examples(context: HookContext, examples:
|
|
365
|
+
def before_add_examples(context: HookContext, examples: list[Case]) -> None:
|
|
245
366
|
"""Called before explicit examples are added to a test via `@example` decorator.
|
|
246
367
|
|
|
247
368
|
`examples` is a list that could be extended with examples provided by the user.
|
|
@@ -249,21 +370,79 @@ def before_add_examples(context: HookContext, examples: List["Case"]) -> None:
|
|
|
249
370
|
|
|
250
371
|
|
|
251
372
|
@all_scopes
|
|
252
|
-
def before_init_operation(context: HookContext, operation:
|
|
373
|
+
def before_init_operation(context: HookContext, operation: APIOperation) -> None:
|
|
253
374
|
"""Allows you to customize a newly created API operation."""
|
|
254
375
|
|
|
255
376
|
|
|
256
377
|
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
257
|
-
def
|
|
258
|
-
"""
|
|
378
|
+
def before_call(context: HookContext, case: Case, kwargs: dict[str, Any]) -> None:
|
|
379
|
+
"""Called before every network call in CLI tests.
|
|
259
380
|
|
|
260
|
-
|
|
381
|
+
Use cases:
|
|
382
|
+
- Modification of `case`. For example, adding some pre-determined value to its query string.
|
|
383
|
+
- Logging
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
388
|
+
def after_call(context: HookContext, case: Case, response: Response) -> None:
|
|
389
|
+
"""Called after every network call in CLI tests.
|
|
390
|
+
|
|
391
|
+
Note that you need to modify the response in-place.
|
|
392
|
+
|
|
393
|
+
Use cases:
|
|
394
|
+
- Response post-processing, like modifying its payload.
|
|
395
|
+
- Logging
|
|
261
396
|
"""
|
|
262
397
|
|
|
263
398
|
|
|
264
399
|
GLOBAL_HOOK_DISPATCHER = HookDispatcher(scope=HookScope.GLOBAL)
|
|
265
400
|
dispatch = GLOBAL_HOOK_DISPATCHER.dispatch
|
|
266
401
|
get_all_by_name = GLOBAL_HOOK_DISPATCHER.get_all_by_name
|
|
267
|
-
register = GLOBAL_HOOK_DISPATCHER.register
|
|
268
402
|
unregister = GLOBAL_HOOK_DISPATCHER.unregister
|
|
269
403
|
unregister_all = GLOBAL_HOOK_DISPATCHER.unregister_all
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def hook(hook: str | Callable) -> Callable:
|
|
407
|
+
"""Register a new hook.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
hook: Either a hook function (autodetecting its name) or a string matching one of the supported hook names.
|
|
411
|
+
|
|
412
|
+
Example:
|
|
413
|
+
Can be used as a decorator in two ways:
|
|
414
|
+
|
|
415
|
+
1. Without arguments (auto-detect the hook name from the function name):
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
@schemathesis.hook
|
|
419
|
+
def filter_query(ctx, query):
|
|
420
|
+
\"\"\"Skip cases where query is None or invalid\"\"\"
|
|
421
|
+
return query and "user_id" in query
|
|
422
|
+
|
|
423
|
+
@schemathesis.hook
|
|
424
|
+
def before_call(ctx, case, **kwargs):
|
|
425
|
+
\"\"\"Modify headers before sending each request\"\"\"
|
|
426
|
+
if case.headers is None:
|
|
427
|
+
case.headers = {}
|
|
428
|
+
case.headers["X-Test-Mode"] = "true"
|
|
429
|
+
return None
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
2. With an explicit hook name as the first argument:
|
|
433
|
+
|
|
434
|
+
```python
|
|
435
|
+
@schemathesis.hook("map_headers")
|
|
436
|
+
def add_custom_header(ctx, headers):
|
|
437
|
+
\"\"\"Inject a test header into every request\"\"\"
|
|
438
|
+
if headers is None:
|
|
439
|
+
headers = {}
|
|
440
|
+
headers["X-Custom"] = "value"
|
|
441
|
+
return headers
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
"""
|
|
445
|
+
return GLOBAL_HOOK_DISPATCHER.hook(hook)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
hook.__dict__ = GLOBAL_HOOK_DISPATCHER.hook.__dict__
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from schemathesis.openapi.loaders import from_asgi, from_dict, from_file, from_path, from_url, from_wsgi
|
|
2
|
+
from schemathesis.specs.openapi import format, media_type
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"from_url",
|
|
6
|
+
"from_asgi",
|
|
7
|
+
"from_wsgi",
|
|
8
|
+
"from_file",
|
|
9
|
+
"from_path",
|
|
10
|
+
"from_dict",
|
|
11
|
+
"format",
|
|
12
|
+
"media_type",
|
|
13
|
+
]
|