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
schemathesis/schemas.py CHANGED
@@ -16,18 +16,12 @@ from typing import (
16
16
  Any,
17
17
  Callable,
18
18
  ContextManager,
19
- Dict,
20
19
  Generator,
21
20
  Iterable,
22
21
  Iterator,
23
- List,
24
22
  NoReturn,
25
- Optional,
26
23
  Sequence,
27
- Tuple,
28
- Type,
29
24
  TypeVar,
30
- Union,
31
25
  TYPE_CHECKING,
32
26
  )
33
27
  from urllib.parse import quote, unquote, urljoin, urlparse, urlsplit, urlunsplit
@@ -41,7 +35,12 @@ from .constants import NOT_SET
41
35
  from ._hypothesis import create_test
42
36
  from .auths import AuthStorage
43
37
  from .code_samples import CodeSampleStyle
44
- from .generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod, DataGenerationMethodInput
38
+ from .generation import (
39
+ DEFAULT_DATA_GENERATION_METHODS,
40
+ DataGenerationMethod,
41
+ DataGenerationMethodInput,
42
+ GenerationConfig,
43
+ )
45
44
  from .exceptions import OperationSchemaError, UsageError
46
45
  from .hooks import HookContext, HookDispatcher, HookScope, dispatch
47
46
  from .internal.result import Result, Ok
@@ -84,31 +83,32 @@ class MethodsDict(CaseInsensitiveDict):
84
83
  C = TypeVar("C", bound=Case)
85
84
 
86
85
 
87
- @lru_cache()
86
+ @lru_cache
88
87
  def get_full_path(base_path: str, path: str) -> str:
89
88
  return unquote(urljoin(base_path, quote(path.lstrip("/"))))
90
89
 
91
90
 
92
91
  @dataclass(eq=False)
93
92
  class BaseSchema(Mapping):
94
- raw_schema: Dict[str, Any]
95
- location: Optional[str] = None
96
- base_url: Optional[str] = None
97
- method: Optional[Filter] = None
98
- endpoint: Optional[Filter] = None
99
- tag: Optional[Filter] = None
100
- operation_id: Optional[Filter] = None
93
+ raw_schema: dict[str, Any]
94
+ location: str | None = None
95
+ base_url: str | None = None
96
+ method: Filter | None = None
97
+ endpoint: Filter | None = None
98
+ tag: Filter | None = None
99
+ operation_id: Filter | None = None
101
100
  app: Any = None
102
101
  hooks: HookDispatcher = field(default_factory=lambda: HookDispatcher(scope=HookScope.SCHEMA))
103
102
  auth: AuthStorage = field(default_factory=AuthStorage)
104
- test_function: Optional[GenericTest] = None
103
+ test_function: GenericTest | None = None
105
104
  validate_schema: bool = True
106
105
  skip_deprecated_operations: bool = False
