schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__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 (229) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +26 -68
  3. schemathesis/checks.py +130 -60
  4. schemathesis/cli/__init__.py +5 -2105
  5. schemathesis/cli/commands/__init__.py +37 -0
  6. schemathesis/cli/commands/run/__init__.py +662 -0
  7. schemathesis/cli/commands/run/checks.py +80 -0
  8. schemathesis/cli/commands/run/context.py +117 -0
  9. schemathesis/cli/commands/run/events.py +30 -0
  10. schemathesis/cli/commands/run/executor.py +141 -0
  11. schemathesis/cli/commands/run/filters.py +202 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
  15. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1368 -0
  17. schemathesis/cli/commands/run/hypothesis.py +105 -0
  18. schemathesis/cli/commands/run/loaders.py +129 -0
  19. schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
  20. schemathesis/cli/constants.py +5 -58
  21. schemathesis/cli/core.py +17 -0
  22. schemathesis/cli/ext/fs.py +14 -0
  23. schemathesis/cli/ext/groups.py +55 -0
  24. schemathesis/cli/{options.py → ext/options.py} +37 -16
  25. schemathesis/cli/hooks.py +36 -0
  26. schemathesis/contrib/__init__.py +1 -3
  27. schemathesis/contrib/openapi/__init__.py +1 -3
  28. schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
  29. schemathesis/core/__init__.py +58 -0
  30. schemathesis/core/compat.py +25 -0
  31. schemathesis/core/control.py +2 -0
  32. schemathesis/core/curl.py +58 -0
  33. schemathesis/core/deserialization.py +65 -0
  34. schemathesis/core/errors.py +370 -0
  35. schemathesis/core/failures.py +315 -0
  36. schemathesis/core/fs.py +19 -0
  37. schemathesis/core/loaders.py +104 -0
  38. schemathesis/core/marks.py +66 -0
  39. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  40. schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
  41. schemathesis/core/output/sanitization.py +197 -0
  42. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  43. schemathesis/core/registries.py +31 -0
  44. schemathesis/core/transforms.py +113 -0
  45. schemathesis/core/transport.py +108 -0
  46. schemathesis/core/validation.py +38 -0
  47. schemathesis/core/version.py +7 -0
  48. schemathesis/engine/__init__.py +30 -0
  49. schemathesis/engine/config.py +59 -0
  50. schemathesis/engine/context.py +119 -0
  51. schemathesis/engine/control.py +36 -0
  52. schemathesis/engine/core.py +157 -0
  53. schemathesis/engine/errors.py +394 -0
  54. schemathesis/engine/events.py +243 -0
  55. schemathesis/engine/phases/__init__.py +66 -0
  56. schemathesis/{runner → engine/phases}/probes.py +49 -68
  57. schemathesis/engine/phases/stateful/__init__.py +66 -0
  58. schemathesis/engine/phases/stateful/_executor.py +301 -0
  59. schemathesis/engine/phases/stateful/context.py +85 -0
  60. schemathesis/engine/phases/unit/__init__.py +175 -0
  61. schemathesis/engine/phases/unit/_executor.py +322 -0
  62. schemathesis/engine/phases/unit/_pool.py +74 -0
  63. schemathesis/engine/recorder.py +246 -0
  64. schemathesis/errors.py +31 -0
  65. schemathesis/experimental/__init__.py +9 -40
  66. schemathesis/filters.py +7 -95
  67. schemathesis/generation/__init__.py +3 -3
  68. schemathesis/generation/case.py +190 -0
  69. schemathesis/generation/coverage.py +22 -22
  70. schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
  71. schemathesis/generation/hypothesis/builder.py +585 -0
  72. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  73. schemathesis/generation/hypothesis/given.py +66 -0
  74. schemathesis/generation/hypothesis/reporting.py +14 -0
  75. schemathesis/generation/hypothesis/strategies.py +16 -0
  76. schemathesis/generation/meta.py +115 -0
  77. schemathesis/generation/modes.py +28 -0
  78. schemathesis/generation/overrides.py +96 -0
  79. schemathesis/generation/stateful/__init__.py +20 -0
  80. schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
  81. schemathesis/generation/targets.py +69 -0
  82. schemathesis/graphql/__init__.py +15 -0
  83. schemathesis/graphql/checks.py +109 -0
  84. schemathesis/graphql/loaders.py +131 -0
  85. schemathesis/hooks.py +17 -62
  86. schemathesis/openapi/__init__.py +13 -0
  87. schemathesis/openapi/checks.py +387 -0
  88. schemathesis/openapi/generation/__init__.py +0 -0
  89. schemathesis/openapi/generation/filters.py +63 -0
  90. schemathesis/openapi/loaders.py +178 -0
  91. schemathesis/pytest/__init__.py +5 -0
  92. schemathesis/pytest/control_flow.py +7 -0
  93. schemathesis/pytest/lazy.py +273 -0
  94. schemathesis/pytest/loaders.py +12 -0
  95. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
  96. schemathesis/python/__init__.py +0 -0
  97. schemathesis/python/asgi.py +12 -0
  98. schemathesis/python/wsgi.py +12 -0
  99. schemathesis/schemas.py +456 -228
  100. schemathesis/specs/graphql/__init__.py +0 -1
  101. schemathesis/specs/graphql/_cache.py +1 -2
  102. schemathesis/specs/graphql/scalars.py +5 -3
  103. schemathesis/specs/graphql/schemas.py +122 -123
  104. schemathesis/specs/graphql/validation.py +11 -17
  105. schemathesis/specs/openapi/__init__.py +6 -1
  106. schemathesis/specs/openapi/_cache.py +1 -2
  107. schemathesis/specs/openapi/_hypothesis.py +97 -134
  108. schemathesis/specs/openapi/checks.py +238 -219
  109. schemathesis/specs/openapi/converter.py +4 -4
  110. schemathesis/specs/openapi/definitions.py +1 -1
  111. schemathesis/specs/openapi/examples.py +22 -20
  112. schemathesis/specs/openapi/expressions/__init__.py +11 -15
  113. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  114. schemathesis/specs/openapi/expressions/nodes.py +33 -32
  115. schemathesis/specs/openapi/formats.py +3 -2
  116. schemathesis/specs/openapi/links.py +123 -299
  117. schemathesis/specs/openapi/media_types.py +10 -12
  118. schemathesis/specs/openapi/negative/__init__.py +2 -1
  119. schemathesis/specs/openapi/negative/mutations.py +3 -2
  120. schemathesis/specs/openapi/parameters.py +8 -6
  121. schemathesis/specs/openapi/patterns.py +1 -1
  122. schemathesis/specs/openapi/references.py +11 -51
  123. schemathesis/specs/openapi/schemas.py +177 -191
  124. schemathesis/specs/openapi/security.py +1 -1
  125. schemathesis/specs/openapi/serialization.py +10 -6
  126. schemathesis/specs/openapi/stateful/__init__.py +97 -91
  127. schemathesis/transport/__init__.py +104 -0
  128. schemathesis/transport/asgi.py +26 -0
  129. schemathesis/transport/prepare.py +99 -0
  130. schemathesis/transport/requests.py +221 -0
  131. schemathesis/{_xml.py → transport/serialization.py} +69 -7
  132. schemathesis/transport/wsgi.py +165 -0
  133. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
  134. schemathesis-4.0.0a2.dist-info/RECORD +151 -0
  135. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
  136. schemathesis/_compat.py +0 -74
  137. schemathesis/_dependency_versions.py +0 -19
  138. schemathesis/_hypothesis.py +0 -559
  139. schemathesis/_override.py +0 -50
  140. schemathesis/_rate_limiter.py +0 -7
  141. schemathesis/cli/context.py +0 -75
  142. schemathesis/cli/debug.py +0 -27
  143. schemathesis/cli/handlers.py +0 -19
  144. schemathesis/cli/junitxml.py +0 -124
  145. schemathesis/cli/output/__init__.py +0 -1
  146. schemathesis/cli/output/default.py +0 -936
  147. schemathesis/cli/output/short.py +0 -59
  148. schemathesis/cli/reporting.py +0 -79
  149. schemathesis/cli/sanitization.py +0 -26
  150. schemathesis/code_samples.py +0 -151
  151. schemathesis/constants.py +0 -56
  152. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  153. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  154. schemathesis/contrib/unique_data.py +0 -41
  155. schemathesis/exceptions.py +0 -571
  156. schemathesis/extra/_aiohttp.py +0 -28
  157. schemathesis/extra/_flask.py +0 -13
  158. schemathesis/extra/_server.py +0 -18
  159. schemathesis/failures.py +0 -277
  160. schemathesis/fixups/__init__.py +0 -37
  161. schemathesis/fixups/fast_api.py +0 -41
  162. schemathesis/fixups/utf8_bom.py +0 -28
  163. schemathesis/generation/_methods.py +0 -44
  164. schemathesis/graphql.py +0 -3
  165. schemathesis/internal/__init__.py +0 -7
  166. schemathesis/internal/checks.py +0 -84
  167. schemathesis/internal/copy.py +0 -32
  168. schemathesis/internal/datetime.py +0 -5
  169. schemathesis/internal/deprecation.py +0 -38
  170. schemathesis/internal/diff.py +0 -15
  171. schemathesis/internal/extensions.py +0 -27
  172. schemathesis/internal/jsonschema.py +0 -36
  173. schemathesis/internal/transformation.py +0 -26
  174. schemathesis/internal/validation.py +0 -34
  175. schemathesis/lazy.py +0 -474
  176. schemathesis/loaders.py +0 -122
  177. schemathesis/models.py +0 -1341
  178. schemathesis/parameters.py +0 -90
  179. schemathesis/runner/__init__.py +0 -605
  180. schemathesis/runner/events.py +0 -389
  181. schemathesis/runner/impl/__init__.py +0 -3
  182. schemathesis/runner/impl/context.py +0 -104
  183. schemathesis/runner/impl/core.py +0 -1246
  184. schemathesis/runner/impl/solo.py +0 -80
  185. schemathesis/runner/impl/threadpool.py +0 -391
  186. schemathesis/runner/serialization.py +0 -544
  187. schemathesis/sanitization.py +0 -252
  188. schemathesis/serializers.py +0 -328
  189. schemathesis/service/__init__.py +0 -18
  190. schemathesis/service/auth.py +0 -11
  191. schemathesis/service/ci.py +0 -202
  192. schemathesis/service/client.py +0 -133
  193. schemathesis/service/constants.py +0 -38
  194. schemathesis/service/events.py +0 -61
  195. schemathesis/service/extensions.py +0 -224
  196. schemathesis/service/hosts.py +0 -111
  197. schemathesis/service/metadata.py +0 -71
  198. schemathesis/service/models.py +0 -258
  199. schemathesis/service/report.py +0 -255
  200. schemathesis/service/serialization.py +0 -173
  201. schemathesis/service/usage.py +0 -66
  202. schemathesis/specs/graphql/loaders.py +0 -364
  203. schemathesis/specs/openapi/expressions/context.py +0 -16
  204. schemathesis/specs/openapi/loaders.py +0 -708
  205. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  206. schemathesis/specs/openapi/stateful/types.py +0 -14
  207. schemathesis/specs/openapi/validation.py +0 -26
  208. schemathesis/stateful/__init__.py +0 -147
  209. schemathesis/stateful/config.py +0 -97
  210. schemathesis/stateful/context.py +0 -135
  211. schemathesis/stateful/events.py +0 -274
  212. schemathesis/stateful/runner.py +0 -309
  213. schemathesis/stateful/sink.py +0 -68
  214. schemathesis/stateful/statistic.py +0 -22
  215. schemathesis/stateful/validation.py +0 -100
  216. schemathesis/targets.py +0 -77
  217. schemathesis/transports/__init__.py +0 -359
  218. schemathesis/transports/asgi.py +0 -7
  219. schemathesis/transports/auth.py +0 -38
  220. schemathesis/transports/headers.py +0 -36
  221. schemathesis/transports/responses.py +0 -57
  222. schemathesis/types.py +0 -44
  223. schemathesis/utils.py +0 -164
  224. schemathesis-3.39.7.dist-info/RECORD +0 -160
  225. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  226. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  227. /schemathesis/{internal → core}/result.py +0 -0
  228. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
  229. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from schemathesis.core.failures import Failure, Severity
