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
@@ -0,0 +1,315 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+ import json
5
+ import re
6
+ from os import PathLike
7
+ from pathlib import Path
8
+ from typing import IO, TYPE_CHECKING, Any, Mapping
9
+
10
+ from schemathesis.config import SchemathesisConfig
11
+ from schemathesis.core import media_types
12
+ from schemathesis.core.deserialization import deserialize_yaml
13
+ from schemathesis.core.errors import LoaderError, LoaderErrorKind
14
+ from schemathesis.core.loaders import load_from_url, prepare_request_kwargs, raise_for_status, require_relative_url
15
+ from schemathesis.hooks import HookContext, dispatch
16
+ from schemathesis.python import asgi, wsgi
17
+
18
+ if TYPE_CHECKING:
19
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
20
+
21
+
22
+ def from_asgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> BaseOpenAPISchema:
23
+ """Load OpenAPI schema from an ASGI application.
24
+
25
+ Args:
26
+ path: Relative URL path to the OpenAPI schema endpoint (e.g., "/openapi.json")
27
+ app: ASGI application instance
28
+ config: Custom configuration. If `None`, uses auto-discovered config
29
+ **kwargs: Additional request parameters passed to the ASGI test client
30
+
31
+ Example:
32
+ ```python
33
+ from fastapi import FastAPI
34
+ import schemathesis
35
+
36
+ app = FastAPI()
37
+ schema = schemathesis.openapi.from_asgi("/openapi.json", app)
38
+ ```
39
+
40
+ """
41
+ require_relative_url(path)
42
+ client = asgi.get_client(app)
43
+ response = load_from_url(client.get, url=path, **kwargs)
44
+ content_type = detect_content_type(headers=response.headers, path=path)
45
+ schema = load_content(response.text, content_type)
46
+ loaded = from_dict(schema=schema, config=config)
47
+ loaded.app = app
48
+ loaded.location = path
49
+ return loaded
50
+
51
+
52
+ def from_wsgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> BaseOpenAPISchema:
53
+ """Load OpenAPI schema from a WSGI application.
54
+
55
+ Args:
56
+ path: Relative URL path to the OpenAPI schema endpoint (e.g., "/openapi.json")
57
+ app: WSGI application instance
58
+ config: Custom configuration. If `None`, uses auto-discovered config
59
+ **kwargs: Additional request parameters passed to the WSGI test client
60
+
61
+ Example:
62
+ ```python
63
+ from flask import Flask
64
+ import schemathesis
65
+
66
+ app = Flask(__name__)
67
+ schema = schemathesis.openapi.from_wsgi("/openapi.json", app)
68
+ ```
69
+
70
+ """
71
+ require_relative_url(path)
72
+ prepare_request_kwargs(kwargs)
73
+ client = wsgi.get_client(app)
74
+ response = client.get(path=path, **kwargs)
75
+ raise_for_status(response)
76
+ content_type = detect_content_type(headers=response.headers, path=path)
77
+ schema = load_content(response.text, content_type)
78
+ loaded = from_dict(schema=schema, config=config)
79
+ loaded.app = app
80
+ loaded.location = path
81
+ return loaded
82
+
83
+
84
+ def from_url(
85
+ url: str, *, config: SchemathesisConfig | None = None, wait_for_schema: float | None = None, **kwargs: Any
86
+ ) -> BaseOpenAPISchema:
87
+ """Load OpenAPI schema from a URL.
88
+
89
+ Args:
90
+ url: Full URL to the OpenAPI schema
91
+ config: Custom configuration. If `None`, uses auto-discovered config
92
+ wait_for_schema: Maximum time in seconds to wait for schema availability
93
+ **kwargs: Additional parameters passed to `requests.get()` (headers, timeout, auth, etc.)
94
+
95
+ Example:
96
+ ```python
97
+ import schemathesis
98
+
99
+ # Basic usage
100
+ schema = schemathesis.openapi.from_url("https://api.example.com/openapi.json")
101
+
102
+ # With authentication and timeout
103
+ schema = schemathesis.openapi.from_url(
104
+ "https://api.example.com/openapi.json",
105
+ headers={"Authorization": "Bearer token"},
106
+ timeout=30,
107
+ wait_for_schema=10.0
108
+ )
109
+ ```
110
+
111
+ """
112
+ import requests
113
+
114
+ response = load_from_url(requests.get, url=url, wait_for_schema=wait_for_schema, **kwargs)
115
+ content_type = detect_content_type(headers=response.headers, path=url)
116
+ schema = load_content(response.text, content_type)
117
+ loaded = from_dict(schema=schema, config=config)
118
+ loaded.location = url
119
+ return loaded
120
+
121
+
122
+ def from_path(
123
+ path: PathLike | str, *, config: SchemathesisConfig | None = None, encoding: str = "utf-8"
124
+ ) -> BaseOpenAPISchema:
125
+ """Load OpenAPI schema from a filesystem path.
126
+
127
+ Args:
128
+ path: File path to the OpenAPI schema (supports JSON / YAML)
129
+ config: Custom configuration. If `None`, uses auto-discovered config
130
+ encoding: Text encoding for reading the file
131
+
132
+ Example:
133
+ ```python
134
+ import schemathesis
135
+
136
+ # Load from file
137
+ schema = schemathesis.openapi.from_path("./specs/openapi.yaml")
138
+
139
+ # With custom encoding
140
+ schema = schemathesis.openapi.from_path("./specs/openapi.json", encoding="cp1252")
141
+ ```
142
+
143
+ """
144
+ with open(path, encoding=encoding) as file:
145
+ content_type = detect_content_type(headers=None, path=str(path))
146
+ schema = load_content(file.read(), content_type)
147
+ loaded = from_dict(schema=schema, config=config)
148
+ loaded.location = Path(path).absolute().as_uri()
149
+ return loaded
150
+
151
+
152
+ def from_file(file: IO[str] | str, *, config: SchemathesisConfig | None = None) -> BaseOpenAPISchema:
153
+ """Load OpenAPI schema from a file-like object or string.
154
+
155
+ Args:
156
+ file: File-like object or raw string containing the OpenAPI schema
157
+ config: Custom configuration. If `None`, uses auto-discovered config
158
+
159
+ Example:
160
+ ```python
161
+ import schemathesis
162
+
163
+ # From string
164
+ schema_content = '{"openapi": "3.0.0", "info": {"title": "API"}}'
165
+ schema = schemathesis.openapi.from_file(schema_content)
166
+
167
+ # From file object
168
+ with open("openapi.yaml") as f:
169
+ schema = schemathesis.openapi.from_file(f)
170
+ ```
171
+
172
+ """
173
+ if isinstance(file, str):
174
+ data = file
175
+ else:
176
+ data = file.read()
177
+ try:
178
+ schema = json.loads(data)
179
+ except json.JSONDecodeError:
180
+ schema = _load_yaml(data)
181
+ return from_dict(schema, config=config)
182
+
183
+
184
+ def from_dict(schema: dict[str, Any], *, config: SchemathesisConfig | None = None) -> BaseOpenAPISchema:
185
+ """Load OpenAPI schema from a dictionary.
186
+
187
+ Args:
188
+ schema: Dictionary containing the parsed OpenAPI schema
189
+ config: Custom configuration. If `None`, uses auto-discovered config
190
+
191
+ Example:
192
+ ```python
193
+ import schemathesis
194
+
195
+ schema_dict = {
196
+ "openapi": "3.0.0",
197
+ "info": {"title": "My API", "version": "1.0.0"},
198
+ "paths": {"/users": {"get": {"responses": {"200": {"description": "OK"}}}}}
199
+ }
200
+
201
+ schema = schemathesis.openapi.from_dict(schema_dict)
202
+ ```
203
+
204
+ """
205
+ from schemathesis.specs.openapi.schemas import OpenApi30, SwaggerV20
206
+
207
+ if not isinstance(schema, dict):
208
+ raise LoaderError(LoaderErrorKind.OPEN_API_INVALID_SCHEMA, SCHEMA_INVALID_ERROR)
209
+ hook_context = HookContext()
210
+ dispatch("before_load_schema", hook_context, schema)
211
+
212
+ if config is None:
213
+ config = SchemathesisConfig.discover()
214
+ project_config = config.projects.get(schema)
215
+
216
+ if "swagger" in schema:
217
+ instance = SwaggerV20(raw_schema=schema, config=project_config)
218
+ elif "openapi" in schema:
219
+ version = schema["openapi"]
220
+ if not OPENAPI_VERSION_RE.match(version):
221
+ raise LoaderError(
222
+ LoaderErrorKind.OPEN_API_UNSUPPORTED_VERSION,
223
+ f"The provided schema uses Open API {version}, which is currently not supported.",
224
+ )
225
+ instance = OpenApi30(raw_schema=schema, config=project_config)
226
+ else:
227
+ raise LoaderError(
228
+ LoaderErrorKind.OPEN_API_UNSPECIFIED_VERSION,
229
+ "Unable to determine the Open API version as it's not specified in the document.",
230
+ )
231
+ instance.filter_set = project_config.operations.filter_set_with(include=instance.filter_set)
232
+ dispatch("after_load_schema", hook_context, instance)
233
+ return instance
234
+
235
+
236
+ class ContentType(enum.Enum):
237
+ """Known content types for schema files."""
238
+
239
+ JSON = enum.auto()
240
+ YAML = enum.auto()
241
+ UNKNOWN = enum.auto()
242
+
243
+
244
+ def detect_content_type(*, headers: Mapping[str, str] | None = None, path: str | None = None) -> ContentType:
245
+ """Detect content type from various sources."""
246
+ if headers is not None and (content_type := _detect_from_headers(headers)) != ContentType.UNKNOWN:
247
+ return content_type
248
+ if path is not None and (content_type := _detect_from_path(path)) != ContentType.UNKNOWN:
249
+ return content_type
250
+ return ContentType.UNKNOWN
251
+
252
+
253
+ def _detect_from_headers(headers: Mapping[str, str]) -> ContentType:
254
+ """Detect content type from HTTP headers."""
255
+ content_type = headers.get("Content-Type", "").lower()
256
+ try:
257
+ if content_type and media_types.is_json(content_type):
258
+ return ContentType.JSON
259
+ if content_type and media_types.is_yaml(content_type):
260
+ return ContentType.YAML
261
+ except ValueError:
262
+ pass
263
+ return ContentType.UNKNOWN
264
+
265
+
266
+ def _detect_from_path(path: str) -> ContentType:
267
+ """Detect content type from file path."""
268
+ suffix = Path(path).suffix.lower()
269
+ if suffix == ".json":
270
+ return ContentType.JSON
271
+ if suffix in (".yaml", ".yml"):
272
+ return ContentType.YAML
273
+ return ContentType.UNKNOWN
274
+
275
+
276
+ def load_content(content: str, content_type: ContentType) -> dict[str, Any]:
277
+ """Load content using appropriate parser."""
278
+ if content_type == ContentType.JSON:
279
+ return _load_json(content)
280
+ if content_type == ContentType.YAML:
281
+ return _load_yaml(content)
282
+ # If type is unknown, try JSON first, then YAML
283
+ try:
284
+ return _load_json(content)
285
+ except LoaderError:
286
+ return _load_yaml(content)
287
+
288
+
289
+ def _load_json(content: str) -> dict[str, Any]:
290
+ try:
291
+ return json.loads(content)
292
+ except json.JSONDecodeError as exc:
293
+ raise LoaderError(
294
+ LoaderErrorKind.SYNTAX_ERROR,
295
+ SCHEMA_SYNTAX_ERROR,
296
+ extras=[entry for entry in str(exc).splitlines() if entry],
297
+ ) from exc
298
+
299
+
300
+ def _load_yaml(content: str) -> dict[str, Any]:
301
+ import yaml
302
+
303
+ try:
304
+ return deserialize_yaml(content)
305
+ except yaml.YAMLError as exc:
306
+ raise LoaderError(
307
+ LoaderErrorKind.SYNTAX_ERROR,
308
+ SCHEMA_SYNTAX_ERROR,
309
+ extras=[entry for entry in str(exc).splitlines() if entry],
310
+ ) from exc
311
+
312
+
313
+ SCHEMA_INVALID_ERROR = "The provided API schema does not appear to be a valid OpenAPI schema"
314
+ SCHEMA_SYNTAX_ERROR = "API schema does not appear syntactically valid"
315
+ OPENAPI_VERSION_RE = re.compile(r"^3\.[01]\.[0-9](-.+)?$")
@@ -0,0 +1,5 @@
1
+ from schemathesis.pytest.loaders import from_fixture
2
+
3
+ __all__ = [
4
+ "from_fixture",
5
+ ]
@@ -0,0 +1,7 @@
1
+ from typing import NoReturn
2
+
3
+ import pytest
4
+
5
+
6
+ def fail_on_no_matches(node_id: str) -> NoReturn: # type: ignore[misc]
7
+ pytest.fail(f"Test function {node_id} does not match any API operations and therefore has no effect")
@@ -0,0 +1,341 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import nullcontext
4
+ from dataclasses import dataclass
5
+ from inspect import signature
6
+ from typing import TYPE_CHECKING, Any, Callable, Generator, Type
7
+
8
+ import pytest
9
+ from hypothesis.core import HypothesisHandle
10
+ from pytest_subtests import SubTests
11
+
12
+ from schemathesis.core.errors import InvalidSchema
13
+ from schemathesis.core.result import Ok, Result
14
+ from schemathesis.filters import FilterSet, FilterValue, MatcherFunc, RegexValue, is_deprecated
15
+ from schemathesis.generation import overrides
16
+ from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, HypothesisTestMode, create_test
17
+ from schemathesis.generation.hypothesis.given import (
18
+ GivenArgsMark,
19
+ GivenInput,
20
+ GivenKwargsMark,
21
+ given_proxy,
22
+ is_given_applied,
23
+ merge_given_args,
24
+ validate_given_args,
25
+ )
26
+ from schemathesis.pytest.control_flow import fail_on_no_matches
27
+ from schemathesis.schemas import BaseSchema
28
+
29
+ if TYPE_CHECKING:
30
+ import hypothesis
31
+ from _pytest.fixtures import FixtureRequest
32
+
33
+ from schemathesis.schemas import APIOperation
34
+
35
+
36
+ def get_all_tests(
37
+ *,
38
+ schema: BaseSchema,
39
+ test_func: Callable,
40
+ settings: hypothesis.settings | None = None,
41
+ seed: int | None = None,
42
+ as_strategy_kwargs: Callable[[APIOperation], dict[str, Any]] | None = None,
43
+ given_kwargs: dict[str, GivenInput] | None = None,
44
+ ) -> Generator[Result[tuple[APIOperation, Callable], InvalidSchema], None, None]:
45
+ """Generate all operations and Hypothesis tests for them."""
46
+ for result in schema.get_all_operations():
47
+ if isinstance(result, Ok):
48
+ operation = result.ok()
49
+ if callable(as_strategy_kwargs):
50
+ _as_strategy_kwargs = as_strategy_kwargs(operation)
51
+ else:
52
+ _as_strategy_kwargs = {}
53
+
54
+ # Get modes from config for this operation
55
+ modes = []
56
+ phases = schema.config.phases_for(operation=operation)
57
+ if phases.examples.enabled:
58
+ modes.append(HypothesisTestMode.EXAMPLES)
59
+ if phases.fuzzing.enabled:
60
+ modes.append(HypothesisTestMode.FUZZING)
61
+ if phases.coverage.enabled:
62
+ modes.append(HypothesisTestMode.COVERAGE)
63
+
64
+ test = create_test(
65
+ operation=operation,
66
+ test_func=test_func,
67
+ config=HypothesisTestConfig(
68
+ settings=settings or schema.config.get_hypothesis_settings(operation=operation),
69
+ modes=modes,
70
+ seed=seed,
71
+ project=schema.config,
72
+ as_strategy_kwargs=_as_strategy_kwargs,
73
+ given_kwargs=given_kwargs or {},
74
+ ),
75
+ )
76
+ yield Ok((operation, test))
77
+ else:
78
+ yield result
79
+
80
+
81
+ @dataclass
82
+ class LazySchema:
83
+ fixture_name: str
84
+ filter_set: FilterSet
85
+
86
+ __slots__ = ("fixture_name", "filter_set")
87
+
88
+ def __init__(
89
+ self,
90
+ fixture_name: str,
91
+ filter_set: FilterSet | None = None,
92
+ ) -> None:
93
+ self.fixture_name = fixture_name
94
+ self.filter_set = filter_set or FilterSet()
95
+
96
+ def include(
97
+ self,
98
+ func: MatcherFunc | None = None,
99
+ *,
100
+ name: FilterValue | None = None,
101
+ name_regex: str | None = None,
102
+ method: FilterValue | None = None,
103
+ method_regex: str | None = None,
104
+ path: FilterValue | None = None,
105
+ path_regex: str | None = None,
106
+ tag: FilterValue | None = None,
107
+ tag_regex: RegexValue | None = None,
108
+ operation_id: FilterValue | None = None,
109
+ operation_id_regex: RegexValue | None = None,
110
+ ) -> LazySchema:
111
+ """Include only operations that match the given filters."""
112
+ filter_set = self.filter_set.clone()
113
+ filter_set.include(
114
+ func,
115
+ name=name,
116
+ name_regex=name_regex,
117
+ method=method,
118
+ method_regex=method_regex,
119
+ path=path,
120
+ path_regex=path_regex,
121
+ tag=tag,
122
+ tag_regex=tag_regex,
123
+ operation_id=operation_id,
124
+ operation_id_regex=operation_id_regex,
125
+ )
126
+ return self.__class__(fixture_name=self.fixture_name, filter_set=filter_set)
127
+
128
+ def exclude(
129
+ self,
130
+ func: MatcherFunc | None = None,
131
+ *,
132
+ name: FilterValue | None = None,
133
+ name_regex: str | None = None,
134
+ method: FilterValue | None = None,
135
+ method_regex: str | None = None,
136
+ path: FilterValue | None = None,
137
+ path_regex: str | None = None,
138
+ tag: FilterValue | None = None,
139
+ tag_regex: RegexValue | None = None,
140
+ operation_id: FilterValue | None = None,
141
+ operation_id_regex: RegexValue | None = None,
142
+ deprecated: bool = False,
143
+ ) -> LazySchema:
144
+ """Exclude operations that match the given filters."""
145
+ filter_set = self.filter_set.clone()
146
+ if deprecated:
147
+ if func is None:
148
+ func = is_deprecated
149
+ else:
150
+ filter_set.exclude(is_deprecated)
151
+ filter_set.exclude(
152
+ func,
153
+ name=name,
154
+ name_regex=name_regex,
155
+ method=method,
156
+ method_regex=method_regex,
157
+ path=path,
158
+ path_regex=path_regex,
159
+ tag=tag,
160
+ tag_regex=tag_regex,
161
+ operation_id=operation_id,
162
+ operation_id_regex=operation_id_regex,
163
+ )
164
+ return self.__class__(fixture_name=self.fixture_name, filter_set=filter_set)
165
+
166
+ def parametrize(self) -> Callable:
167
+ def wrapper(test_func: Callable) -> Callable:
168
+ if is_given_applied(test_func):
169
+ # The user wrapped the test function with `@schema.given`
170
+ # These args & kwargs go as extra to the underlying test generator
171
+ given_args = GivenArgsMark.get(test_func)
172
+ given_kwargs = GivenKwargsMark.get(test_func)
173
+ assert given_args is not None
174
+ assert given_kwargs is not None
175
+ test_function = validate_given_args(test_func, given_args, given_kwargs)
176
+ if test_function is not None:
177
+ return test_function
178
+ given_kwargs = merge_given_args(test_func, given_args, given_kwargs)
179
+ del given_args
180
+ else:
181
+ given_kwargs = {}
182
+
183
+ def wrapped_test(*args: Any, request: FixtureRequest, **kwargs: Any) -> None:
184
+ """The actual test, which is executed by pytest."""
185
+ __tracebackhide__ = True
186
+
187
+ # Load all checks eagerly, so they are accessible inside the test function
188
+ from schemathesis.checks import load_all_checks
189
+
190
+ load_all_checks()
191
+
192
+ schema = get_schema(
193
+ request=request,
194
+ name=self.fixture_name,
195
+ test_function=test_func,
196
+ filter_set=self.filter_set,
197
+ )
198
+ # Check if test function is a method and inject self from request.instance
199
+ sig = signature(test_func)
200
+ if "self" in sig.parameters and request.instance is not None:
201
+ fixtures = {"self": request.instance}
202
+ fixtures.update(get_fixtures(test_func, request, given_kwargs))
203
+ else:
204
+ fixtures = get_fixtures(test_func, request, given_kwargs)
205
+ # Changing the node id is required for better reporting - the method and path will appear there
206
+ node_id = request.node._nodeid
207
+ settings = getattr(wrapped_test, "_hypothesis_internal_use_settings", None)
208
+
209
+ def as_strategy_kwargs(_operation: APIOperation) -> dict[str, Any]:
210
+ as_strategy_kwargs: dict[str, Any] = {}
211
+
212
+ auth = schema.config.auth_for(operation=_operation)
213
+ if auth is not None:
214
+ from requests.auth import _basic_auth_str
215
+
216
+ as_strategy_kwargs["headers"] = {"Authorization": _basic_auth_str(*auth)}
217
+
218
+ headers = schema.config.headers_for(operation=_operation)
219
+ if headers:
220
+ as_strategy_kwargs["headers"] = headers
221
+
222
+ override = overrides.for_operation(config=schema.config, operation=_operation)
223
+ for location, entry in override.items():
224
+ if entry:
225
+ as_strategy_kwargs[location.container_name] = entry
226
+
227
+ return as_strategy_kwargs
228
+
229
+ tests = list(
230
+ get_all_tests(
231
+ schema=schema,
232
+ test_func=test_func,
233
+ settings=settings,
234
+ as_strategy_kwargs=as_strategy_kwargs,
235
+ given_kwargs=given_kwargs,
236
+ )
237
+ )
238
+ if not tests:
239
+ fail_on_no_matches(node_id)
240
+ request.session.testscollected += len(tests)
241
+ suspend_capture_ctx = _get_capturemanager(request)
242
+ subtests = SubTests(request.node.ihook, suspend_capture_ctx, request)
243
+ for result in tests:
244
+ if isinstance(result, Ok):
245
+ operation, sub_test = result.ok()
246
+ subtests.item._nodeid = f"{node_id}[{operation.method.upper()} {operation.path}]"
247
+ run_subtest(operation, fixtures, sub_test, subtests)
248
+ else:
249
+ _schema_error(subtests, result.err(), node_id)
250
+ subtests.item._nodeid = node_id
251
+
252
+ sig = signature(test_func)
253
+ if "self" in sig.parameters:
254
+ # For methods, wrap with staticmethod to prevent pytest from passing self
255
+ wrapped_test = staticmethod(wrapped_test) # type: ignore[assignment]
256
+ wrapped_func = wrapped_test.__func__ # type: ignore[attr-defined]
257
+ else:
258
+ wrapped_func = wrapped_test
259
+
260
+ wrapped_func = pytest.mark.usefixtures(self.fixture_name)(wrapped_func)
261
+ _copy_marks(test_func, wrapped_func)
262
+
263
+ # Needed to prevent a failure when settings are applied to the test function
264
+ wrapped_func.is_hypothesis_test = True
265
+ wrapped_func.hypothesis = HypothesisHandle(test_func, wrapped_func, given_kwargs)
266
+
267
+ return wrapped_test if "self" in sig.parameters else wrapped_func
268
+
269
+ return wrapper
270
+
271
+ def given(self, *args: GivenInput, **kwargs: GivenInput) -> Callable:
272
+ return given_proxy(*args, **kwargs)
273
+
274
+
275
+ def _copy_marks(source: Callable, target: Callable) -> None:
276
+ marks = getattr(source, "pytestmark", [])
277
+ # Pytest adds this attribute in `usefixtures`
278
+ target.pytestmark.extend(marks) # type: ignore[attr-defined]
279
+
280
+
281
+ def _get_capturemanager(request: FixtureRequest) -> Generator | Type[nullcontext]:
282
+ capturemanager = request.node.config.pluginmanager.get_plugin("capturemanager")
283
+ if capturemanager is not None:
284
+ return capturemanager.global_and_fixture_disabled
285
+ return nullcontext
286
+
287
+
288
+ def run_subtest(operation: APIOperation, fixtures: dict[str, Any], sub_test: Callable, subtests: SubTests) -> None:
289
+ """Run the given subtest with pytest fixtures."""
290
+ __tracebackhide__ = True
291
+
292
+ with subtests.test(label=operation.label):
293
+ sub_test(**fixtures)
294
+
295
+
296
+ SEPARATOR = "\n===================="
297
+
298
+
299
+ def _schema_error(subtests: SubTests, error: InvalidSchema, node_id: str) -> None:
300
+ """Run a failing test, that will show the underlying problem."""
301
+ sub_test = error.as_failing_test_function()
302
+ kwargs = {"path": error.path}
303
+ if error.method:
304
+ kwargs["method"] = error.method.upper()
305
+ subtests.item._nodeid = _get_partial_node_name(node_id, **kwargs)
306
+ __tracebackhide__ = True
307
+ with subtests.test(**kwargs):
308
+ sub_test()
309
+
310
+
311
+ def _get_partial_node_name(node_id: str, **kwargs: Any) -> str:
312
+ """Make a test node name for failing tests caused by schema errors."""
313
+ name = node_id
314
+ if "method" in kwargs:
315
+ name += f"[{kwargs['method']} {kwargs['path']}]"
316
+ else:
317
+ name += f"[{kwargs['path']}]"
318
+ return name
319
+
320
+
321
+ def get_schema(*, request: FixtureRequest, name: str, filter_set: FilterSet, test_function: Callable) -> BaseSchema:
322
+ """Loads a schema from the fixture."""
323
+ schema = request.getfixturevalue(name)
324
+ if not isinstance(schema, BaseSchema):
325
+ raise ValueError(f"The given schema must be an instance of BaseSchema, got: {type(schema)}")
326
+
327
+ # Merge config-based operation filters with user-provided filters
328
+ # This ensures operations disabled in schemathesis.toml are respected
329
+ merged_filter_set = schema.config.operations.filter_set_with(include=filter_set)
330
+
331
+ return schema.clone(filter_set=merged_filter_set, test_function=test_function)
332
+
333
+
334
+ def get_fixtures(func: Callable, request: FixtureRequest, given_kwargs: dict[str, Any]) -> dict[str, Any]:
335
+ """Load fixtures, needed for the test function."""
336
+ sig = signature(func)
337
+ return {
338
+ name: request.getfixturevalue(name)
339
+ for name in sig.parameters
340
+ if name not in ("case", "self") and name not in given_kwargs
341
+ }