107
- data_generation_methods: List[DataGenerationMethod] = field(
106
+ data_generation_methods: list[DataGenerationMethod] = field(
108
107
  default_factory=lambda: list(DEFAULT_DATA_GENERATION_METHODS)
109
108
  )
109
+ generation_config: GenerationConfig = field(default_factory=GenerationConfig)
110
110
  code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
111
- rate_limiter: Optional[Limiter] = None
111
+ rate_limiter: Limiter | None = None
112
112
  sanitize_output: bool = True
113
113
 
114
114
  def __iter__(self) -> Iterator[str]:
@@ -127,7 +127,7 @@ class BaseSchema(Mapping):
127
127
  def __len__(self) -> int:
128
128
  return len(self.operations)
129
129
 
130
- def hook(self, hook: Union[str, Callable]) -> Callable:
130
+ def hook(self, hook: str | Callable) -> Callable:
131
131
  return self.hooks.register(hook)
132
132
 
133
133
  @property
@@ -169,7 +169,7 @@ class BaseSchema(Mapping):
169
169
  raise NotImplementedError
170
170
 
171
171
  @property
172
- def operations(self) -> Dict[str, MethodsDict]:
172
+ def operations(self) -> dict[str, MethodsDict]:
173
173
  if not hasattr(self, "_operations"):
174
174
  operations = self.get_all_operations()
175
175
  self._operations = operations_to_dict(operations)
@@ -179,38 +179,43 @@ class BaseSchema(Mapping):
179
179
  def operations_count(self) -> int:
180
180
  raise NotImplementedError
181
181
 
182
+ @property
183
+ def links_count(self) -> int:
184
+ raise NotImplementedError
185
+
182
186
  def get_all_operations(
183
- self, hooks: Optional[HookDispatcher] = None
187
+ self, hooks: HookDispatcher | None = None
184
188
  ) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
185
189
  raise NotImplementedError
186
190
 
187
- def get_strategies_from_examples(self, operation: APIOperation) -> List[SearchStrategy[Case]]:
191
+ def get_strategies_from_examples(self, operation: APIOperation) -> list[SearchStrategy[Case]]:
188
192
  """Get examples from the API operation."""
189
193
  raise NotImplementedError
190
194
 
191
- def get_security_requirements(self, operation: APIOperation) -> List[str]:
195
+ def get_security_requirements(self, operation: APIOperation) -> list[str]:
192
196
  """Get applied security requirements for the given API operation."""
193
197
  raise NotImplementedError
194
198
 
195
199
  def get_stateful_tests(
196
- self, response: GenericResponse, operation: APIOperation, stateful: Optional[Stateful]
200
+ self, response: GenericResponse, operation: APIOperation, stateful: Stateful | None
197
201
  ) -> Sequence[StatefulTest]:
198
202
  """Get a list of additional tests, that should be executed after this response from the API operation."""
199
203
  raise NotImplementedError
200
204
 
201
- def get_parameter_serializer(self, operation: APIOperation, location: str) -> Optional[Callable]:
205
+ def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
202
206
  """Get a function that serializes parameters for the given location."""
203
207
  raise NotImplementedError
204
208
 
205
209
  def get_all_tests(
206
210
  self,
207
211
  func: Callable,
208
- settings: Optional[hypothesis.settings] = None,
209
- seed: Optional[int] = None,
210
- as_strategy_kwargs: Optional[Dict[str, Any]] = None,
211
- hooks: Optional[HookDispatcher] = None,
212
- _given_kwargs: Optional[Dict[str, GivenInput]] = None,
213
- ) -> Generator[Result[Tuple[APIOperation, Callable], OperationSchemaError], None, None]:
212
+ settings: hypothesis.settings | None = None,
213
+ generation_config: GenerationConfig | None = None,
214
+ seed: int | None = None,
215
+ as_strategy_kwargs: dict[str, Any] | None = None,
216
+ hooks: HookDispatcher | None = None,
217
+ _given_kwargs: dict[str, GivenInput] | None = None,
218
+ ) -> Generator[Result[tuple[APIOperation, Callable], OperationSchemaError], None, None]:
214
219
  """Generate all operations and Hypothesis tests for them."""
215
220
  for result in self.get_all_operations(hooks=hooks):
216
221
  if isinstance(result, Ok):
@@ -220,6 +225,7 @@ class BaseSchema(Mapping):
220
225
  settings=settings,
221
226
  seed=seed,
222
227
  data_generation_methods=self.data_generation_methods,
228
+ generation_config=generation_config,
223
229
  as_strategy_kwargs=as_strategy_kwargs,
224
230
  _given_kwargs=_given_kwargs,
225
231
  )
@@ -229,14 +235,14 @@ class BaseSchema(Mapping):
229
235
 