6
+
7
+ if TYPE_CHECKING:
8
+ from graphql.error import GraphQLFormattedError
9
+
10
+
11
+ class UnexpectedGraphQLResponse(Failure):
12
+ """GraphQL response is not a JSON object."""
13
+
14
+ __slots__ = ("operation", "type_name", "title", "message", "case_id", "severity")
15
+
16
+ def __init__(
17
+ self,
18
+ *,
19
+ operation: str,
20
+ type_name: str,
21
+ title: str = "Unexpected GraphQL Response",
22
+ message: str,
23
+ case_id: str | None = None,
24
+ ) -> None:
25
+ self.operation = operation
26
+ self.type_name = type_name
27
+ self.title = title
28
+ self.message = message
29
+ self.case_id = case_id
30
+ self.severity = Severity.MEDIUM
31
+
32
+ @property
33
+ def _unique_key(self) -> str:
34
+ return self.type_name
35
+
36
+
37
+ class GraphQLClientError(Failure):
38
+ """GraphQL query has not been executed."""
39
+
40
+ __slots__ = ("operation", "errors", "title", "message", "case_id", "_unique_key_cache", "severity")
41
+
42
+ def __init__(
43
+ self,
44
+ *,
45
+ operation: str,
46
+ message: str,
47
+ errors: list[GraphQLFormattedError],
48
+ title: str = "GraphQL client error",
49
+ case_id: str | None = None,
50
+ ) -> None:
51
+ self.operation = operation
52
+ self.errors = errors
53
+ self.title = title
54
+ self.message = message
55
+ self.case_id = case_id
56
+ self._unique_key_cache: str | None = None
57
+ self.severity = Severity.MEDIUM
58
+
59
+ @property
60
+ def _unique_key(self) -> str:
61
+ if self._unique_key_cache is None:
62
+ self._unique_key_cache = _group_graphql_errors(self.errors)
63
+ return self._unique_key_cache
64
+
65
+
66
+ class GraphQLServerError(Failure):
67
+ """GraphQL response indicates at least one server error."""
68
+
69
+ __slots__ = ("operation", "errors", "title", "message", "case_id", "_unique_key_cache", "severity")
70
+
71
+ def __init__(
72
+ self,
73
+ *,
74
+ operation: str,
75
+ message: str,
76
+ errors: list[GraphQLFormattedError],
77
+ title: str = "GraphQL server error",
78
+ case_id: str | None = None,
79
+ ) -> None:
80
+ self.operation = operation
81
+ self.errors = errors
82
+ self.title = title
83
+ self.message = message
84
+ self.case_id = case_id
85
+ self._unique_key_cache: str | None = None
86
+ self.severity = Severity.CRITICAL
87
+
88
+ @property
89
+ def _unique_key(self) -> str:
90
+ if self._unique_key_cache is None:
91
+ self._unique_key_cache = _group_graphql_errors(self.errors)
92
+ return self._unique_key_cache
93
+
94
+
95
+ def _group_graphql_errors(errors: list[GraphQLFormattedError]) -> str:
96
+ entries = []
97
+ for error in errors:
98
+ message = error["message"]
99
+ if "locations" in error:
100
+ message += ";locations:"
101
+ for location in sorted(error["locations"]):
102
+ message += f"({location['line'], location['column']})"
103
+ if "path" in error:
104
+ message += ";path:"
105
+ for chunk in error["path"]:
106
+ message += str(chunk)
107
+ entries.append(message)
108
+ entries.sort()
109
+ return "".join(entries)
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from functools import lru_cache
5
+ from os import PathLike
6
+ from pathlib import Path
7
+ from typing import IO, TYPE_CHECKING, Any, Callable, Dict, NoReturn, TypeVar, cast
8
+
9
+ from schemathesis.core.errors import LoaderError, LoaderErrorKind
10
+ from schemathesis.core.loaders import load_from_url, prepare_request_kwargs, raise_for_status, require_relative_url
11
+ from schemathesis.hooks import HookContext, dispatch
12
+ from schemathesis.python import asgi, wsgi
13
+
14
+ if TYPE_CHECKING:
15
+ from graphql import DocumentNode
16
+
17
+ from schemathesis.specs.graphql.schemas import GraphQLSchema
18
+
19
+
20
+ def from_asgi(path: str, app: Any, **kwargs: Any) -> GraphQLSchema:
21
+ require_relative_url(path)
22
+ kwargs.setdefault("json", {"query": get_introspection_query()})
23
+ client = asgi.get_client(app)
24
+ response = load_from_url(client.post, url=path, **kwargs)
25
+ schema = extract_schema_from_response(response, lambda r: r.json())
26
+ return from_dict(schema=schema).configure(app=app, location=path)
27
+
28
+
29
+ def from_wsgi(path: str, app: Any, **kwargs: Any) -> GraphQLSchema:
30
+ require_relative_url(path)
31
+ prepare_request_kwargs(kwargs)
32
+ kwargs.setdefault("json", {"query": get_introspection_query()})
33
+ client = wsgi.get_client(app)
34
+ response = client.post(path=path, **kwargs)
35
+ raise_for_status(response)
36
+ schema = extract_schema_from_response(response, lambda r: r.json)
37
+ return from_dict(schema=schema).configure(app=app, location=path)
38
+
39
+
40
+ def from_url(url: str, *, wait_for_schema: float | None = None, **kwargs: Any) -> GraphQLSchema:
41
+ """Load from URL."""
42
+ import requests
43
+
44
+ kwargs.setdefault("json", {"query": get_introspection_query()})
45
+ response = load_from_url(requests.post, url=url, wait_for_schema=wait_for_schema, **kwargs)
46
+ schema = extract_schema_from_response(response, lambda r: r.json())
47
+ return from_dict(schema).configure(location=url)
48
+
49
+
50
+ def from_path(path: PathLike | str, *, encoding: str = "utf-8") -> GraphQLSchema:
51
+ """Load from a filesystem path."""
52
+ with open(path, encoding=encoding) as file:
53
+ return from_file(file=file).configure(location=Path(path).absolute().as_uri())
54
+
55
+
56
+ def from_file(file: IO[str] | str) -> GraphQLSchema:
57
+ """Load from file-like object or string."""
58
+ import graphql
59
+
60
+ if isinstance(file, str):
61
+ data = file
62
+ else:
63
+ data = file.read()
64
+ try:
65
+ document = graphql.build_schema(data)
66
+ result = graphql.execute(document, get_introspection_query_ast())
67
+ # TYPES: We don't pass `is_awaitable` above, therefore `result` is of the `ExecutionResult` type
68
+ result = cast(graphql.ExecutionResult, result)
69
+ # TYPES:
70
+ # - `document` is a valid schema, because otherwise `build_schema` will rise an error;
71
+ # - `INTROSPECTION_QUERY` is a valid query - it is known upfront;
72
+ # Therefore the execution result is always valid at this point and `result.data` is not `None`
73
+ schema = cast(Dict[str, Any], result.data)
74
+ except Exception as exc:
75
+ try:
76
+ schema = json.loads(data)
77
+ if not isinstance(schema, dict) or "__schema" not in schema:
78
+ _on_invalid_schema(exc)
79
+ except json.JSONDecodeError:
80
+ _on_invalid_schema(exc, extras=[entry for entry in str(exc).splitlines() if entry])
81
+ return from_dict(schema)
82
+
83
+
84
+ def from_dict(schema: dict[str, Any]) -> GraphQLSchema:
85
+ """Base loader that others build upon."""
86
+ from schemathesis.specs.graphql.schemas import GraphQLSchema
87
+
88
+ if "data" in schema:
89
+ schema = schema["data"]
90
+ hook_context = HookContext()
91
+ dispatch("before_load_schema", hook_context, schema)
92
+ instance = GraphQLSchema(schema)
93
+ dispatch("after_load_schema", hook_context, instance)
94
+ return instance
95
+
96
+
97
+ @lru_cache
98
+ def get_introspection_query() -> str:
99
+ import graphql
100
+
101
+ return graphql.get_introspection_query()
102
+
103
+
104
+ @lru_cache
105
+ def get_introspection_query_ast() -> DocumentNode:
106
+ import graphql
107
+
108
+ query = get_introspection_query()
109
+ return graphql.parse(query)
110
+
111
+
112
+ R = TypeVar("R")
113
+
114
+
115
+ def extract_schema_from_response(response: R, callback: Callable[[R], Any]) -> dict[str, Any]:
116
+ try:
117
+ decoded = callback(response)
118
+ except json.JSONDecodeError as exc:
119
+ raise LoaderError(
120
+ LoaderErrorKind.UNEXPECTED_CONTENT_TYPE,
121
+ "Received unsupported content while expecting a JSON payload for GraphQL",
122
+ ) from exc
123
+ return decoded
124
+
125
+
126
+ def _on_invalid_schema(exc: Exception, extras: list[str] | None = None) -> NoReturn:
127
+ raise LoaderError(
128
+ LoaderErrorKind.GRAPHQL_INVALID_SCHEMA,
129
+ "The provided API schema does not appear to be a valid GraphQL schema",
130
+ extras=extras or [],
131
+ ) from exc
schemathesis/hooks.py CHANGED
@@ -2,22 +2,22 @@ from __future__ import annotations
2
2
 
