schemathesis 3.13.0__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.
Files changed (245) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1016
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +683 -247
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +27 -0
  127. schemathesis/specs/graphql/scalars.py +86 -0
  128. schemathesis/specs/graphql/schemas.py +395 -123
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +578 -317
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +753 -74
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +117 -68
  154. schemathesis/specs/openapi/negative/mutations.py +294 -104
  155. schemathesis/specs/openapi/negative/utils.py +3 -6
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +648 -650
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +404 -69
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -41
  189. schemathesis/_hypothesis.py +0 -115
  190. schemathesis/cli/callbacks.py +0 -188
  191. schemathesis/cli/cassettes.py +0 -253
  192. schemathesis/cli/context.py +0 -36
  193. schemathesis/cli/debug.py +0 -21
  194. schemathesis/cli/handlers.py +0 -11
  195. schemathesis/cli/junitxml.py +0 -41
  196. schemathesis/cli/options.py +0 -51
  197. schemathesis/cli/output/__init__.py +0 -1
  198. schemathesis/cli/output/default.py +0 -508
  199. schemathesis/cli/output/short.py +0 -40
  200. schemathesis/constants.py +0 -79
  201. schemathesis/exceptions.py +0 -207
  202. schemathesis/extra/_aiohttp.py +0 -27
  203. schemathesis/extra/_flask.py +0 -10
  204. schemathesis/extra/_server.py +0 -16
  205. schemathesis/extra/pytest_plugin.py +0 -216
  206. schemathesis/failures.py +0 -131
  207. schemathesis/fixups/__init__.py +0 -29
  208. schemathesis/fixups/fast_api.py +0 -30
  209. schemathesis/lazy.py +0 -227
  210. schemathesis/models.py +0 -1041
  211. schemathesis/parameters.py +0 -88
  212. schemathesis/runner/__init__.py +0 -460
  213. schemathesis/runner/events.py +0 -240
  214. schemathesis/runner/impl/__init__.py +0 -3
  215. schemathesis/runner/impl/core.py +0 -755
  216. schemathesis/runner/impl/solo.py +0 -85
  217. schemathesis/runner/impl/threadpool.py +0 -367
  218. schemathesis/runner/serialization.py +0 -189
  219. schemathesis/serializers.py +0 -233
  220. schemathesis/service/__init__.py +0 -3
  221. schemathesis/service/client.py +0 -46
  222. schemathesis/service/constants.py +0 -12
  223. schemathesis/service/events.py +0 -39
  224. schemathesis/service/handler.py +0 -39
  225. schemathesis/service/models.py +0 -7
  226. schemathesis/service/serialization.py +0 -153
  227. schemathesis/service/worker.py +0 -40
  228. schemathesis/specs/graphql/loaders.py +0 -215
  229. schemathesis/specs/openapi/constants.py +0 -7
  230. schemathesis/specs/openapi/expressions/context.py +0 -12
  231. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  232. schemathesis/specs/openapi/filters.py +0 -44
  233. schemathesis/specs/openapi/links.py +0 -302
  234. schemathesis/specs/openapi/loaders.py +0 -453
  235. schemathesis/specs/openapi/parameters.py +0 -413
  236. schemathesis/specs/openapi/security.py +0 -129
  237. schemathesis/specs/openapi/validation.py +0 -24
  238. schemathesis/stateful.py +0 -349
  239. schemathesis/targets.py +0 -32
  240. schemathesis/types.py +0 -38
  241. schemathesis/utils.py +0 -436
  242. schemathesis-3.13.0.dist-info/METADATA +0 -202
  243. schemathesis-3.13.0.dist-info/RECORD +0 -91
  244. schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
  245. /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 copy import deepcopy
5
+ from contextlib import contextmanager
6
+ from dataclasses import dataclass, field
4
7
  from enum import Enum, unique
5
- from typing import TYPE_CHECKING, Any, Callable, DefaultDict, Dict, List, Optional, Union, cast
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 .types import GenericTest
11
- from .utils import GenericResponse, deprecated_property
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 .models import APIOperation, Case
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
- @attr.s(slots=True) # pragma: no mutate
31
+ @dataclass
25
32
  class RegisteredHook:
26
- signature: inspect.Signature = attr.ib() # pragma: no mutate
27
- scopes: List[HookScope] = attr.ib() # pragma: no mutate
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
- @attr.s(slots=True) # pragma: no mutate
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
- :ivar Optional[APIOperation] operation: API operation that is currently being processed.
35
- Might be absent in some cases.
36
- """
45
+ operation: APIOperation | None
46
+ """API operation that is currently being processed."""
37
47
 
38
- operation: Optional["APIOperation"] = attr.ib(default=None) # pragma: no mutate
48
+ __slots__ = ("operation",)
39
49
 
40
- @deprecated_property(removed_in="4.0", replacement="operation")
41
- def endpoint(self) -> Optional["APIOperation"]:
42
- return self.operation
50
+ def __init__(self, *, operation: APIOperation | None = None) -> None:
51
+ self.operation = operation
43
52
 
44
53
 
45
- @attr.s(slots=True) # pragma: no mutate
46
- class HookDispatcher:
47
- """Generic hook dispatcher.
54
+ def to_filterable_hook(dispatcher: HookDispatcher) -> Callable:
55
+ filter_used = False
56
+ filter_set = FilterSet()
48
57
 