230
236
  def parametrize(
231
237
  self,
232
- method: Optional[Filter] = NOT_SET,
233
- endpoint: Optional[Filter] = NOT_SET,
234
- tag: Optional[Filter] = NOT_SET,
235
- operation_id: Optional[Filter] = NOT_SET,
236
- validate_schema: Union[bool, NotSet] = NOT_SET,
237
- skip_deprecated_operations: Union[bool, NotSet] = NOT_SET,
238
- data_generation_methods: Union[Iterable[DataGenerationMethod], NotSet] = NOT_SET,
239
- code_sample_style: Union[str, NotSet] = NOT_SET,
238
+ method: Filter | None = NOT_SET,
239
+ endpoint: Filter | None = NOT_SET,
240
+ tag: Filter | None = NOT_SET,
241
+ operation_id: Filter | None = NOT_SET,
242
+ validate_schema: bool | NotSet = NOT_SET,
243
+ skip_deprecated_operations: bool | NotSet = NOT_SET,
244
+ data_generation_methods: Iterable[DataGenerationMethod] | NotSet = NOT_SET,
245
+ code_sample_style: str | NotSet = NOT_SET,
240
246
  ) -> Callable:
241
247
  """Mark a test function as a parametrized one."""
242
248
  _code_sample_style = (
@@ -278,22 +284,23 @@ class BaseSchema(Mapping):
278
284
  def clone(
279
285
  self,
280
286
  *,
281
- base_url: Union[Optional[str], NotSet] = NOT_SET,
282
- test_function: Optional[GenericTest] = None,
283
- method: Optional[Filter] = NOT_SET,
284
- endpoint: Optional[Filter] = NOT_SET,
285
- tag: Optional[Filter] = NOT_SET,
286
- operation_id: Optional[Filter] = NOT_SET,
287
+ base_url: str | None | NotSet = NOT_SET,
288
+ test_function: GenericTest | None = None,
289
+ method: Filter | None = NOT_SET,
290
+ endpoint: Filter | None = NOT_SET,
291
+ tag: Filter | None = NOT_SET,
292
+ operation_id: Filter | None = NOT_SET,
287
293
  app: Any = NOT_SET,
288
- hooks: Union[HookDispatcher, NotSet] = NOT_SET,
289
- auth: Union[AuthStorage, NotSet] = NOT_SET,
290
- validate_schema: Union[bool, NotSet] = NOT_SET,
291
- skip_deprecated_operations: Union[bool, NotSet] = NOT_SET,
292
- data_generation_methods: Union[DataGenerationMethodInput, NotSet] = NOT_SET,
293
- code_sample_style: Union[CodeSampleStyle, NotSet] = NOT_SET,
294
- rate_limiter: Optional[Limiter] = NOT_SET,
295
- sanitize_output: Optional[Union[bool, NotSet]] = NOT_SET,
296
- ) -> "BaseSchema":
294
+ hooks: HookDispatcher | NotSet = NOT_SET,
295
+ auth: AuthStorage | NotSet = NOT_SET,
296
+ validate_schema: bool | NotSet = NOT_SET,
297
+ skip_deprecated_operations: bool | NotSet = NOT_SET,
298
+ data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
299
+ generation_config: GenerationConfig | NotSet = NOT_SET,
300
+ code_sample_style: CodeSampleStyle | NotSet = NOT_SET,
301
+ rate_limiter: Limiter | None = NOT_SET,
302
+ sanitize_output: bool | NotSet | None = NOT_SET,
303
+ ) -> BaseSchema:
297
304
  if base_url is NOT_SET:
298
305
  base_url = self.base_url
299
306
  if method is NOT_SET:
@@ -316,6 +323,8 @@ class BaseSchema(Mapping):
316
323
  auth = self.auth
317
324
  if data_generation_methods is NOT_SET:
318
325
  data_generation_methods = self.data_generation_methods
326
+ if generation_config is NOT_SET:
327
+ generation_config = self.generation_config
319
328
  if code_sample_style is NOT_SET:
320
329
  code_sample_style = self.code_sample_style
321
330
  if rate_limiter is NOT_SET:
@@ -338,12 +347,13 @@ class BaseSchema(Mapping):
338
347
  validate_schema=validate_schema, # type: ignore
339
348
  skip_deprecated_operations=skip_deprecated_operations, # type: ignore
340
349
  data_generation_methods=data_generation_methods, # type: ignore
350
+ generation_config=generation_config, # type: ignore
341
351
  code_sample_style=code_sample_style, # type: ignore
342
352
  rate_limiter=rate_limiter, # type: ignore
343
353
  sanitize_output=sanitize_output, # type: ignore
344
354
  )
345
355
 
346
- def get_local_hook_dispatcher(self) -> Optional[HookDispatcher]:
356
+ def get_local_hook_dispatcher(self) -> HookDispatcher | None:
347
357
  """Get a HookDispatcher instance bound to the test if present."""
348
358
  # It might be not present when it is used without pytest via `APIOperation.as_strategy()`
349
359
  if self.test_function is not None:
@@ -361,51 +371,52 @@ class BaseSchema(Mapping):
361
371
 
362
372
  def prepare_multipart(
363
373
  self, form_data: FormData, operation: APIOperation
364
- ) -> Tuple[Optional[List], Optional[Dict[str, Any]]]:
374
+ ) -> tuple[list | None, dict[str, Any] | None]:
365
375
  """Split content of `form_data` into files & data.
366
376
 
367
377
  Forms may contain file fields, that we should send via `files` argument in `requests`.
368
378
  """
369
379
  raise NotImplementedError
370
380
 
371
- def get_request_payload_content_types(self, operation: APIOperation) -> List[str]:
381
+ def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
372
382
  raise NotImplementedError
373
383
 
374
384
  def make_case(
375
385
  self,
376
386
  *,
377
- case_cls: Type[C],
387
+ case_cls: type[C],
378
388
  operation: APIOperation,
379
- path_parameters: Optional[PathParameters] = None,
380
- headers: Optional[Headers] = None,
381
- cookies: Optional[Cookies] = None,
382
- query: Optional[Query] = None,
383
- body: Union[Body, NotSet] = NOT_SET,
384
- media_type: Optional[str] = None,
389
+ path_parameters: PathParameters | None = None,
390
+ headers: Headers | None = None,
391
+ cookies: Cookies | None = None,
392
+ query: Query | None = None,
393
+ body: Body | NotSet = NOT_SET,
394
+ media_type: str | None = None,
385
395
  ) -> C:
386
396
  raise NotImplementedError
387
397
 
388
398
  def get_case_strategy(
389
399
  self,
390
400
  operation: APIOperation,
391
- hooks: Optional[HookDispatcher] = None,
392
- auth_storage: Optional[AuthStorage] = None,
401
+ hooks: HookDispatcher | None = None,
402
+ auth_storage: AuthStorage | None = None,
393
403
  data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
404
+ generation_config: GenerationConfig | None = None,
394
405
  **kwargs: Any,
395
406
  ) -> SearchStrategy:
396
407
  raise NotImplementedError
397
408
 
398
- def as_state_machine(self) -> Type[APIStateMachine]:
409
+ def as_state_machine(self) -> type[APIStateMachine]:
399
410
  """Create a state machine class.
400
411
 
401
412
  Use it for stateful testing.
402
413
  """
403
414
  raise NotImplementedError
404
415
 
405
- def get_links(self, operation: APIOperation) -> Dict[str, Dict[str, Any]]:
416
+ def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
406
417
  raise NotImplementedError
407
418
 
408
- def validate_response(self, operation: APIOperation, response: GenericResponse) -> None:
419
+ def validate_response(self, operation: APIOperation, response: GenericResponse) -> bool | None:
409
420
  raise NotImplementedError
410
421
 
411
422
  def prepare_schema(self, schema: Any) -> Any:
@@ -418,14 +429,14 @@ class BaseSchema(Mapping):
418
429
  return self.rate_limiter.ratelimit(label, delay=True, max_delay=0)
419
430
  return nullcontext()
420
431
 
421
- def _get_payload_schema(self, definition: Dict[str, Any], media_type: str) -> Optional[Dict[str, Any]]:
432
+ def _get_payload_schema(self, definition: dict[str, Any], media_type: str) -> dict[str, Any] | None:
422
433
  raise NotImplementedError
423
434
 
424
435
 
425
436
  def operations_to_dict(
426
437
  operations: Generator[Result[APIOperation, OperationSchemaError], None, None]
427
- ) -> Dict[str, MethodsDict]:
428
- output: Dict[str, MethodsDict] = {}
438
+ ) -> dict[str, MethodsDict]:
439
+ output: dict[str, MethodsDict] = {}
429
440
  for result in operations:
430
441
  if isinstance(result, Ok):
431
442
  operation = result.ok()
@@ -1,10 +1,19 @@
1
+ from __future__ import annotations
1
2
  import binascii
2
3
  import os
3
4
  from dataclasses import dataclass
4
5
  from io import BytesIO
5
- from typing import TYPE_CHECKING, Any, Callable, Collection, Dict, Generator, Optional, Type, cast
6
-
7
- from typing_extensions import Protocol, runtime_checkable
6
+ from typing import (
7
+ TYPE_CHECKING,
8
+ Any,
9
+ Callable,
10
+ Collection,
11
+ Dict,
12
+ Generator,
13
+ cast,
14
+ Protocol,
15
+ runtime_checkable,
16
+ )
8
17
 
9
18
  from .internal.copy import fast_deepcopy
10
19
  from ._xml import _to_xml
@@ -19,7 +28,7 @@ if TYPE_CHECKING:
19
28
  from .models import Case
20
29
 
21
30
 
22
- SERIALIZERS: Dict[str, Type["Serializer"]] = {}
31
+ SERIALIZERS: dict[str, type[Serializer]] = {}
23
32
 
24
33
 
25
34
  @dataclass
@@ -40,7 +49,7 @@ class SerializerContext:
40
49
  :ivar Case case: Generated example that is being processed.
41
50
  """
42
51
 
43
- case: "Case"
52
+ case: Case
44
53
 
45
54
  @property
46
55
  def media_type(self) -> str:
@@ -52,11 +61,11 @@ class SerializerContext:
52
61
  # Therefore `schema` is never `None` if called from here. However, `APIOperation.get_raw_payload_schema` is
53
62
  # generic and can be called from other places where it may return `None`
54
63
 
55
- def get_raw_payload_schema(self) -> Dict[str, Any]:
64
+ def get_raw_payload_schema(self) -> dict[str, Any]:
56
65
  schema = self.case.operation.get_raw_payload_schema(self.media_type)
57
66
  return cast(Dict[str, Any], schema)
58
67
 
59
- def get_resolved_payload_schema(self) -> Dict[str, Any]:
68
+ def get_resolved_payload_schema(self) -> dict[str, Any]:
60
69
  schema = self.case.operation.get_resolved_payload_schema(self.media_type)
61
70
  return cast(Dict[str, Any], schema)
62
71
 
@@ -69,14 +78,14 @@ class Serializer(Protocol):
69
78
  `requests` and `werkzeug` transports.
70
79
  """
71
80
 
72
- def as_requests(self, context: SerializerContext, payload: Any) -> Dict[str, Any]:
81
+ def as_requests(self, context: SerializerContext, payload: Any) -> dict[str, Any]:
73
82
  raise NotImplementedError
74
83
 
75
- def as_werkzeug(self, context: SerializerContext, payload: Any) -> Dict[str, Any]:
84
+ def as_werkzeug(self, context: SerializerContext, payload: Any) -> dict[str, Any]:
76
85
  raise NotImplementedError
77
86
 
78
87
 
