schemathesis 3.21.2__py3-none-any.whl → 3.22.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 (95) hide show
  1. schemathesis/__init__.py +1 -1
  2. schemathesis/_compat.py +2 -18
  3. schemathesis/_dependency_versions.py +1 -6
  4. schemathesis/_hypothesis.py +15 -12
  5. schemathesis/_lazy_import.py +3 -2
  6. schemathesis/_xml.py +12 -11
  7. schemathesis/auths.py +88 -81
  8. schemathesis/checks.py +4 -4
  9. schemathesis/cli/__init__.py +202 -171
  10. schemathesis/cli/callbacks.py +29 -32
  11. schemathesis/cli/cassettes.py +25 -25
  12. schemathesis/cli/context.py +18 -12
  13. schemathesis/cli/junitxml.py +2 -2
  14. schemathesis/cli/options.py +10 -11
  15. schemathesis/cli/output/default.py +64 -34
  16. schemathesis/code_samples.py +10 -10
  17. schemathesis/constants.py +1 -1
  18. schemathesis/contrib/unique_data.py +2 -2
  19. schemathesis/exceptions.py +55 -42
  20. schemathesis/extra/_aiohttp.py +2 -2
  21. schemathesis/extra/_flask.py +2 -2
  22. schemathesis/extra/_server.py +3 -2
  23. schemathesis/extra/pytest_plugin.py +10 -10
  24. schemathesis/failures.py +16 -16
  25. schemathesis/filters.py +40 -41
  26. schemathesis/fixups/__init__.py +4 -3
  27. schemathesis/fixups/fast_api.py +5 -4
  28. schemathesis/generation/__init__.py +16 -4
  29. schemathesis/hooks.py +25 -25
  30. schemathesis/internal/jsonschema.py +4 -3
  31. schemathesis/internal/transformation.py +3 -2
  32. schemathesis/lazy.py +39 -31
  33. schemathesis/loaders.py +8 -8
  34. schemathesis/models.py +128 -126
  35. schemathesis/parameters.py +6 -5
  36. schemathesis/runner/__init__.py +107 -81
  37. schemathesis/runner/events.py +37 -26
  38. schemathesis/runner/impl/core.py +86 -81
  39. schemathesis/runner/impl/solo.py +19 -15
  40. schemathesis/runner/impl/threadpool.py +40 -22
  41. schemathesis/runner/serialization.py +67 -40
  42. schemathesis/sanitization.py +18 -20
  43. schemathesis/schemas.py +83 -72
  44. schemathesis/serializers.py +39 -30
  45. schemathesis/service/ci.py +20 -21
  46. schemathesis/service/client.py +29 -9
  47. schemathesis/service/constants.py +1 -0
  48. schemathesis/service/events.py +2 -2
  49. schemathesis/service/hosts.py +8 -7
  50. schemathesis/service/metadata.py +5 -0
  51. schemathesis/service/models.py +22 -4
  52. schemathesis/service/report.py +15 -15
  53. schemathesis/service/serialization.py +23 -27
  54. schemathesis/service/usage.py +8 -7
  55. schemathesis/specs/graphql/loaders.py +31 -24
  56. schemathesis/specs/graphql/nodes.py +3 -2
  57. schemathesis/specs/graphql/scalars.py +26 -2
  58. schemathesis/specs/graphql/schemas.py +38 -34
  59. schemathesis/specs/openapi/_hypothesis.py +62 -44
  60. schemathesis/specs/openapi/checks.py +10 -10
  61. schemathesis/specs/openapi/converter.py +10 -9
  62. schemathesis/specs/openapi/definitions.py +2 -2
  63. schemathesis/specs/openapi/examples.py +22 -21
  64. schemathesis/specs/openapi/expressions/nodes.py +5 -4
  65. schemathesis/specs/openapi/expressions/parser.py +7 -6
  66. schemathesis/specs/openapi/filters.py +6 -6
  67. schemathesis/specs/openapi/formats.py +2 -2
  68. schemathesis/specs/openapi/links.py +19 -21
  69. schemathesis/specs/openapi/loaders.py +133 -78
  70. schemathesis/specs/openapi/negative/__init__.py +16 -11
  71. schemathesis/specs/openapi/negative/mutations.py +11 -10
  72. schemathesis/specs/openapi/parameters.py +20 -19
  73. schemathesis/specs/openapi/references.py +21 -20
  74. schemathesis/specs/openapi/schemas.py +97 -84
  75. schemathesis/specs/openapi/security.py +25 -24
  76. schemathesis/specs/openapi/serialization.py +20 -23
  77. schemathesis/specs/openapi/stateful/__init__.py +12 -11
  78. schemathesis/specs/openapi/stateful/links.py +7 -7
  79. schemathesis/specs/openapi/utils.py +4 -3
  80. schemathesis/specs/openapi/validation.py +3 -2
  81. schemathesis/stateful/__init__.py +15 -16
  82. schemathesis/stateful/state_machine.py +9 -9
  83. schemathesis/targets.py +3 -3
  84. schemathesis/throttling.py +2 -2
  85. schemathesis/transports/auth.py +2 -2
  86. schemathesis/transports/content_types.py +5 -0
  87. schemathesis/transports/headers.py +3 -2
  88. schemathesis/transports/responses.py +1 -1
  89. schemathesis/utils.py +7 -10
  90. {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/METADATA +12 -13
  91. schemathesis-3.22.1.dist-info/RECORD +130 -0
  92. schemathesis-3.21.2.dist-info/RECORD +0 -130
  93. {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/WHEEL +0 -0
  94. {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/entry_points.txt +0 -0
  95. {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/licenses/LICENSE +0 -0
@@ -3,19 +3,24 @@ import io
3
3
  import json
4
4
  import pathlib
5
5
  import re
6
- from typing import IO, Any, Callable, Dict, List, Optional, Tuple, Union, cast, TYPE_CHECKING
6
+ from typing import IO, Any, Callable, cast, TYPE_CHECKING
7
7
  from urllib.parse import urljoin
8
8
 
9
9
  from ... import experimental, fixups
10
10
  from ...code_samples import CodeSampleStyle
11
- from ...generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethodInput, DataGenerationMethod
11
+ from ...generation import (
12
+ DEFAULT_DATA_GENERATION_METHODS,
13
+ DataGenerationMethodInput,
14
+ DataGenerationMethod,
15
+ GenerationConfig,
16
+ )
12
17
  from ...constants import WAIT_FOR_SCHEMA_INTERVAL
13
18
  from ...exceptions import SchemaError, SchemaErrorType
14
19
  from ...hooks import HookContext, dispatch
15
20
  from ...loaders import load_schema_from_url, load_yaml
16
21
  from ...throttling import build_limiter
17
22
  from ...types import Filter, NotSet, PathLike
18
- from ...transports.content_types import is_json_media_type
23
+ from ...transports.content_types import is_json_media_type, is_yaml_media_type
19
24
  from ...transports.headers import setup_default_headers
20
25
  from ...internal.validation import require_relative_url
21
26
  from ...constants import NOT_SET
@@ -37,27 +42,44 @@ def _is_json_response(response: GenericResponse) -> bool:
37
42
  return False
38
43
 
39
44
 
40
- def _is_json_path(path: PathLike) -> bool:
45
+ def _has_suffix(path: PathLike, suffix: str) -> bool:
41
46
  if isinstance(path, str):
42
- return path.endswith(".json")
43
- return path.suffix == ".json"
47
+ return path.endswith(suffix)
48
+ return path.suffix == suffix
49
+
50
+
51
+ def _is_json_path(path: PathLike) -> bool:
52
+ return _has_suffix(path, ".json")
53
+
54
+
55
+ def _is_yaml_response(response: GenericResponse) -> bool:
56
+ """Guess if the response contains YAML."""
57
+ content_type = response.headers.get("Content-Type")
58
+ if content_type is not None:
59
+ return is_yaml_media_type(content_type)
60
+ return False
61
+
62
+
63
+ def _is_yaml_path(path: PathLike) -> bool:
64
+ return _has_suffix(path, ".yaml") or _has_suffix(path, ".yml")
44
65
 
45
66
 
46
67
  def from_path(
47
68
  path: PathLike,
48
69
  *,
49
70
  app: Any = None,
50
- base_url: Optional[str] = None,
51
- method: Optional[Filter] = None,
52
- endpoint: Optional[Filter] = None,
53
- tag: Optional[Filter] = None,
54
- operation_id: Optional[Filter] = None,
71
+ base_url: str | None = None,
72
+ method: Filter | None = None,
73
+ endpoint: Filter | None = None,
74
+ tag: Filter | None = None,
75
+ operation_id: Filter | None = None,
55
76
  skip_deprecated_operations: bool = False,
56
77
  validate_schema: bool = False,
57
- force_schema_version: Optional[str] = None,
78
+ force_schema_version: str | None = None,
58
79
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
80
+ generation_config: GenerationConfig | None = None,
59
81
  code_sample_style: str = CodeSampleStyle.default().name,
60
- rate_limit: Optional[str] = None,
82
+ rate_limit: str | None = None,
61
83
  encoding: str = "utf8",
62
84
  sanitize_output: bool = True,
63
85
  ) -> BaseOpenAPISchema:
@@ -79,11 +101,13 @@ def from_path(
79
101
  validate_schema=validate_schema,
80
102
  force_schema_version=force_schema_version,
81
103
  data_generation_methods=data_generation_methods,
104
+ generation_config=generation_config,
82
105
  code_sample_style=code_sample_style,
83
106
  location=pathlib.Path(path).absolute().as_uri(),
84
107
  rate_limit=rate_limit,
85
108
  sanitize_output=sanitize_output,
86
109
  __expects_json=_is_json_path(path),
110
+ __expects_yaml=_is_yaml_path(path),
87
111
  )
88
112
 
89
113
 
@@ -91,19 +115,20 @@ def from_uri(
91
115
  uri: str,
92
116
  *,
93
117
  app: Any = None,
94
- base_url: Optional[str] = None,
95
- port: Optional[int] = None,
96
- method: Optional[Filter] = None,
97
- endpoint: Optional[Filter] = None,
98
- tag: Optional[Filter] = None,
99
- operation_id: Optional[Filter] = None,
118
+ base_url: str | None = None,
119
+ port: int | None = None,
120
+ method: Filter | None = None,
121
+ endpoint: Filter | None = None,
122
+ tag: Filter | None = None,
123
+ operation_id: Filter | None = None,
100
124
  skip_deprecated_operations: bool = False,
101
125
  validate_schema: bool = False,
102
- force_schema_version: Optional[str] = None,
126
+ force_schema_version: str | None = None,
103
127
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
128
+ generation_config: GenerationConfig | None = None,
104
129
  code_sample_style: str = CodeSampleStyle.default().name,
105
- wait_for_schema: Optional[float] = None,
106
- rate_limit: Optional[str] = None,
130
+ wait_for_schema: float | None = None,
131
+ rate_limit: str | None = None,
107
132
  sanitize_output: bool = True,
108
133
  **kwargs: Any,
109
134
  ) -> BaseOpenAPISchema:
@@ -149,44 +174,57 @@ def from_uri(
149
174
  validate_schema=validate_schema,
150
175
  force_schema_version=force_schema_version,
151
176
  data_generation_methods=data_generation_methods,
177
+ generation_config=generation_config,
152
178
  code_sample_style=code_sample_style,
153
179
  location=uri,
154
180
  rate_limit=rate_limit,
155
181
  sanitize_output=sanitize_output,
156
182
  __expects_json=_is_json_response(response),
183
+ __expects_yaml=_is_yaml_response(response),
157
184
  )
158
185
 
159
186
 
160
187
  SCHEMA_LOADING_ERROR = "Received unsupported content while expecting a JSON or YAML payload for Open API"
188
+ SCHEMA_SYNTAX_ERROR = "API schema does not appear syntactically valid"
161
189
 
162
190
 
163
- def _load_yaml(data: str) -> Dict[str, Any]:
191
+ def _load_yaml(data: str, include_details_on_error: bool = False) -> dict[str, Any]:
164
192
  import yaml
165
193
 
166
194
  try:
167
195
  return load_yaml(data)
168
196
  except yaml.YAMLError as exc:
169
- raise SchemaError(SchemaErrorType.UNEXPECTED_CONTENT_TYPE, SCHEMA_LOADING_ERROR) from exc
197
+ if include_details_on_error:
198
+ type_ = SchemaErrorType.SYNTAX_ERROR
199
+ message = SCHEMA_SYNTAX_ERROR
200
+ extras = [entry for entry in str(exc).splitlines() if entry]
201
+ else:
202
+ type_ = SchemaErrorType.UNEXPECTED_CONTENT_TYPE
203
+ message = SCHEMA_LOADING_ERROR
204
+ extras = []
205
+ raise SchemaError(type_, message, extras=extras) from exc
170
206
 
171
207
 
172
208
  def from_file(
173
- file: Union[IO[str], str],
209
+ file: IO[str] | str,
174
210
  *,
175
211
  app: Any = None,
176
- base_url: Optional[str] = None,
177
- method: Optional[Filter] = None,
178
- endpoint: Optional[Filter] = None,
179
- tag: Optional[Filter] = None,
180
- operation_id: Optional[Filter] = None,
212
+ base_url: str | None = None,
213
+ method: Filter | None = None,
214
+ endpoint: Filter | None = None,
215
+ tag: Filter | None = None,
216
+ operation_id: Filter | None = None,
181
217
  skip_deprecated_operations: bool = False,
182
218
  validate_schema: bool = False,
183
- force_schema_version: Optional[str] = None,
219
+ force_schema_version: str | None = None,
184
220
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
221
+ generation_config: GenerationConfig | None = None,
185
222
  code_sample_style: str = CodeSampleStyle.default().name,
186
- location: Optional[str] = None,
187
- rate_limit: Optional[str] = None,
223
+ location: str | None = None,
224
+ rate_limit: str | None = None,
188
225
  sanitize_output: bool = True,
189
226
  __expects_json: bool = False,
227
+ __expects_yaml: bool = False,
190
228
  **kwargs: Any, # needed in the runner to have compatible API across all loaders
191
229
  ) -> BaseOpenAPISchema:
192
230
  """Load Open API schema from a file descriptor, string or bytes.
@@ -200,13 +238,20 @@ def from_file(
200
238
  if __expects_json:
201
239
  try:
202
240
  raw = json.loads(data)
203
- except json.JSONDecodeError:
241
+ except json.JSONDecodeError as exc:
204
242
  # Fallback to a slower YAML loader. This way we'll still load schemas from responses with
205
243
  # invalid `Content-Type` headers or YAML files that have the `.json` extension.
206
244
  # This is a rare case, and it will be slower but trying JSON first improves a more common use case
207
- raw = _load_yaml(data)
245
+ try:
246
+ raw = _load_yaml(data)
247
+ except SchemaError:
248
+ raise SchemaError(
249
+ SchemaErrorType.SYNTAX_ERROR,
250
+ SCHEMA_SYNTAX_ERROR,
251
+ extras=[entry for entry in str(exc).splitlines() if entry],
252
+ ) from exc
208
253
  else:
209
- raw = _load_yaml(data)
254
+ raw = _load_yaml(data, include_details_on_error=__expects_yaml)
210
255
  return from_dict(
211
256
  raw,
212
257
  app=app,
@@ -219,6 +264,7 @@ def from_file(
219
264
  validate_schema=validate_schema,
220
265
  force_schema_version=force_schema_version,
221
266
  data_generation_methods=data_generation_methods,
267
+ generation_config=generation_config,
222
268
  code_sample_style=code_sample_style,
223
269
  location=location,
224
270
  rate_limit=rate_limit,
@@ -234,21 +280,22 @@ def _is_fast_api(app: Any) -> bool:
234
280
 
235
281
 
236
282
  def from_dict(
237
- raw_schema: Dict[str, Any],
283
+ raw_schema: dict[str, Any],
238
284
  *,
239
285
  app: Any = None,
240
- base_url: Optional[str] = None,
241
- method: Optional[Filter] = None,
242
- endpoint: Optional[Filter] = None,
243
- tag: Optional[Filter] = None,
244
- operation_id: Optional[Filter] = None,
286
+ base_url: str | None = None,
287
+ method: Filter | None = None,
288
+ endpoint: Filter | None = None,
289
+ tag: Filter | None = None,
290
+ operation_id: Filter | None = None,
245
291
  skip_deprecated_operations: bool = False,
246
292
  validate_schema: bool = False,
247
- force_schema_version: Optional[str] = None,
293
+ force_schema_version: str | None = None,
248
294
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
295
+ generation_config: GenerationConfig | None = None,
249
296
  code_sample_style: str = CodeSampleStyle.default().name,
250
- location: Optional[str] = None,
251
- rate_limit: Optional[str] = None,
297
+ location: str | None = None,
298
+ rate_limit: str | None = None,
252
299
  sanitize_output: bool = True,
253
300
  ) -> BaseOpenAPISchema:
254
301
  """Load Open API schema from a Python dictionary.
@@ -266,7 +313,7 @@ def from_dict(
266
313
  elif _is_fast_api(app):
267
314
  fixups.fast_api.adjust_schema(raw_schema)
268
315
  dispatch("before_load_schema", hook_context, raw_schema)
269
- rate_limiter: Optional[Limiter] = None
316
+ rate_limiter: Limiter | None = None
270
317
  if rate_limit is not None:
271
318
  rate_limiter = build_limiter(rate_limit)
272
319
 
@@ -356,7 +403,7 @@ NON_STRING_OBJECT_KEY_MESSAGE = (
356
403
  )
357
404
 
358
405
 
359
- def _format_status_codes(status_codes: List[Tuple[int, List[Union[str, int]]]]) -> str:
406
+ def _format_status_codes(status_codes: list[tuple[int, list[str | int]]]) -> str:
360
407
  buffer = io.StringIO()
361
408
  for status_code, path in status_codes:
362
409
  buffer.write(f" - {status_code} at schema['paths']")
@@ -367,7 +414,7 @@ def _format_status_codes(status_codes: List[Tuple[int, List[Union[str, int]]]])
367
414
 
368
415
 
369
416
  def _maybe_validate_schema(
370
- instance: Dict[str, Any], validator: jsonschema.validators.Draft4Validator, validate_schema: bool
417
+ instance: dict[str, Any], validator: jsonschema.validators.Draft4Validator, validate_schema: bool
371
418
  ) -> None:
372
419
  from jsonschema import ValidationError
373
420
 
@@ -397,16 +444,17 @@ def from_pytest_fixture(
397
444
  fixture_name: str,
398
445
  *,
399
446
  app: Any = NOT_SET,
400
- base_url: Union[Optional[str], NotSet] = NOT_SET,
401
- method: Optional[Filter] = NOT_SET,
402
- endpoint: Optional[Filter] = NOT_SET,
403
- tag: Optional[Filter] = NOT_SET,
404
- operation_id: Optional[Filter] = NOT_SET,
447
+ base_url: str | None | NotSet = NOT_SET,
448
+ method: Filter | None = NOT_SET,
449
+ endpoint: Filter | None = NOT_SET,
450
+ tag: Filter | None = NOT_SET,
451
+ operation_id: Filter | None = NOT_SET,
405
452
  skip_deprecated_operations: bool = False,
406
453
  validate_schema: bool = False,
407
- data_generation_methods: Union[DataGenerationMethodInput, NotSet] = NOT_SET,
454
+ data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
455
+ generation_config: GenerationConfig | NotSet = NOT_SET,
408
456
  code_sample_style: str = CodeSampleStyle.default().name,
409
- rate_limit: Optional[str] = None,
457
+ rate_limit: str | None = None,
410
458
  sanitize_output: bool = True,
411
459
  ) -> LazySchema:
412
460
  """Load schema from a ``pytest`` fixture.
@@ -421,13 +469,13 @@ def from_pytest_fixture(
421
469
  from ...lazy import LazySchema
422
470
 
423
471
  _code_sample_style = CodeSampleStyle.from_str(code_sample_style)
424
- _data_generation_methods: Union[DataGenerationMethodInput, NotSet]
472
+ _data_generation_methods: DataGenerationMethodInput | NotSet
425
473
  if data_generation_methods is not NOT_SET:
426
474
  data_generation_methods = cast(DataGenerationMethodInput, data_generation_methods)
427
475
  _data_generation_methods = DataGenerationMethod.ensure_list(data_generation_methods)
428
476
  else:
429
477
  _data_generation_methods = data_generation_methods
430
- rate_limiter: Optional[Limiter] = None
478
+ rate_limiter: Limiter | None = None
431
479
  if rate_limit is not None:
432
480
  rate_limiter = build_limiter(rate_limit)
433
481
  return LazySchema(
@@ -441,6 +489,7 @@ def from_pytest_fixture(
441
489
  skip_deprecated_operations=skip_deprecated_operations,
442
490
  validate_schema=validate_schema,
443
491
  data_generation_methods=_data_generation_methods,
492
+ generation_config=generation_config,
444
493
  code_sample_style=_code_sample_style,
445
494
  rate_limiter=rate_limiter,
446
495
  sanitize_output=sanitize_output,
@@ -451,17 +500,18 @@ def from_wsgi(
451
500
  schema_path: str,
452
501
  app: Any,
453
502
  *,
454
- base_url: Optional[str] = None,
455
- method: Optional[Filter] = None,
456
- endpoint: Optional[Filter] = None,
457
- tag: Optional[Filter] = None,
458
- operation_id: Optional[Filter] = None,
503
+ base_url: str | None = None,
504
+ method: Filter | None = None,
505
+ endpoint: Filter | None = None,
506
+ tag: Filter | None = None,
507
+ operation_id: Filter | None = None,
459
508
  skip_deprecated_operations: bool = False,
460
509
  validate_schema: bool = False,
461
- force_schema_version: Optional[str] = None,
510
+ force_schema_version: str | None = None,
462
511
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
512
+ generation_config: GenerationConfig | None = None,
463
513
  code_sample_style: str = CodeSampleStyle.default().name,
464
- rate_limit: Optional[str] = None,
514
+ rate_limit: str | None = None,
465
515
  sanitize_output: bool = True,
466
516
  **kwargs: Any,
467
517
  ) -> BaseOpenAPISchema:
@@ -489,6 +539,7 @@ def from_wsgi(
489
539
  validate_schema=validate_schema,
490
540
  force_schema_version=force_schema_version,
491
541
  data_generation_methods=data_generation_methods,
542
+ generation_config=generation_config,
492
543
  code_sample_style=code_sample_style,
493
544
  location=schema_path,
494
545
  rate_limit=rate_limit,
@@ -511,17 +562,18 @@ def from_aiohttp(
511
562
  schema_path: str,
512
563
  app: Any,
513
564
  *,
514
- base_url: Optional[str] = None,
515
- method: Optional[Filter] = None,
516
- endpoint: Optional[Filter] = None,
517
- tag: Optional[Filter] = None,
518
- operation_id: Optional[Filter] = None,
565
+ base_url: str | None = None,
566
+ method: Filter | None = None,
567
+ endpoint: Filter | None = None,
568
+ tag: Filter | None = None,
569
+ operation_id: Filter | None = None,
519
570
  skip_deprecated_operations: bool = False,
520
571
  validate_schema: bool = False,
521
- force_schema_version: Optional[str] = None,
572
+ force_schema_version: str | None = None,
522
573
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
574
+ generation_config: GenerationConfig | None = None,
523
575
  code_sample_style: str = CodeSampleStyle.default().name,
524
- rate_limit: Optional[str] = None,
576
+ rate_limit: str | None = None,
525
577
  sanitize_output: bool = True,
526
578
  **kwargs: Any,
527
579
  ) -> BaseOpenAPISchema:
@@ -546,6 +598,7 @@ def from_aiohttp(
546
598
  validate_schema=validate_schema,
547
599
  force_schema_version=force_schema_version,
548
600
  data_generation_methods=data_generation_methods,
601
+ generation_config=generation_config,
549
602
  code_sample_style=code_sample_style,
550
603
  rate_limit=rate_limit,
551
604
  sanitize_output=sanitize_output,
@@ -557,17 +610,18 @@ def from_asgi(
557
610
  schema_path: str,
558
611
  app: Any,
559
612
  *,
560
- base_url: Optional[str] = None,
561
- method: Optional[Filter] = None,
562
- endpoint: Optional[Filter] = None,
563
- tag: Optional[Filter] = None,
564
- operation_id: Optional[Filter] = None,
613
+ base_url: str | None = None,
614
+ method: Filter | None = None,
615
+ endpoint: Filter | None = None,
616
+ tag: Filter | None = None,
617
+ operation_id: Filter | None = None,
565
618
  skip_deprecated_operations: bool = False,
566
619
  validate_schema: bool = False,
567
- force_schema_version: Optional[str] = None,
620
+ force_schema_version: str | None = None,
568
621
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
622
+ generation_config: GenerationConfig | None = None,
569
623
  code_sample_style: str = CodeSampleStyle.default().name,
570
- rate_limit: Optional[str] = None,
624
+ rate_limit: str | None = None,
571
625
  sanitize_output: bool = True,
572
626
  **kwargs: Any,
573
627
  ) -> BaseOpenAPISchema:
@@ -594,6 +648,7 @@ def from_asgi(
594
648
  validate_schema=validate_schema,
595
649
  force_schema_version=force_schema_version,
596
650
  data_generation_methods=data_generation_methods,
651
+ generation_config=generation_config,
597
652
  code_sample_style=code_sample_style,
598
653
  location=schema_path,
599
654
  rate_limit=rate_limit,
@@ -1,6 +1,7 @@
1
+ from __future__ import annotations
1
2
  from dataclasses import dataclass
2
3
  from functools import lru_cache
3
- from typing import Any, Dict, Optional, Tuple
4
+ from typing import Any
4
5
  from urllib.parse import urlencode
5
6
 
6
7
  import jsonschema
@@ -10,6 +11,7 @@ from hypothesis_jsonschema import from_schema
10
11
  from ..constants import ALL_KEYWORDS
11
12
  from .mutations import MutationContext
12
13
  from .types import Draw, Schema
14
+ from ....generation import GenerationConfig
13
15
 
14
16
 
15
17
  @dataclass
@@ -27,15 +29,15 @@ class CacheKey:
27
29
  return hash((self.operation_name, self.location))
28
30
 
29
31
 
30
- @lru_cache()
32
+ @lru_cache
31
33
  def get_validator(cache_key: CacheKey) -> jsonschema.Draft4Validator:
32
34
  """Get JSON Schema validator for the given schema."""
33
35
  # Each operation / location combo has only a single schema, therefore could be cached
34
36
  return jsonschema.Draft4Validator(cache_key.schema)
35
37
 
36
38
 
37
- @lru_cache()
38
- def split_schema(cache_key: CacheKey) -> Tuple[Schema, Schema]:
39
+ @lru_cache
40
+ def split_schema(cache_key: CacheKey) -> tuple[Schema, Schema]:
39
41
  """Split the schema in two parts.
40
42
 
41
43
  The first one contains only validation JSON Schema keywords, the second one everything else.
@@ -53,9 +55,10 @@ def negative_schema(
53
55
  schema: Schema,
54
56
  operation_name: str,
55
57
  location: str,
56
- media_type: Optional[str],
58
+ media_type: str | None,
59
+ generation_config: GenerationConfig,
57
60
  *,
58
- custom_formats: Dict[str, st.SearchStrategy[str]],
61
+ custom_formats: dict[str, st.SearchStrategy[str]],
59
62
  ) -> st.SearchStrategy:
60
63
  """A strategy for instances that DO NOT match the input schema.
61
64
 
@@ -69,20 +72,22 @@ def negative_schema(
69
72
 
70
73
  if location == "query":
71
74
 
72
- def filter_values(value: Dict[str, Any]) -> bool:
75
+ def filter_values(value: dict[str, Any]) -> bool:
73
76
  return is_non_empty_query(value) and not validator.is_valid(value)
74
77
 
75
78
  else:
76
79
 
77
- def filter_values(value: Dict[str, Any]) -> bool:
80
+ def filter_values(value: dict[str, Any]) -> bool:
78
81
  return not validator.is_valid(value)
79
82
 
80
83
  return mutated(keywords, non_keywords, location, media_type).flatmap(
81
- lambda s: from_schema(s, custom_formats=custom_formats).filter(filter_values)
84
+ lambda s: from_schema(
85
+ s, custom_formats=custom_formats, allow_x00=generation_config.allow_x00, codec=generation_config.codec
86
+ ).filter(filter_values)
82
87
  )
83
88
 
84
89
 
85
- def is_non_empty_query(query: Dict[str, Any]) -> bool:
90
+ def is_non_empty_query(query: dict[str, Any]) -> bool:
86
91
  # Whether this query parameters will be encoded to a non-empty query string
87
92
  result = []
88
93
  for key, values in query.items():
@@ -100,7 +105,7 @@ def is_non_empty_query(query: Dict[str, Any]) -> bool:
100
105
 
101
106
 
102
107
  @st.composite # type: ignore
103
- def mutated(draw: Draw, keywords: Schema, non_keywords: Schema, location: str, media_type: Optional[str]) -> Any:
108
+ def mutated(draw: Draw, keywords: Schema, non_keywords: Schema, location: str, media_type: str | None) -> Any:
104
109
  return MutationContext(
105
110
  keywords=keywords, non_keywords=non_keywords, location=location, media_type=media_type
106
111
  ).mutate(draw)
@@ -1,8 +1,9 @@
1
1
  """Schema mutations."""
2
+ from __future__ import annotations
2
3
  import enum
3
4
  from dataclasses import dataclass
4
5
  from functools import wraps
5
- from typing import Any, Callable, List, Optional, Sequence, Set, Tuple, TypeVar
6
+ from typing import Any, Callable, Sequence, TypeVar
6
7
 
7
8
  from hypothesis import reject
8
9
  from hypothesis import strategies as st
@@ -28,10 +29,10 @@ class MutationResult(enum.Enum):
28
29
  SUCCESS = 1
29
30
  FAILURE = 2
30
31
 
31
- def __ior__(self, other: Any) -> "MutationResult":
32
+ def __ior__(self, other: Any) -> MutationResult:
32
33
  return self | other
33
34
 
34
- def __or__(self, other: Any) -> "MutationResult":
35
+ def __or__(self, other: Any) -> MutationResult:
35
36
  # Syntactic sugar to simplify handling of multiple results
36
37
  if self == MutationResult.SUCCESS:
37
38
  return self
@@ -68,7 +69,7 @@ class MutationContext:
68
69
  # Schema location within API operation (header, query, etc)
69
70
  location: str
70
71
  # Payload media type, if available
71
- media_type: Optional[str]
72
+ media_type: str | None
72
73
 
73
74
  @property
74
75
  def is_header_location(self) -> bool:
@@ -81,7 +82,7 @@ class MutationContext:
81
82
  def mutate(self, draw: Draw) -> Schema:
82
83
  # On the top level, Schemathesis creates "object" schemas for all parameter "in" values except "body", which is
83
84
  # taken as-is. Therefore, we can only apply mutations that won't change the Open API semantics of the schema.
84
- mutations: List[Mutation]
85
+ mutations: list[Mutation]
85
86
  if self.location in ("header", "cookie", "query"):
86
87
  # These objects follow this pattern:
87
88
  # {
@@ -225,7 +226,7 @@ def change_type(context: MutationContext, draw: Draw, schema: Schema) -> Mutatio
225
226
  return MutationResult.SUCCESS
226
227
 
227
228
 
228
- def _get_type_candidates(context: MutationContext, schema: Schema) -> Set[str]:
229
+ def _get_type_candidates(context: MutationContext, schema: Schema) -> set[str]:
229
230
  types = set(get_type(schema))
230
231
  if context.is_path_location:
231
232
  candidates = {"string", "integer", "number", "boolean", "null"} - types
@@ -334,7 +335,7 @@ def _change_items_object(context: MutationContext, draw: Draw, schema: Schema, i
334
335
  return MutationResult.SUCCESS
335
336
 
336
337
 
337
- def _change_items_array(context: MutationContext, draw: Draw, schema: Schema, items: List) -> MutationResult:
338
+ def _change_items_array(context: MutationContext, draw: Draw, schema: Schema, items: list) -> MutationResult:
338
339
  latest_success_index = None
339
340
  for idx, item in enumerate(items):
340
341
  result = MutationResult.FAILURE
@@ -397,12 +398,12 @@ def negate_constraints(context: MutationContext, draw: Draw, schema: Schema) ->
397
398
  DEPENDENCIES = {"exclusiveMaximum": "maximum", "exclusiveMinimum": "minimum"}
398
399
 
399
400
 
400
- def get_mutations(draw: Draw, schema: Schema) -> Tuple[Mutation, ...]:
401
+ def get_mutations(draw: Draw, schema: Schema) -> tuple[Mutation, ...]:
401
402
  """Get mutations possible for a schema."""
402
403
  types = get_type(schema)
403
404
  # On the top-level of Open API schemas, types are always strings, but inside "schema" objects, they are the same as
404
405
  # in JSON Schema, where it could be either a string or an array of strings.
405
- options: List[Mutation] = [negate_constraints, change_type]
406
+ options: list[Mutation] = [negate_constraints, change_type]
406
407
  if "object" in types:
407
408
  options.extend([change_properties, remove_required_property])
408
409
  elif "array" in types:
@@ -414,7 +415,7 @@ def ident(x: T) -> T:
414
415
  return x
415
416
 
416
417
 
417
- def ordered(items: Sequence[T], unique_by: Callable[[T], Any] = ident) -> st.SearchStrategy[List[T]]:
418
+ def ordered(items: Sequence[T], unique_by: Callable[[T], Any] = ident) -> st.SearchStrategy[list[T]]:
418
419
  """Returns a strategy that generates randomly ordered lists of T.
419
420
 
420
421
  NOTE. Items should be unique.