49
- Provides a mechanism to extend Schemathesis in registered hook points.
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
- scope: HookScope = attr.ib() # pragma: no mutate
53
- _hooks: DefaultDict[str, List[Callable]] = attr.ib(factory=lambda: defaultdict(list)) # pragma: no mutate
54
- _specs: Dict[str, RegisteredHook] = {} # pragma: no mutate
66
+ def register(hook: str | Callable) -> Callable:
67
+ nonlocal filter_set
55
68
 
56
- def register(self, hook: Union[str, Callable]) -> Callable:
57
- """Register a new hook.
69
+ if filter_used:
70
+ with _reset_on_error():
71
+ validate_filterable_hook(hook)
58
72
 
59
- :param hook: Either a hook function or a string.
73
+ if isinstance(hook, str):
60
74
 
61
- Can be used as a decorator in two forms.
62
- Without arguments for registering hooks and autodetecting their names:
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
- .. code-block:: python
83
+ init_filter_set(decorator)
84
+ return decorator
65
85
 
66
- @schemathesis.hooks.register
67
- def before_generate_query(context, strategy):
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
- With a hook name as the first argument:
90
+ def init_filter_set(target: Callable) -> FilterSet:
91
+ nonlocal filter_used
71
92
 
72
- .. code-block:: python
93
+ filter_used = False
94
+ filter_set = FilterSet()
73
95
 
74
- @schemathesis.hooks.register("before_generate_query")
75
- def hook(context, strategy):
76
- ...
77
- """
78
- if isinstance(hook, str):
96
+ def include(*args: Any, **kwargs: Any) -> None:
97
+ nonlocal filter_used
79
98
 
80
- def decorator(func: Callable) -> Callable:
81
- hook_name = cast(str, hook)
82
- return self.register_hook_with_name(func, hook_name)
99
+ filter_used = True
100
+ with _reset_on_error():
101
+ filter_set.include(*args, **kwargs)
83
102
 
84
- return decorator
85
- return self.register_hook_with_name(hook, hook.__name__)
103
+ def exclude(*args: Any, **kwargs: Any) -> None:
104
+ nonlocal filter_used
86
105
 
87
- def merge(self, other: "HookDispatcher") -> "HookDispatcher":
88
- """Merge two dispatches together.
106
+ filter_used = True
107
+ with _reset_on_error():
108
+ filter_set.exclude(*args, **kwargs)
89
109
 
90
- The resulting dispatcher will call the `self` hooks first.
91
- """
92
- all_hooks = deepcopy(self._hooks)
93
- for name, hooks in other._hooks.items():
94
- all_hooks[name].extend(hooks)
95
- instance = self.__class__(scope=self.scope)
96
- instance._hooks = all_hooks
97
- return instance
98
-
99
- def apply(self, hook: Callable, *, name: Optional[str] = None) -> Callable[[Callable], Callable]:
100
- """Register hook to run only on one test function.
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
- :param hook: A hook function.
103
- :param Optional[str] name: A hook name.
125
+ scope: HookScope
126
+ _hooks: defaultdict[str, list[Callable]] = field(default_factory=lambda: defaultdict(list))
127
+ _specs: ClassVar[dict[str, RegisteredHook]] = {}
104
128
 
105
- .. code-block:: python
129
+ @property
130
+ def hook(self) -> Callable:
131
+ return to_filterable_hook(self)
106
132
 
107
- def before_generate_query(context, strategy):
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(before_generate_query)
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: GenericTest) -> GenericTest:
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: GenericTest) -> "HookDispatcher":
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 hasattr(func, "_schemathesis_hooks"):
134
- func._schemathesis_hooks = cls(scope=HookScope.TEST) # type: ignore
135
- return func._schemathesis_hooks # type: ignore
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: List[HookScope]) -> Callable:
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) -> List[Callable]:
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 dispatch(self, name: str, context: HookContext, *args: Any, **kwargs: Any) -> None:
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(context, *args, **kwargs)
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["Case"]) -> st.SearchStrategy["Case"]:
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: Dict[str, Any]) -> None:
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: Dict[str, Any]) -> None:
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: List["Case"]) -> None:
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: "APIOperation") -> None:
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 add_case(context: HookContext, case: "Case", response: GenericResponse) -> Optional["Case"]:
258
- """Creates an additional test per API operation. If this hook returns None, no additional test created.
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
- Called with a copy of the original case object and the server's response to the original case.
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
+ ]