79
- def register(media_type: str, *, aliases: Collection[str] = ()) -> Callable[[Type[Serializer]], Type[Serializer]]:
88
+ def register(media_type: str, *, aliases: Collection[str] = ()) -> Callable[[type[Serializer]], type[Serializer]]:
80
89
  """Register a serializer for the given media type.
81
90
 
82
91
  Schemathesis uses ``requests`` for regular network calls and ``werkzeug`` for WSGI applications. Your serializer
@@ -99,7 +108,7 @@ def register(media_type: str, *, aliases: Collection[str] = ()) -> Callable[[Typ
99
108
 
100
109
  """
101
110
 
102
- def wrapper(serializer: Type[Serializer]) -> Type[Serializer]:
111
+ def wrapper(serializer: type[Serializer]) -> type[Serializer]:
103
112
  if not issubclass(serializer, Serializer):
104
113
  raise TypeError(
105
114
  f"`{serializer.__name__}` is not a valid serializer. "
@@ -118,7 +127,7 @@ def unregister(media_type: str) -> None:
118
127
  del SERIALIZERS[media_type]
119
128
 
120
129
 
121
- def _to_json(value: Any) -> Dict[str, Any]:
130
+ def _to_json(value: Any) -> dict[str, Any]:
122
131
  if isinstance(value, bytes):
123
132
  # Possible to get via explicit examples, e.g. `externalValue`
124
133
  return {"data": value}
@@ -132,14 +141,14 @@ def _to_json(value: Any) -> Dict[str, Any]:
132
141
 
133
142
  @register("application/json")
134
143
  class JSONSerializer:
135
- def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
144
+ def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
136
145
  return _to_json(value)
137
146
 
138
- def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
147
+ def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
139
148
  return _to_json(value)
140
149
 
141
150
 
142
- def _to_yaml(value: Any) -> Dict[str, Any]:
151
+ def _to_yaml(value: Any) -> dict[str, Any]:
143
152
  import yaml
144
153
 
145
154
  try:
@@ -154,19 +163,19 @@ def _to_yaml(value: Any) -> Dict[str, Any]:
154
163
 
155
164
  @register("text/yaml", aliases=("text/x-yaml", "application/x-yaml", "text/vnd.yaml"))
156
165
  class YAMLSerializer:
157
- def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
166
+ def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
158
167
  return _to_yaml(value)
159
168
 
160
- def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
169
+ def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
161
170
  return _to_yaml(value)
162
171
 
163
172
 
164
173
  @register("application/xml")
165
174
  class XMLSerializer:
166
- def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
175
+ def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
167
176
  return _to_xml(value, context.get_raw_payload_schema(), context.get_resolved_payload_schema())
168
177
 
169
- def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
178
+ def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
170
179
  return _to_xml(value, context.get_raw_payload_schema(), context.get_resolved_payload_schema())
171
180
 
172
181
 
@@ -176,7 +185,7 @@ def _should_coerce_to_bytes(item: Any) -> bool:
176
185
  return isinstance(item, Binary) or not isinstance(item, (bytes, str, int))
177
186
 
178
187
 
179
- def _prepare_form_data(data: Dict[str, Any]) -> Dict[str, Any]:
188
+ def _prepare_form_data(data: dict[str, Any]) -> dict[str, Any]:
180
189
  """Make the generated data suitable for sending as multipart.
181
190
 
182
191
  If the schema is loose, Schemathesis can generate data that can't be sent as multipart. In these cases,
@@ -222,7 +231,7 @@ def _encode_multipart(value: Any, boundary: str) -> bytes:
222
231
 
223
232
  @register("multipart/form-data")
224
233
  class MultipartSerializer:
225
- def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
234
+ def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
226
235
  if isinstance(value, bytes):
227
236
  return {"data": value}
228
237
  if isinstance(value, dict):
@@ -236,27 +245,27 @@ class MultipartSerializer:
236
245
  content_type = f"multipart/form-data; boundary={boundary}"
237
246
  return {"data": raw_data, "headers": {"Content-Type": content_type}}
238
247
 
239
- def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
248
+ def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
240
249
  return {"data": value}
241
250
 
242
251
 
243
252
  @register("application/x-www-form-urlencoded")
244
253
  class URLEncodedFormSerializer:
245
- def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
254
+ def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
246
255
  return {"data": value}
247
256
 
248
- def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
257
+ def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
249
258
  return {"data": value}
250
259
 
251
260
 
252
261
  @register("text/plain")
253
262
  class TextSerializer:
254
- def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
263
+ def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
255
264
  if isinstance(value, bytes):
256
265
  return {"data": value}
257
266
  return {"data": str(value).encode("utf8")}
258
267
 
259
- def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
268
+ def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
260
269
  if isinstance(value, bytes):
261
270
  return {"data": value}
262
271
  return {"data": str(value)}
@@ -264,10 +273,10 @@ class TextSerializer:
264
273
 
265
274
  @register("application/octet-stream")
266
275
  class OctetStreamSerializer:
267
- def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
276
+ def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
268
277
  return {"data": _to_bytes(value)}
269
278
 
270
- def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
279
+ def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
271
280
  return {"data": _to_bytes(value)}
272
281
 
273
282
 
@@ -287,11 +296,11 @@ def get_matching_media_types(media_type: str) -> Generator[str, None, None]:
287
296
  yield registered_media_type
288
297
 
289
298
 
290
- def get_first_matching_media_type(media_type: str) -> Optional[str]:
299
+ def get_first_matching_media_type(media_type: str) -> str | None:
291
300
  return next(get_matching_media_types(media_type), None)
292
301
 
293
302
 
294
- def get(media_type: str) -> Optional[Type[Serializer]]:
303
+ def get(media_type: str) -> type[Serializer] | None:
295
304
  """Get an appropriate serializer for the given media type."""
296
305
  if is_json_media_type(media_type):
297
306
  media_type = "application/json"
@@ -1,9 +1,8 @@
1
+ from __future__ import annotations
1
2
  import enum
2
3
  import os
3
4
  from dataclasses import asdict, dataclass
4
- from typing import Dict, Optional
5
-
6
- from typing_extensions import Protocol, runtime_checkable
5
+ from typing import Protocol, runtime_checkable
7
6
 
8
7
 
9
8
  @enum.unique
@@ -25,17 +24,17 @@ class Environment(Protocol):
25
24
  pass
26
25
 
27
26
  @classmethod
28
- def from_env(cls) -> "Environment":
27
+ def from_env(cls) -> Environment:
29
28
  pass
30
29
 
31
- def asdict(self) -> Dict[str, Optional[str]]:
30
+ def asdict(self) -> dict[str, str | None]:
32
31
  pass
33
32
 
34
- def as_env(self) -> Dict[str, Optional[str]]:
33
+ def as_env(self) -> dict[str, str | None]:
35
34
  pass
36
35
 
37
36
 
38
- def environment() -> Optional[Environment]:
37
+ def environment() -> Environment | None:
39
38
  """Collect environment data for a supported CI provider."""
40
39
  provider = detect()
41
40
  if provider == CIProvider.GITHUB:
@@ -45,7 +44,7 @@ def environment() -> Optional[Environment]:
45
44
  return None
46
45
 
47
46
 
48
- def detect() -> Optional[CIProvider]:
47
+ def detect() -> CIProvider | None:
49
48
  """Detect the current CI provider."""
50
49
  if GitHubActionsEnvironment.is_set():
51
50
  return GitHubActionsEnvironment.provider
@@ -54,7 +53,7 @@ def detect() -> Optional[CIProvider]:
54
53
  return None
55
54
 
56
55
 
57
- def _asdict(env: Environment) -> Dict[str, Optional[str]]:
56
+ def _asdict(env: Environment) -> dict[str, str | None]:
58
57
  data = asdict(env) # type: ignore
59
58
  data["provider"] = env.provider.value
60
59
  return data
@@ -89,24 +88,24 @@ class GitHubActionsEnvironment:
89
88
  workflow: str
90
89
  # The head ref or source branch of the pull request in a workflow run.
91
90
  # For example, `dd/report-ci`.
92
- head_ref: Optional[str]
91
+ head_ref: str | None
93
92
  # The name of the base ref or target branch of the pull request in a workflow run.
94
93
  # For example, `main`.
95
- base_ref: Optional[str]
94
+ base_ref: str | None
96
95
  # The branch or tag ref that triggered the workflow run.
97
96
  # This is only set if a branch or tag is available for the event type.
98
97
  # For example, `refs/pull/1533/merge`
99
- ref: Optional[str]
98
+ ref: str | None
100
99
  # The Schemathesis GitHub Action version.
101
100
  # For example `v1.0.1`
102
- action_ref: Optional[str]
101
+ action_ref: str | None
103
102
 
104
103
  @classmethod
105
104
  def is_set(cls) -> bool:
106
105
  return os.getenv(cls.variable_name) == "true"
107
106
 
108
107
  @classmethod
109
- def from_env(cls) -> "GitHubActionsEnvironment":
108
+ def from_env(cls) -> GitHubActionsEnvironment:
110
109
  return cls(
111
110
  api_url=os.environ["GITHUB_API_URL"],
112
111
  repository=os.environ["GITHUB_REPOSITORY"],
@@ -120,7 +119,7 @@ class GitHubActionsEnvironment:
120
119
  action_ref=os.getenv("SCHEMATHESIS_ACTION_REF"),
121
120
  )
122
121
 
123
- def as_env(self) -> Dict[str, Optional[str]]:
122
+ def as_env(self) -> dict[str, str | None]:
124
123
  return {
125
124
  "GITHUB_API_URL": self.api_url,
126
125
  "GITHUB_REPOSITORY": self.repository,
@@ -161,23 +160,23 @@ class GitLabCIEnvironment:
161
160
  # not documented.
162
161
  # The commit branch name. Not available in merge request pipelines or tag pipelines.
163
162
  # For example, `dd/report-ci`.
164
- commit_branch: Optional[str]
163
+ commit_branch: str | None
165
164
  # The source branch name of the merge request. Only available in merge request pipelines.
166
165
  # For example, `dd/report-ci`.
167
- merge_request_source_branch_name: Optional[str]
166
+ merge_request_source_branch_name: str | None
168
167
  # The target branch name of the merge request.
169
168
  # For example, `main`.
170
- merge_request_target_branch_name: Optional[str]
169
+ merge_request_target_branch_name: str | None
171
170
  # The project-level internal ID of the merge request.
172
171
  # For example, `42`.
173
- merge_request_iid: Optional[str]
172
+ merge_request_iid: str | None
174
173
 
175
174
  @classmethod
176
175
  def is_set(cls) -> bool:
177
176
  return os.getenv(cls.variable_name) == "true"
178
177
 
179
178
  @classmethod
180
- def from_env(cls) -> "GitLabCIEnvironment":
179
+ def from_env(cls) -> GitLabCIEnvironment:
181
180
  return cls(
182
181
  api_v4_url=os.environ["CI_API_V4_URL"],
183
182
  project_id=os.environ["CI_PROJECT_ID"],
@@ -189,7 +188,7 @@ class GitLabCIEnvironment:
189
188
  merge_request_iid=os.getenv("CI_MERGE_REQUEST_IID"),
190
189
  )
191
190
 
192
- def as_env(self) -> Dict[str, Optional[str]]:
191
+ def as_env(self) -> dict[str, str | None]:
193
192
  return {
194
193
  "CI_API_V4_URL": self.api_v4_url,
195
194
  "CI_PROJECT_ID": self.project_id,