3
3
  import inspect
4
4
  from collections import defaultdict
5
- from copy import deepcopy
6
5
  from dataclasses import dataclass, field
7
6
  from enum import Enum, unique
8
7
  from functools import partial
9
8
  from typing import TYPE_CHECKING, Any, Callable, ClassVar, cast
10
9
 
11
- from .filters import FilterSet, attach_filter_chain
12
- from .internal.deprecation import deprecated_property
10
+ from schemathesis.core.marks import Mark
11
+ from schemathesis.core.transport import Response
12
+ from schemathesis.filters import FilterSet, attach_filter_chain
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  from hypothesis import strategies as st
16
16
 
17
- from .models import APIOperation, Case
18
- from .schemas import BaseSchema
19
- from .transports.responses import GenericResponse
20
- from .types import GenericTest
17
+ from schemathesis.generation.case import Case
18
+ from schemathesis.schemas import APIOperation, BaseSchema
19
+
20
+ HookDispatcherMark = Mark["HookDispatcher"](attr_name="hook_dispatcher")
21
21
 
22
22
 
23
23
  @unique
@@ -45,10 +45,6 @@ class HookContext:
45
45
 
46
46
  operation: APIOperation | None = None
47
47
 
48
- @deprecated_property(removed_in="4.0", replacement="`operation`")
49
- def endpoint(self) -> APIOperation | None:
50
- return self.operation
51
-
52
48
 
