schemathesis 3.19.7__py3-none-any.whl → 3.20.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. schemathesis/_compat.py +3 -2
  2. schemathesis/_hypothesis.py +21 -6
  3. schemathesis/_xml.py +177 -0
  4. schemathesis/auths.py +48 -10
  5. schemathesis/cli/__init__.py +77 -19
  6. schemathesis/cli/callbacks.py +42 -18
  7. schemathesis/cli/context.py +2 -1
  8. schemathesis/cli/output/default.py +102 -34
  9. schemathesis/cli/sanitization.py +15 -0
  10. schemathesis/code_samples.py +141 -0
  11. schemathesis/constants.py +1 -24
  12. schemathesis/exceptions.py +127 -26
  13. schemathesis/experimental/__init__.py +85 -0
  14. schemathesis/extra/pytest_plugin.py +10 -4
  15. schemathesis/fixups/__init__.py +8 -2
  16. schemathesis/fixups/fast_api.py +11 -1
  17. schemathesis/fixups/utf8_bom.py +7 -1
  18. schemathesis/hooks.py +63 -0
  19. schemathesis/lazy.py +10 -4
  20. schemathesis/loaders.py +57 -0
  21. schemathesis/models.py +120 -96
  22. schemathesis/parameters.py +3 -0
  23. schemathesis/runner/__init__.py +3 -0
  24. schemathesis/runner/events.py +55 -20
  25. schemathesis/runner/impl/core.py +54 -54
  26. schemathesis/runner/serialization.py +75 -34
  27. schemathesis/sanitization.py +248 -0
  28. schemathesis/schemas.py +21 -6
  29. schemathesis/serializers.py +32 -3
  30. schemathesis/service/serialization.py +5 -1
  31. schemathesis/specs/graphql/loaders.py +44 -13
  32. schemathesis/specs/graphql/schemas.py +56 -25
  33. schemathesis/specs/openapi/_hypothesis.py +11 -23
  34. schemathesis/specs/openapi/definitions.py +572 -0
  35. schemathesis/specs/openapi/loaders.py +100 -49
  36. schemathesis/specs/openapi/parameters.py +2 -2
  37. schemathesis/specs/openapi/schemas.py +87 -13
  38. schemathesis/specs/openapi/security.py +1 -0
  39. schemathesis/stateful.py +2 -2
  40. schemathesis/utils.py +30 -9
  41. schemathesis-3.20.1.dist-info/METADATA +342 -0
  42. {schemathesis-3.19.7.dist-info → schemathesis-3.20.1.dist-info}/RECORD +45 -39
  43. schemathesis-3.19.7.dist-info/METADATA +0 -291
  44. {schemathesis-3.19.7.dist-info → schemathesis-3.20.1.dist-info}/WHEEL +0 -0
  45. {schemathesis-3.19.7.dist-info → schemathesis-3.20.1.dist-info}/entry_points.txt +0 -0
  46. {schemathesis-3.19.7.dist-info → schemathesis-3.20.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,7 @@
1
1
  import io
2
2
  import json
3
3
  import pathlib
4
+ import re
4
5
  from typing import IO, Any, Callable, Dict, List, Optional, Tuple, Union, cast
5
6
  from urllib.parse import urljoin
6
7
 
@@ -15,10 +16,13 @@ from starlette_testclient import TestClient as ASGIClient
15
16
  from werkzeug.test import Client
16
17
  from yarl import URL
17
18
 
18
- from ...constants import DEFAULT_DATA_GENERATION_METHODS, WAIT_FOR_SCHEMA_INTERVAL, CodeSampleStyle
19
- from ...exceptions import HTTPError, SchemaLoadingError
19
+ from ... import experimental, fixups
20
+ from ...code_samples import CodeSampleStyle
21
+ from ...constants import DEFAULT_DATA_GENERATION_METHODS, WAIT_FOR_SCHEMA_INTERVAL
22
+ from ...exceptions import SchemaError, SchemaErrorType
20
23
  from ...hooks import HookContext, dispatch
21
24
  from ...lazy import LazySchema
25
+ from ...loaders import load_schema_from_url
22
26
  from ...throttling import build_limiter
23
27
  from ...types import DataGenerationMethodInput, Filter, NotSet, PathLike
24
28
  from ...utils import (
@@ -65,6 +69,7 @@ def from_path(
65
69
  code_sample_style: str = CodeSampleStyle.default().name,
66
70
  rate_limit: Optional[str] = None,
67
71
  encoding: str = "utf8",
72
+ sanitize_output: bool = True,
68
73
  ) -> BaseOpenAPISchema:
69
74
  """Load Open API schema via a file from an OS path.
70
75
 
@@ -87,6 +92,7 @@ def from_path(
87
92
  code_sample_style=code_sample_style,
88
93
  location=pathlib.Path(path).absolute().as_uri(),
89
94
  rate_limit=rate_limit,
95
+ sanitize_output=sanitize_output,
90
96
  __expects_json=_is_json_path(path),
91
97
  )
92
98
 
@@ -108,6 +114,7 @@ def from_uri(
108
114
  code_sample_style: str = CodeSampleStyle.default().name,
109
115
  wait_for_schema: Optional[float] = None,
110
116
  rate_limit: Optional[str] = None,
117
+ sanitize_output: bool = True,
111
118
  **kwargs: Any,
112
119
  ) -> BaseOpenAPISchema:
113
120
  """Load Open API schema from the network.
@@ -134,44 +141,35 @@ def from_uri(
134
141
  else:
135
142
  _load_schema = requests.get
136
143
 
137
- response = _load_schema(uri, **kwargs)
138
- HTTPError.raise_for_status(response)
139
- try:
140
- return from_file(
141
- response.text,
142
- app=app,
143
- base_url=base_url,
144
- method=method,
145
- endpoint=endpoint,
146
- tag=tag,
147
- operation_id=operation_id,
148
- skip_deprecated_operations=skip_deprecated_operations,
149
- validate_schema=validate_schema,
150
- force_schema_version=force_schema_version,
151
- data_generation_methods=data_generation_methods,
152
- code_sample_style=code_sample_style,
153
- location=uri,
154
- rate_limit=rate_limit,
155
- __expects_json=_is_json_response(response),
156
- )
157
- except SchemaLoadingError as exc:
158
- content_type = response.headers.get("Content-Type")
159
- if content_type is not None:
160
- raise SchemaLoadingError(f"{exc.args[0]}. The actual response has `{content_type}` Content-Type") from exc
161
- raise
144
+ response = load_schema_from_url(lambda: _load_schema(uri, **kwargs))
145
+ return from_file(
146
+ response.text,
147
+ app=app,
148
+ base_url=base_url,
149
+ method=method,
150
+ endpoint=endpoint,
151
+ tag=tag,
152
+ operation_id=operation_id,
153
+ skip_deprecated_operations=skip_deprecated_operations,
154
+ validate_schema=validate_schema,
155
+ force_schema_version=force_schema_version,
156
+ data_generation_methods=data_generation_methods,
157
+ code_sample_style=code_sample_style,
158
+ location=uri,
159
+ rate_limit=rate_limit,
160
+ sanitize_output=sanitize_output,
161
+ __expects_json=_is_json_response(response),
162
+ )
162
163
 
163
164
 
164
- SCHEMA_LOADING_ERROR = (
165
- "It seems like the schema you are trying to load is malformed. "
166
- "Schemathesis expects API schemas in JSON or YAML formats"
167
- )
165
+ SCHEMA_LOADING_ERROR = "Received unsupported content while expecting a JSON or YAML payload for Open API"
168
166
 
169
167
 
170
168
  def _load_yaml(data: str) -> Dict[str, Any]:
171
169
  try:
172
170
  return yaml.load(data, StringDatesYAMLLoader)
173
171
  except yaml.YAMLError as exc:
174
- raise SchemaLoadingError(SCHEMA_LOADING_ERROR) from exc
172
+ raise SchemaError(SchemaErrorType.UNEXPECTED_CONTENT_TYPE, SCHEMA_LOADING_ERROR) from exc
175
173
 
176
174
 
177
175
  def from_file(
@@ -190,6 +188,7 @@ def from_file(
190
188
  code_sample_style: str = CodeSampleStyle.default().name,
191
189
  location: Optional[str] = None,
192
190
  rate_limit: Optional[str] = None,
191
+ sanitize_output: bool = True,
193
192
  __expects_json: bool = False,
194
193
  **kwargs: Any, # needed in the runner to have compatible API across all loaders
195
194
  ) -> BaseOpenAPISchema:
@@ -226,9 +225,17 @@ def from_file(
226
225
  code_sample_style=code_sample_style,
227
226
  location=location,
228
227
  rate_limit=rate_limit,
228
+ sanitize_output=sanitize_output,
229
229
  )
230
230
 
231
231
 
232
+ def _is_fast_api(app: Any) -> bool:
233
+ for cls in app.__class__.__mro__:
234
+ if f"{cls.__module__}.{cls.__qualname__}" == "fastapi.applications.FastAPI":
235
+ return True
236
+ return False
237
+
238
+
232
239
  def from_dict(
233
240
  raw_schema: Dict[str, Any],
234
241
  *,
@@ -245,6 +252,7 @@ def from_dict(
245
252
  code_sample_style: str = CodeSampleStyle.default().name,
246
253
  location: Optional[str] = None,
247
254
  rate_limit: Optional[str] = None,
255
+ sanitize_output: bool = True,
248
256
  ) -> BaseOpenAPISchema:
249
257
  """Load Open API schema from a Python dictionary.
250
258
 
@@ -252,6 +260,12 @@ def from_dict(
252
260
  """
253
261
  _code_sample_style = CodeSampleStyle.from_str(code_sample_style)
254
262
  hook_context = HookContext()
263
+ is_openapi_31 = raw_schema.get("openapi", "").startswith("3.1")
264
+ is_fast_api_fixup_installed = fixups.is_installed("fast_api")
265
+ if is_fast_api_fixup_installed and is_openapi_31:
266
+ fixups.fast_api.uninstall()
267
+ elif _is_fast_api(app):
268
+ fixups.fast_api.adjust_schema(raw_schema)
255
269
  dispatch("before_load_schema", hook_context, raw_schema)
256
270
  rate_limiter: Optional[Limiter] = None
257
271
  if rate_limit is not None:
@@ -273,12 +287,27 @@ def from_dict(
273
287
  code_sample_style=_code_sample_style,
274
288
  location=location,
275
289
  rate_limiter=rate_limiter,
290
+ sanitize_output=sanitize_output,
276
291
  )
277
292
  dispatch("after_load_schema", hook_context, instance)
278
293
  return instance
279
294
 
280
- def init_openapi_3() -> OpenApi30:
281
- _maybe_validate_schema(raw_schema, definitions.OPENAPI_30_VALIDATOR, validate_schema)
295
+ def init_openapi_3(forced: bool) -> OpenApi30:
296
+ version = raw_schema["openapi"]
297
+ if (
298
+ not (is_openapi_31 and experimental.OPEN_API_3_1.is_enabled)
299
+ and not forced
300
+ and not OPENAPI_30_VERSION_RE.match(version)
301
+ ):
302
+ raise SchemaError(
303
+ SchemaErrorType.OPEN_API_UNSUPPORTED_VERSION,
304
+ f"The provided schema uses Open API {version}, which is currently not supported.",
305
+ )
306
+ if is_openapi_31:
307
+ validator = definitions.OPENAPI_31_VALIDATOR
308
+ else:
309
+ validator = definitions.OPENAPI_30_VALIDATOR
310
+ _maybe_validate_schema(raw_schema, validator, validate_schema)
282
311
  instance = OpenApi30(
283
312
  raw_schema,
284
313
  app=app,
@@ -293,6 +322,7 @@ def from_dict(
293
322
  code_sample_style=_code_sample_style,
294
323
  location=location,
295
324
  rate_limiter=rate_limiter,
325
+ sanitize_output=sanitize_output,
296
326
  )
297
327
  dispatch("after_load_schema", hook_context, instance)
298
328
  return instance
@@ -300,22 +330,31 @@ def from_dict(
300
330
  if force_schema_version == "20":
301
331
  return init_openapi_2()
302
332
  if force_schema_version == "30":
303
- return init_openapi_3()
333
+ return init_openapi_3(forced=True)
304
334
  if "swagger" in raw_schema:
305
335
  return init_openapi_2()
306
336
  if "openapi" in raw_schema:
307
- return init_openapi_3()
308
- raise SchemaLoadingError("Unsupported schema type")
337
+ return init_openapi_3(forced=False)
338
+ raise SchemaError(
339
+ SchemaErrorType.OPEN_API_UNSPECIFIED_VERSION,
340
+ "Unable to determine the Open API version as it's not specified in the document.",
341
+ )
342
+
309
343
 
344
+ OPENAPI_30_VERSION_RE = re.compile(r"^3\.0\.\d(-.+)?$")
310
345
 
311
346
  # It is a common case when API schemas are stored in the YAML format and HTTP status codes are numbers
312
347
  # The Open API spec requires HTTP status codes as strings
313
348
  DOC_ENTRY = "https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#patterned-fields-1"
314
- NUMERIC_STATUS_CODES_MESSAGE = f"""The input schema contains HTTP status codes as numbers.
315
- The Open API spec requires them to be strings:
316
- {DOC_ENTRY}
349
+ NUMERIC_STATUS_CODES_MESSAGE = f"""Numeric HTTP status codes detected in your YAML schema.
350
+ According to the Open API specification, status codes must be strings, not numbers.
351
+ For more details, check the Open API documentation: {DOC_ENTRY}
352
+
317
353
  Please, stringify the following status codes:"""
318
- NON_STRING_OBJECT_KEY = "The input schema contains non-string keys in sub-schemas"
354
+ NON_STRING_OBJECT_KEY_MESSAGE = (
355
+ "The Open API specification requires all keys in the schema to be strings. "
356
+ "You have some keys that are not strings."
357
+ )
319
358
 
320
359
 
321
360
  def _format_status_codes(status_codes: List[Tuple[int, List[Union[str, int]]]]) -> str:
@@ -339,12 +378,18 @@ def _maybe_validate_schema(
339
378
  status_codes = validation.find_numeric_http_status_codes(instance)
340
379
  if status_codes:
341
380
  message = _format_status_codes(status_codes)
342
- raise SchemaLoadingError(f"{NUMERIC_STATUS_CODES_MESSAGE}\n{message}") from exc
381
+ raise SchemaError(
382
+ SchemaErrorType.YAML_NUMERIC_STATUS_CODES, f"{NUMERIC_STATUS_CODES_MESSAGE}\n{message}"
383
+ ) from exc
343
384
  # Some other pattern error
344
- raise SchemaLoadingError(NON_STRING_OBJECT_KEY) from exc
345
- raise SchemaLoadingError("Invalid schema") from exc
385
+ raise SchemaError(SchemaErrorType.YAML_NON_STRING_KEYS, NON_STRING_OBJECT_KEY_MESSAGE) from exc
386
+ raise SchemaError(SchemaErrorType.UNCLASSIFIED, "Unknown error") from exc
346
387
  except ValidationError as exc:
347
- raise SchemaLoadingError("The input schema is not a valid Open API schema") from exc
388
+ raise SchemaError(
389
+ SchemaErrorType.OPEN_API_INVALID_SCHEMA,
390
+ "The provided API schema does not appear to be a valid OpenAPI schema",
391
+ extras=[entry for entry in str(exc).splitlines() if entry],
392
+ ) from exc
348
393
 
349
394
 
350
395
  def from_pytest_fixture(
@@ -361,6 +406,7 @@ def from_pytest_fixture(
361
406
  data_generation_methods: Union[DataGenerationMethodInput, NotSet] = NOT_SET,
362
407
  code_sample_style: str = CodeSampleStyle.default().name,
363
408
  rate_limit: Optional[str] = None,
409
+ sanitize_output: bool = True,
364
410
  ) -> LazySchema:
365
411
  """Load schema from a ``pytest`` fixture.
366
412
 
@@ -394,6 +440,7 @@ def from_pytest_fixture(
394
440
  data_generation_methods=_data_generation_methods,
395
441
  code_sample_style=_code_sample_style,
396
442
  rate_limiter=rate_limiter,
443
+ sanitize_output=sanitize_output,
397
444
  )
398
445
 
399
446
 
@@ -412,6 +459,7 @@ def from_wsgi(
412
459
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
413
460
  code_sample_style: str = CodeSampleStyle.default().name,
414
461
  rate_limit: Optional[str] = None,
462
+ sanitize_output: bool = True,
415
463
  **kwargs: Any,
416
464
  ) -> BaseOpenAPISchema:
417
465
  """Load Open API schema from a WSGI app.
@@ -422,8 +470,7 @@ def from_wsgi(
422
470
  require_relative_url(schema_path)
423
471
  setup_headers(kwargs)
424
472
  client = Client(app, WSGIResponse)
425
- response = client.get(schema_path, **kwargs)
426
- HTTPError.check_response(response, schema_path)
473
+ response = load_schema_from_url(lambda: client.get(schema_path, **kwargs))
427
474
  return from_file(
428
475
  response.data,
429
476
  app=app,
@@ -439,6 +486,7 @@ def from_wsgi(
439
486
  code_sample_style=code_sample_style,
440
487
  location=schema_path,
441
488
  rate_limit=rate_limit,
489
+ sanitize_output=sanitize_output,
442
490
  __expects_json=_is_json_response(response),
443
491
  )
444
492
 
@@ -466,6 +514,7 @@ def from_aiohttp(
466
514
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
467
515
  code_sample_style: str = CodeSampleStyle.default().name,
468
516
  rate_limit: Optional[str] = None,
517
+ sanitize_output: bool = True,
469
518
  **kwargs: Any,
470
519
  ) -> BaseOpenAPISchema:
471
520
  """Load Open API schema from an AioHTTP app.
@@ -491,6 +540,7 @@ def from_aiohttp(
491
540
  data_generation_methods=data_generation_methods,
492
541
  code_sample_style=code_sample_style,
493
542
  rate_limit=rate_limit,
543
+ sanitize_output=sanitize_output,
494
544
  **kwargs,
495
545
  )
496
546
 
@@ -510,6 +560,7 @@ def from_asgi(
510
560
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
511
561
  code_sample_style: str = CodeSampleStyle.default().name,
512
562
  rate_limit: Optional[str] = None,
563
+ sanitize_output: bool = True,
513
564
  **kwargs: Any,
514
565
  ) -> BaseOpenAPISchema:
515
566
  """Load Open API schema from an ASGI app.
@@ -520,8 +571,7 @@ def from_asgi(
520
571
  require_relative_url(schema_path)
521
572
  setup_headers(kwargs)
522
573
  client = ASGIClient(app)
523
- response = client.get(schema_path, **kwargs)
524
- HTTPError.check_response(response, schema_path)
574
+ response = load_schema_from_url(lambda: client.get(schema_path, **kwargs))
525
575
  return from_file(
526
576
  response.text,
527
577
  app=app,
@@ -537,5 +587,6 @@ def from_asgi(
537
587
  code_sample_style=code_sample_style,
538
588
  location=schema_path,
539
589
  rate_limit=rate_limit,
590
+ sanitize_output=sanitize_output,
540
591
  __expects_json=_is_json_response(response),
541
592
  )
@@ -2,7 +2,7 @@ import json
2
2
  from dataclasses import dataclass
3
3
  from typing import Any, ClassVar, Dict, Iterable, List, Optional, Tuple
4
4
 
5
- from ...exceptions import InvalidSchema
5
+ from ...exceptions import OperationSchemaError
6
6
  from ...models import APIOperation
7
7
  from ...parameters import Parameter
8
8
  from .converter import to_json_schema_recursive
@@ -412,7 +412,7 @@ def get_parameter_schema(operation: APIOperation, data: Dict[str, Any]) -> Dict[
412
412
  try:
413
413
  content = data["content"]
414
414
  except KeyError as exc:
415
- raise InvalidSchema(
415
+ raise OperationSchemaError(
416
416
  MISSING_SCHEMA_OR_CONTENT_MESSAGE.format(location=data.get("in", ""), name=data.get("name", "<UNKNOWN>")),
417
417
  path=operation.path,
418
418
  method=operation.method,
@@ -1,7 +1,7 @@
1
1
  import itertools
2
2
  import json
3
3
  from collections import defaultdict
4
- from contextlib import ExitStack, contextmanager
4
+ from contextlib import ExitStack, contextmanager, suppress
5
5
  from dataclasses import dataclass, field
6
6
  from difflib import get_close_matches
7
7
  from hashlib import sha1
@@ -15,6 +15,7 @@ from typing import (
15
15
  Generator,
16
16
  Iterable,
17
17
  List,
18
+ NoReturn,
18
19
  Optional,
19
20
  Sequence,
20
21
  Tuple,
@@ -29,17 +30,17 @@ import requests
29
30
  from hypothesis.strategies import SearchStrategy
30
31
  from requests.structures import CaseInsensitiveDict
31
32
 
32
- from ... import failures
33
+ from ... import experimental, failures
33
34
  from ...auths import AuthStorage
34
35
  from ...constants import HTTP_METHODS, DataGenerationMethod
35
36
  from ...exceptions import (
36
- InvalidSchema,
37
+ OperationSchemaError,
37
38
  UsageError,
38
39
  get_missing_content_type_error,
39
40
  get_response_parsing_error,
40
41
  get_schema_validation_error,
41
42
  )
42
- from ...hooks import HookContext, HookDispatcher
43
+ from ...hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, should_skip_operation
43
44
  from ...models import APIOperation, Case, OperationDefinition
44
45
  from ...schemas import BaseSchema
45
46
  from ...stateful import APIStateMachine, Stateful, StatefulTest
@@ -58,6 +59,7 @@ from ...utils import (
58
59
  from . import links, serialization
59
60
  from ._hypothesis import get_case_strategy
60
61
  from .converter import to_json_schema, to_json_schema_recursive
62
+ from .definitions import OPENAPI_30_VALIDATOR, OPENAPI_31_VALIDATOR, SWAGGER_20_VALIDATOR
61
63
  from .examples import get_strategies_from_examples
62
64
  from .filters import (
63
65
  should_skip_by_operation_id,
@@ -78,7 +80,7 @@ from .references import RECURSION_DEPTH_LIMIT, ConvertingResolver, InliningResol
78
80
  from .security import BaseSecurityProcessor, OpenAPISecurityProcessor, SwaggerSecurityProcessor
79
81
  from .stateful import create_state_machine
80
82
 
81
- SCHEMA_ERROR_MESSAGE = "Schema parsing failed. Please check your schema."
83
+ SCHEMA_ERROR_MESSAGE = "Ensure that the definition complies with the OpenAPI specification"
82
84
  SCHEMA_PARSING_ERRORS = (KeyError, AttributeError, jsonschema.exceptions.RefResolutionError)
83
85
 
84
86
 
@@ -146,7 +148,9 @@ class BaseOpenAPISchema(BaseSchema):
146
148
  continue
147
149
  return total
148
150
 
149
- def get_all_operations(self) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
151
+ def get_all_operations(
152
+ self, hooks: Optional[HookDispatcher] = None
153
+ ) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
150
154
  """Iterate over all operations defined in the API.
151
155
 
152
156
  Each yielded item is either `Ok` or `Err`, depending on the presence of errors during schema processing.
@@ -162,11 +166,12 @@ class BaseOpenAPISchema(BaseSchema):
162
166
  In both cases, Schemathesis lets the callee decide what to do with these variants. It allows it to test valid
163
167
  operations and show errors for invalid ones.
164
168
  """
169
+ __tracebackhide__ = True
165
170
  try:
166
171
  paths = self.raw_schema["paths"]
167
172
  except KeyError as exc:
168
173
  # Missing `paths` is not recoverable
169
- raise InvalidSchema(SCHEMA_ERROR_MESSAGE) from exc
174
+ self._raise_invalid_schema(exc)
170
175
 
171
176
  context = HookContext()
172
177
  for path, methods in paths.items():
@@ -199,19 +204,55 @@ class BaseOpenAPISchema(BaseSchema):
199
204
  raw_definition = OperationDefinition(
200
205
  raw_methods[method], resolved_definition, scope, parameters
201
206
  )
202
- yield Ok(self.make_operation(path, method, parameters, raw_definition))
207
+ operation = self.make_operation(path, method, parameters, raw_definition)
208
+ context = HookContext(operation=operation)
209
+ if (
210
+ should_skip_operation(GLOBAL_HOOK_DISPATCHER, context)
211
+ or should_skip_operation(self.hooks, context)
212
+ or (hooks and should_skip_operation(hooks, context))
213
+ ):
214
+ continue
215
+ yield Ok(operation)
203
216
  except SCHEMA_PARSING_ERRORS as exc:
204
217
  yield self._into_err(exc, path, method)
205
218
  except SCHEMA_PARSING_ERRORS as exc:
206
219
  yield self._into_err(exc, path, method)
207
220
 
208
- def _into_err(self, error: Exception, path: Optional[str], method: Optional[str]) -> Err[InvalidSchema]:
221
+ def _into_err(self, error: Exception, path: Optional[str], method: Optional[str]) -> Err[OperationSchemaError]:
222
+ __tracebackhide__ = True
209
223
  try:
210
224
  full_path = self.get_full_path(path) if isinstance(path, str) else None
211
- raise InvalidSchema(SCHEMA_ERROR_MESSAGE, path=path, method=method, full_path=full_path) from error
212
- except InvalidSchema as exc:
225
+ self._raise_invalid_schema(error, full_path, path, method)
226
+ except OperationSchemaError as exc:
213
227
  return Err(exc)
214
228
 
229
+ def _raise_invalid_schema(
230
+ self,
231
+ error: Exception,
232
+ full_path: Optional[str] = None,
233
+ path: Optional[str] = None,
234
+ method: Optional[str] = None,
235
+ ) -> NoReturn:
236
+ __tracebackhide__ = True
237
+ if isinstance(error, jsonschema.exceptions.RefResolutionError):
238
+ raise OperationSchemaError.from_reference_resolution_error(
239
+ error, path=path, method=method, full_path=full_path
240
+ ) from None
241
+ try:
242
+ self.validate()
243
+ except jsonschema.ValidationError as exc:
244
+ raise OperationSchemaError.from_jsonschema_error(
245
+ exc, path=path, method=method, full_path=full_path
246
+ ) from None
247
+ raise OperationSchemaError(SCHEMA_ERROR_MESSAGE, path=path, method=method, full_path=full_path) from error
248
+
249
+ def validate(self) -> None:
250
+ with suppress(TypeError):
251
+ self._validate()
252
+
253
+ def _validate(self) -> None:
254
+ raise NotImplementedError
255
+
215
256
  def collect_parameters(
216
257
  self, parameters: Iterable[Dict[str, Any]], definition: Dict[str, Any]
217
258
  ) -> List[OpenAPIParameter]:
@@ -238,6 +279,7 @@ class BaseOpenAPISchema(BaseSchema):
238
279
  raw_definition: OperationDefinition,
239
280
  ) -> APIOperation:
240
281
  """Create JSON schemas for the query, body, etc from Swagger parameters definitions."""
282
+ __tracebackhide__ = True
241
283
  base_url = self.get_base_url()
242
284
  operation: APIOperation[OpenAPIParameter, Case] = APIOperation(
243
285
  path=path,
@@ -353,7 +395,9 @@ class BaseOpenAPISchema(BaseSchema):
353
395
  responses = operation.definition.resolved["responses"]
354
396
  except KeyError as exc:
355
397
  # Possible to get if `validate_schema=False` is passed during schema creation
356
- raise InvalidSchema("Schema parsing failed. Please check your schema.") from exc
398
+ path = operation.path
399
+ full_path = self.get_full_path(path) if isinstance(path, str) else None
400
+ self._raise_invalid_schema(exc, full_path, path, operation.method)
357
401
  status_code = str(response.status_code)
358
402
  if status_code in responses:
359
403
  return responses[status_code]
@@ -494,9 +538,13 @@ class BaseOpenAPISchema(BaseSchema):
494
538
  resolver = ConvertingResolver(
495
539
  self.location or "", self.raw_schema, nullable_name=self.nullable_name, is_response_schema=True
496
540
  )
541
+ if self.spec_version.startswith("3.1") and experimental.OPEN_API_3_1.is_enabled:
542
+ cls = jsonschema.Draft202012Validator
543
+ else:
544
+ cls = jsonschema.Draft4Validator
497
545
  with in_scopes(resolver, scopes):
498
546
  try:
499
- jsonschema.validate(data, schema, cls=jsonschema.Draft4Validator, resolver=resolver)
547
+ jsonschema.validate(data, schema, cls=cls, resolver=resolver)
500
548
  except jsonschema.ValidationError as exc:
501
549
  exc_class = get_schema_validation_error(exc)
502
550
  raise exc_class(
@@ -656,6 +704,9 @@ class SwaggerV20(BaseOpenAPISchema):
656
704
  def verbose_name(self) -> str:
657
705
  return f"Swagger {self.spec_version}"
658
706
 
707
+ def _validate(self) -> None:
708
+ SWAGGER_20_VALIDATOR.validate(self.raw_schema)
709
+
659
710
  def _get_base_path(self) -> str:
660
711
  return self.raw_schema.get("basePath", "/")
661
712
 
@@ -806,6 +857,12 @@ class SwaggerV20(BaseOpenAPISchema):
806
857
  consumes = global_consumes
807
858
  return consumes
808
859
 
860
+ def _get_payload_schema(self, definition: Dict[str, Any], media_type: str) -> Optional[Dict[str, Any]]:
861
+ for parameter in definition.get("parameters", []):
862
+ if parameter["in"] == "body":
863
+ return parameter["schema"]
864
+ return None
865
+
809
866
 
810
867
  class OpenApi30(SwaggerV20):
811
868
  nullable_name = "nullable"
@@ -824,6 +881,13 @@ class OpenApi30(SwaggerV20):
824
881
  def verbose_name(self) -> str:
825
882
  return f"Open API {self.spec_version}"
826
883
 
884
+ def _validate(self) -> None:
885
+ if self.spec_version.startswith("3.1"):
886
+ # Currently we treat Open API 3.1 as 3.0 in some regard
887
+ OPENAPI_31_VALIDATOR.validate(self.raw_schema)
888
+ else:
889
+ OPENAPI_30_VALIDATOR.validate(self.raw_schema)
890
+
827
891
  def _get_base_path(self) -> str:
828
892
  servers = self.raw_schema.get("servers", [])
829
893
  if servers:
@@ -898,3 +962,13 @@ class OpenApi30(SwaggerV20):
898
962
  files.append((name, (None, form_data[name])))
899
963
  # `None` is the default value for `files` and `data` arguments in `requests.request`
900
964
  return files or None, None
965
+
966
+ def _get_payload_schema(self, definition: Dict[str, Any], media_type: str) -> Optional[Dict[str, Any]]:
967
+ if "requestBody" in definition:
968
+ if "$ref" in definition["requestBody"]:
969
+ body = self.resolver.resolve_all(definition["requestBody"], RECURSION_DEPTH_LIMIT)
970
+ else:
971
+ body = definition["requestBody"]
972
+ if "content" in body and media_type in body["content"]:
973
+ return body["content"][media_type]["schema"]
974
+ return None
@@ -16,6 +16,7 @@ class BaseSecurityProcessor:
16
16
 
17
17
  def process_definitions(self, schema: Dict[str, Any], operation: APIOperation, resolver: RefResolver) -> None:
18
18
  """Add relevant security parameters to data generation."""
19
+ __tracebackhide__ = True
19
20
  for definition in self._get_active_definitions(schema, operation, resolver):
20
21
  name = definition.get("name")
21
22
  location = definition.get("in")
schemathesis/stateful.py CHANGED
@@ -11,7 +11,7 @@ from requests.structures import CaseInsensitiveDict
11
11
  from starlette.applications import Starlette
12
12
 
13
13
  from ._compat import IS_HYPOTHESIS_ABOVE_6_68_1
14
- from .exceptions import InvalidSchema
14
+ from .exceptions import OperationSchemaError
15
15
  from .models import APIOperation, Case, CheckFunction
16
16
  from .utils import NOT_SET, GenericResponse, Ok, Result
17
17
 
@@ -100,7 +100,7 @@ class Feedback:
100
100
  settings: Optional[hypothesis.settings],
101
101
  seed: Optional[int],
102
102
  as_strategy_kwargs: Optional[Dict[str, Any]],
103
- ) -> Generator[Result[Tuple[APIOperation, Callable], InvalidSchema], None, None]:
103
+ ) -> Generator[Result[Tuple[APIOperation, Callable], OperationSchemaError], None, None]:
104
104
  """Generate additional tests that use data from the previous ones."""
105
105
  from ._hypothesis import create_test
106
106