53
49
  def to_filterable_hook(dispatcher: HookDispatcher) -> Callable:
54
50
  filter_used = False
@@ -140,18 +136,6 @@ class HookDispatcher:
140
136
  """
141
137
  raise NotImplementedError
142
138
 
143
- def merge(self, other: HookDispatcher) -> HookDispatcher:
144
- """Merge two dispatches together.
145
-
146
- The resulting dispatcher will call the `self` hooks first.
147
- """
148
- all_hooks = deepcopy(self._hooks)
149
- for name, hooks in other._hooks.items():
150
- all_hooks[name].extend(hooks)
151
- instance = self.__class__(scope=self.scope)
152
- instance._hooks = all_hooks
153
- return instance
154
-
155
139
  def apply(self, hook: Callable, *, name: str | None = None) -> Callable[[Callable], Callable]:
156
140
  """Register hook to run only on one test function.
157
141
 
@@ -175,7 +159,7 @@ class HookDispatcher:
175
159
  else:
176
160
  hook_name = name
177
161
 
178
- def decorator(func: GenericTest) -> GenericTest:
162
+ def decorator(func: Callable) -> Callable:
179
163
  dispatcher = self.add_dispatcher(func)
180
164
  dispatcher.register_hook_with_name(hook, hook_name)
181
165
  return func
@@ -183,11 +167,13 @@ class HookDispatcher:
183
167
  return decorator
184
168
 
185
169
  @classmethod
186
- def add_dispatcher(cls, func: GenericTest) -> HookDispatcher:
170
+ def add_dispatcher(cls, func: Callable) -> HookDispatcher:
187
171
  """Attach a new dispatcher instance to the test if it is not already present."""
188
- if not hasattr(func, "_schemathesis_hooks"):
189
- func._schemathesis_hooks = cls(scope=HookScope.TEST) # type: ignore
190
- return func._schemathesis_hooks # type: ignore
172
+ if not HookDispatcherMark.is_set(func):
173
+ HookDispatcherMark.set(func, cls(scope=HookScope.TEST))
174
+ dispatcher = HookDispatcherMark.get(func)
175
+ assert dispatcher is not None
176
+ return dispatcher
191
177
 
192
178
  def register_hook_with_name(self, hook: Callable, name: str) -> Callable:
193
179
  """A helper for hooks registration."""
@@ -226,19 +212,10 @@ class HookDispatcher:
226
212
  f"Hook '{name}' takes {len(spec.signature.parameters)} arguments but {len(signature.parameters)} is defined"
227
213
  )
228
214
 
229
- def collect_statistic(self) -> dict[str, int]:
230
- return {name: len(hooks) for name, hooks in self._hooks.items()}
231
-
232
215
  def get_all_by_name(self, name: str) -> list[Callable]:
233
216
  """Get a list of hooks registered for a name."""
234
217
  return self._hooks.get(name, [])
235
218
 
236
- def is_installed(self, name: str, needle: Callable) -> bool:
237
- for hook in self.get_all_by_name(name):
238
- if hook is needle:
239
- return True
240
- return False
241
-
242
219
  def apply_to_container(
243
220
  self, strategy: st.SearchStrategy, container: str, context: HookContext
244
221
  ) -> st.SearchStrategy:
@@ -307,19 +284,12 @@ def apply_to_all_dispatchers(
307
284
  return strategy
308
285
 
309
286
 
310
- def should_skip_operation(dispatcher: HookDispatcher, context: HookContext) -> bool:
311
- for hook in dispatcher.get_all_by_name("filter_operations"):
312
- if not hook(context):
313
- return True
314
- return False
315
-
316
-
317
287
  def validate_filterable_hook(hook: str | Callable) -> None:
318
288
  if callable(hook):
319
289
  name = hook.__name__
320
290
  else:
321
291
  name = hook
322
- if name in ("before_process_path", "before_load_schema", "after_load_schema", "after_init_cli_run_handlers"):
292
+ if name in ("before_process_path", "before_load_schema", "after_load_schema"):
323
293
  raise ValueError(f"Filters are not applicable to this hook: `{name}`")
324
294
 
325
295
 
@@ -373,11 +343,6 @@ def before_process_path(context: HookContext, path: str, methods: dict[str, Any]
373
343
  """Called before API path is processed."""
374
344
 
375
345
 
376
- @all_scopes
377
- def filter_operations(context: HookContext) -> bool | None:
378
- """Decide whether testing of this particular API operation should be skipped or not."""
379
-
380
-
381
346
  @HookDispatcher.register_spec([HookScope.GLOBAL])
382
347
  def before_load_schema(context: HookContext, raw_schema: dict[str, Any]) -> None:
383
348
  """Called before schema instance is created."""
@@ -402,15 +367,7 @@ def before_init_operation(context: HookContext, operation: APIOperation) -> None
402
367
 
403
368
 
404
369
  @HookDispatcher.register_spec([HookScope.GLOBAL])
405
- def add_case(context: HookContext, case: Case, response: GenericResponse) -> Case | None:
406
- """Creates an additional test per API operation. If this hook returns None, no additional test created.
407
-
408
- Called with a copy of the original case object and the server's response to the original case.
409
- """
410
-
411
-
412
- @HookDispatcher.register_spec([HookScope.GLOBAL])
413
- def before_call(context: HookContext, case: Case) -> None:
370
+ def before_call(context: HookContext, case: Case, **kwargs: Any) -> None:
414
371
  """Called before every network call in CLI tests.
415
372
 
416
373
  Use cases:
@@ -420,7 +377,7 @@ def before_call(context: HookContext, case: Case) -> None:
420
377
 
421
378
 
422
379
  @HookDispatcher.register_spec([HookScope.GLOBAL])
423
- def after_call(context: HookContext, case: Case, response: GenericResponse) -> None:
380
+ def after_call(context: HookContext, case: Case, response: Response) -> None:
424
381
  """Called after every network call in CLI tests.
425
382
 
426
383
  Note that you need to modify the response in-place.
@@ -434,8 +391,6 @@ def after_call(context: HookContext, case: Case, response: GenericResponse) -> N
434
391
  GLOBAL_HOOK_DISPATCHER = HookDispatcher(scope=HookScope.GLOBAL)
435
392
  dispatch = GLOBAL_HOOK_DISPATCHER.dispatch
436
393
  get_all_by_name = GLOBAL_HOOK_DISPATCHER.get_all_by_name
437
- is_installed = GLOBAL_HOOK_DISPATCHER.is_installed
438
- collect_statistic = GLOBAL_HOOK_DISPATCHER.collect_statistic
439
394
  register = GLOBAL_HOOK_DISPATCHER.register
440
395
  unregister = GLOBAL_HOOK_DISPATCHER.unregister
441
396
  unregister_all = GLOBAL_HOOK_DISPATCHER.unregister_all
@@ -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
+ ]