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/models.py CHANGED
@@ -13,18 +13,14 @@ from typing import (
13
13
  TYPE_CHECKING,
14
14
  Any,
15
15
  Callable,
16
- Dict,
17
16
  Generator,
18
17
  Generic,
19
18
  Iterator,
20
- List,
21
19
  NoReturn,
22
20
  Optional,
23
21
  Sequence,
24
- Tuple,
25
22
  Type,
26
23
  TypeVar,
27
- Union,
28
24
  cast,
29
25
  )
30
26
  from urllib.parse import quote, unquote, urljoin, urlparse, urlsplit, urlunsplit
@@ -33,7 +29,7 @@ from . import failures, serializers
33
29
  from ._dependency_versions import IS_WERKZEUG_ABOVE_3
34
30
  from .auths import AuthStorage
35
31
  from .code_samples import CodeSampleStyle
36
- from .generation import DataGenerationMethod
32
+ from .generation import DataGenerationMethod, GenerationConfig
37
33
  from .constants import (
38
34
  DEFAULT_RESPONSE_TIMEOUT,
39
35
  SCHEMATHESIS_TEST_CASE_HEADER,
@@ -51,6 +47,7 @@ from .exceptions import (
51
47
  get_grouped_exception,
52
48
  get_timeout_error,
53
49
  prepare_response_payload,
50
+ SkipTest,
54
51
  )
55
52
  from .internal.deprecation import deprecated_property
56
53
  from .internal.copy import fast_deepcopy
@@ -64,6 +61,7 @@ from .generation import generate_random_case_id
64
61
 
65
62
  if TYPE_CHECKING:
66
63
  import werkzeug
64
+ import unittest
67
65
  from requests.structures import CaseInsensitiveDict
68
66
  from hypothesis import strategies as st
69
67
  import requests.auth
@@ -76,11 +74,11 @@ if TYPE_CHECKING:
76
74
  class CaseSource:
77
75
  """Data sources, used to generate a test case."""
78
76
 
79
- case: "Case"
77
+ case: Case
80
78
  response: GenericResponse
81
79
  elapsed: float
82
80
 
83
- def partial_deepcopy(self) -> "CaseSource":
81
+ def partial_deepcopy(self) -> CaseSource:
84
82
  from .transports.responses import copy_response
85
83
 
86
84
  return self.__class__(
@@ -98,7 +96,7 @@ def cant_serialize(media_type: str) -> NoReturn: # type: ignore
98
96
  reject() # type: ignore
99
97
 
100
98
 
101
- @lru_cache()
99
+ @lru_cache
102
100
  def get_request_signature() -> inspect.Signature:
103
101
  import requests
104
102
 
@@ -109,11 +107,11 @@ def get_request_signature() -> inspect.Signature:
109
107
  class PreparedRequestData:
110
108
  method: str
111
109
  url: str
112
- body: Optional[Union[str, bytes]]
110
+ body: str | bytes | None
113
111
  headers: Headers
114
112
 
115
113
 
116
- def prepare_request_data(kwargs: Dict[str, Any]) -> PreparedRequestData:
114
+ def prepare_request_data(kwargs: dict[str, Any]) -> PreparedRequestData:
117
115
  """Prepare request data for generating code samples."""
118
116
  import requests
119
117
 
@@ -128,23 +126,23 @@ def prepare_request_data(kwargs: Dict[str, Any]) -> PreparedRequestData:
128
126
  class Case:
129
127
  """A single test case parameters."""
130
128
 
131
- operation: "APIOperation"
129
+ operation: APIOperation
132
130
  # Unique test case identifier
133
131
  id: str = field(default_factory=generate_random_case_id, compare=False)
134
- path_parameters: Optional[PathParameters] = None
135
- headers: Optional[CaseInsensitiveDict] = None
136
- cookies: Optional[Cookies] = None
137
- query: Optional[Query] = None
132
+ path_parameters: PathParameters | None = None
133
+ headers: CaseInsensitiveDict | None = None
134
+ cookies: Cookies | None = None
135
+ query: Query | None = None
138
136
  # By default, there is no body, but we can't use `None` as the default value because it clashes with `null`
139
137
  # which is a valid payload.
140
- body: Union[Body, NotSet] = NOT_SET
138
+ body: Body | NotSet = NOT_SET
141
139
  # The media type for cases with a payload. For example, "application/json"
142
- media_type: Optional[str] = None
143
- source: Optional[CaseSource] = None
140
+ media_type: str | None = None
141
+ source: CaseSource | None = None
144
142
 
145
143
  # The way the case was generated (None for manually crafted ones)
146
- data_generation_method: Optional[DataGenerationMethod] = None
147
- _auth: Optional[requests.auth.AuthBase] = None
144
+ data_generation_method: DataGenerationMethod | None = None
145
+ _auth: requests.auth.AuthBase | None = None
148
146
 
149
147
  def __repr__(self) -> str:
150
148
  parts = [f"{self.__class__.__name__}("]
@@ -163,7 +161,7 @@ class Case:
163
161
  return hash(self.as_curl_command({SCHEMATHESIS_TEST_CASE_HEADER: "0"}))
164
162
 
165
163
  @deprecated_property(removed_in="4.0", replacement="operation")
166
- def endpoint(self) -> "APIOperation":
164
+ def endpoint(self) -> APIOperation:
167
165
  return self.operation
168
166
 
169
167
  @property
@@ -179,14 +177,14 @@ class Case:
179
177
  return self.operation.method.upper()
180
178
 
181
179
  @property
182
- def base_url(self) -> Optional[str]:
180
+ def base_url(self) -> str | None:
183
181
  return self.operation.base_url
184
182
 
185
183
  @property
186
184
  def app(self) -> Any:
187
185
  return self.operation.app
188
186
 
189
- def set_source(self, response: GenericResponse, case: "Case", elapsed: float) -> None:
187
+ def set_source(self, response: GenericResponse, case: Case, elapsed: float) -> None:
190
188
  self.source = CaseSource(case=case, response=response, elapsed=elapsed)
191
189
 
192
190
  @property
@@ -202,7 +200,7 @@ class Case:
202
200
  # A single unmatched `}` inside the path template may cause this
203
201
  raise OperationSchemaError(f"Malformed path template: `{self.path}`\n\n {exc}") from exc
204
202
 
205
- def get_full_base_url(self) -> Optional[str]:
203
+ def get_full_base_url(self) -> str | None:
206
204
  """Create a full base url, adding "localhost" for WSGI apps."""
207
205
  parts = urlsplit(self.base_url)
208
206
  if not parts.hostname:
@@ -210,15 +208,15 @@ class Case:
210
208
  return urlunsplit(("http", "localhost", path or "", "", ""))
211
209
  return self.base_url
212
210
 
213
- def prepare_code_sample_data(self, headers: Optional[Dict[str, Any]]) -> PreparedRequestData:
211
+ def prepare_code_sample_data(self, headers: dict[str, Any] | None) -> PreparedRequestData:
214
212
  base_url = self.get_full_base_url()
215
213
  kwargs = self.as_requests_kwargs(base_url, headers=headers)
216
214
  return prepare_request_data(kwargs)
217
215
 
218
216
  def get_code_to_reproduce(
219
217
  self,
220
- headers: Optional[Dict[str, Any]] = None,
221
- request: Optional[requests.PreparedRequest] = None,
218
+ headers: dict[str, Any] | None = None,
219
+ request: requests.PreparedRequest | None = None,
222
220
  verify: bool = True,
223
221
  ) -> str:
224
222
  """Construct a Python code to reproduce this case with `requests`."""
@@ -242,7 +240,7 @@ class Case:
242
240
  extra_headers=request_data.headers,
243
241
  )
244
242
 
245
- def as_curl_command(self, headers: Optional[Dict[str, Any]] = None, verify: bool = True) -> str:
243
+ def as_curl_command(self, headers: dict[str, Any] | None = None, verify: bool = True) -> str:
246
244
  """Construct a curl command for a given case."""
247
245
  request_data = self.prepare_code_sample_data(headers)
248
246
  return CodeSampleStyle.curl.generate(
@@ -254,7 +252,7 @@ class Case:
254
252
  extra_headers=request_data.headers,
255
253
  )
256
254
 
257
- def _get_base_url(self, base_url: Optional[str] = None) -> str:
255
+ def _get_base_url(self, base_url: str | None = None) -> str:
258
256
  if base_url is None:
259
257
  if self.base_url is not None:
260
258
  base_url = self.base_url
@@ -265,7 +263,7 @@ class Case:
265
263
  )
266
264
  return base_url
267
265
 
268
- def _get_headers(self, headers: Optional[Dict[str, str]] = None) -> CaseInsensitiveDict:
266
+ def _get_headers(self, headers: dict[str, str] | None = None) -> CaseInsensitiveDict:
269
267
  from requests.structures import CaseInsensitiveDict
270
268
 
271
269
  final_headers = self.headers.copy() if self.headers is not None else CaseInsensitiveDict()
@@ -275,7 +273,7 @@ class Case:
275
273
  final_headers.setdefault(SCHEMATHESIS_TEST_CASE_HEADER, self.id)
276
274
  return final_headers
277
275
 
278
- def _get_serializer(self) -> Optional[Serializer]:
276
+ def _get_serializer(self) -> Serializer | None:
279
277
  """Get a serializer for the payload, if there is any."""
280
278
  if self.media_type is not None:
281
279
  media_type = serializers.get_first_matching_media_type(self.media_type)
@@ -288,9 +286,7 @@ class Case:
288
286
  return cls()
289
287
  return None
290
288
 
291
- def as_requests_kwargs(
292
- self, base_url: Optional[str] = None, headers: Optional[Dict[str, str]] = None
293
- ) -> Dict[str, Any]:
289
+ def as_requests_kwargs(self, base_url: str | None = None, headers: dict[str, str] | None = None) -> dict[str, Any]:
294
290
  """Convert the case into a dictionary acceptable by requests."""
295
291
  final_headers = self._get_headers(headers)
296
292
  if self.media_type and self.media_type != "multipart/form-data" and not isinstance(self.body, NotSet):
@@ -302,7 +298,7 @@ class Case:
302
298
  if not base_url.endswith("/"):
303
299
  base_url += "/"
304
300
  url = unquote(urljoin(base_url, quote(formatted_path)))
305
- extra: Dict[str, Any]
301
+ extra: dict[str, Any]
306
302
  serializer = self._get_serializer()
307
303
  if serializer is not None and not isinstance(self.body, NotSet):
308
304
  context = SerializerContext(case=self)
@@ -327,11 +323,11 @@ class Case:
327
323
 
328
324
  def call(
329
325
  self,
330
- base_url: Optional[str] = None,
331
- session: Optional[requests.Session] = None,
332
- headers: Optional[Dict[str, Any]] = None,
333
- params: Optional[Dict[str, Any]] = None,
334
- cookies: Optional[Dict[str, Any]] = None,
326
+ base_url: str | None = None,
327
+ session: requests.Session | None = None,
328
+ headers: dict[str, Any] | None = None,
329
+ params: dict[str, Any] | None = None,
330
+ cookies: dict[str, Any] | None = None,
335
331
  **kwargs: Any,
336
332
  ) -> requests.Response:
337
333
  import requests
@@ -371,13 +367,13 @@ class Case:
371
367
  session.close()
372
368
  return response
373
369
 
374
- def as_werkzeug_kwargs(self, headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
370
+ def as_werkzeug_kwargs(self, headers: dict[str, str] | None = None) -> dict[str, Any]:
375
371
  """Convert the case into a dictionary acceptable by werkzeug.Client."""
376
372
  final_headers = self._get_headers(headers)
377
373
  if self.media_type and not isinstance(self.body, NotSet):
378
374
  # If we need to send a payload, then the Content-Type header should be set
379
375
  final_headers["Content-Type"] = self.media_type
380
- extra: Dict[str, Any]
376
+ extra: dict[str, Any]
381
377
  serializer = self._get_serializer()
382
378
  if serializer is not None and not isinstance(self.body, NotSet):
383
379
  context = SerializerContext(case=self)
@@ -396,8 +392,8 @@ class Case:
396
392
  def call_wsgi(
397
393
  self,
398
394
  app: Any = None,
399
- headers: Optional[Dict[str, str]] = None,
400
- query_string: Optional[Dict[str, str]] = None,
395
+ headers: dict[str, str] | None = None,
396
+ query_string: dict[str, str] | None = None,
401
397
  **kwargs: Any,
402
398
  ) -> WSGIResponse:
403
399
  from .transports.responses import WSGIResponse
@@ -426,8 +422,8 @@ class Case:
426
422
  def call_asgi(
427
423
  self,
428
424
  app: Any = None,
429
- base_url: Optional[str] = None,
430
- headers: Optional[Dict[str, str]] = None,
425
+ base_url: str | None = None,
426
+ headers: dict[str, str] | None = None,
431
427
  **kwargs: Any,
432
428
  ) -> requests.Response:
433
429
  from starlette_testclient import TestClient as ASGIClient
@@ -447,10 +443,10 @@ class Case:
447
443
  def validate_response(
448
444
  self,
449
445
  response: GenericResponse,
450
- checks: Tuple["CheckFunction", ...] = (),
451
- additional_checks: Tuple["CheckFunction", ...] = (),
452
- excluded_checks: Tuple["CheckFunction", ...] = (),
453
- code_sample_style: Optional[str] = None,
446
+ checks: tuple[CheckFunction, ...] = (),
447
+ additional_checks: tuple[CheckFunction, ...] = (),
448
+ excluded_checks: tuple[CheckFunction, ...] = (),
449
+ code_sample_style: str | None = None,
454
450
  ) -> None:
455
451
  """Validate application response.
456
452
 
@@ -536,11 +532,11 @@ class Case:
536
532
 
537
533
  def call_and_validate(
538
534
  self,
539
- base_url: Optional[str] = None,
540
- session: Optional[requests.Session] = None,
541
- headers: Optional[Dict[str, Any]] = None,
542
- checks: Tuple["CheckFunction", ...] = (),
543
- code_sample_style: Optional[str] = None,
535
+ base_url: str | None = None,
536
+ session: requests.Session | None = None,
537
+ headers: dict[str, Any] | None = None,
538
+ checks: tuple[CheckFunction, ...] = (),
539
+ code_sample_style: str | None = None,
544
540
  **kwargs: Any,
545
541
  ) -> requests.Response:
546
542
  __tracebackhide__ = True
@@ -558,7 +554,7 @@ class Case:
558
554
  prepared = requests.Session().prepare_request(request) # type: ignore
559
555
  return cast(str, prepared.url)
560
556
 
561
- def partial_deepcopy(self) -> "Case":
557
+ def partial_deepcopy(self) -> Case:
562
558
  return self.__class__(
563
559
  operation=self.operation.partial_deepcopy(),
564
560
  data_generation_method=self.data_generation_method,
@@ -572,14 +568,14 @@ class Case:
572
568
  )
573
569
 
574
570
 
575
- def _merge_dict_to(data: Dict[str, Any], data_key: str, new: Dict[str, Any]) -> None:
571
+ def _merge_dict_to(data: dict[str, Any], data_key: str, new: dict[str, Any]) -> None:
576
572
  original = data[data_key] or {}
577
573
  for key, value in new.items():
578
574
  original[key] = value
579
575
  data[data_key] = original
580
576
 
581
577
 
582
- def validate_vanilla_requests_kwargs(data: Dict[str, Any]) -> None:
578
+ def validate_vanilla_requests_kwargs(data: dict[str, Any]) -> None:
583
579
  """Check arguments for `requests.Session.request`.
584
580
 
585
581
  Some arguments can be valid for cases like ASGI integration, but at the same time they won't work for the regular
@@ -595,7 +591,7 @@ def validate_vanilla_requests_kwargs(data: Dict[str, Any]) -> None:
595
591
 
596
592
 
597
593
  @contextmanager
598
- def cookie_handler(client: werkzeug.Client, cookies: Optional[Cookies]) -> Generator[None, None, None]:
594
+ def cookie_handler(client: werkzeug.Client, cookies: Cookies | None) -> Generator[None, None, None]:
599
595
  """Set cookies required for a call."""
600
596
  if not cookies:
601
597
  yield
@@ -631,13 +627,13 @@ class OperationDefinition(Generic[P, D]):
631
627
  scope: str
632
628
  parameters: Sequence[P]
633
629
 
634
- def __contains__(self, item: Union[str, int]) -> bool:
630
+ def __contains__(self, item: str | int) -> bool:
635
631
  return item in self.resolved
636
632
 
637
- def __getitem__(self, item: Union[str, int]) -> Union[None, bool, float, str, list, Dict[str, Any]]:
633
+ def __getitem__(self, item: str | int) -> None | bool | float | str | list | dict[str, Any]:
638
634
  return self.resolved[item]
639
635
 
640
- def get(self, item: Union[str, int], default: Any = None) -> Union[None, bool, float, str, list, Dict[str, Any]]:
636
+ def get(self, item: str | int, default: Any = None) -> None | bool | float | str | list | dict[str, Any]:
641
637
  return self.resolved.get(item, default)
642
638
 
643
639
 
@@ -663,16 +659,16 @@ class APIOperation(Generic[P, C]):
663
659
  path: str
664
660
  method: str
665
661
  definition: OperationDefinition = field(repr=False)
666
- schema: "BaseSchema"
662
+ schema: BaseSchema
667
663
  verbose_name: str = None # type: ignore
668
664
  app: Any = None
669
- base_url: Optional[str] = None
665
+ base_url: str | None = None
670
666
  path_parameters: ParameterSet[P] = field(default_factory=ParameterSet)
671
667
  headers: ParameterSet[P] = field(default_factory=ParameterSet)
672
668
  cookies: ParameterSet[P] = field(default_factory=ParameterSet)
673
669
  query: ParameterSet[P] = field(default_factory=ParameterSet)
674
670
  body: PayloadAlternatives[P] = field(default_factory=PayloadAlternatives)
675
- case_cls: Type[C] = Case # type: ignore
671
+ case_cls: type[C] = Case # type: ignore
676
672
 
677
673
  def __post_init__(self) -> None:
678
674
  if self.verbose_name is None:
@@ -683,14 +679,14 @@ class APIOperation(Generic[P, C]):
683
679
  return self.schema.get_full_path(self.path)
684
680
 
685
681
  @property
686
- def links(self) -> Dict[str, Dict[str, Any]]:
682
+ def links(self) -> dict[str, dict[str, Any]]:
687
683
  return self.schema.get_links(self)
688
684
 
689
685
  def iter_parameters(self) -> Iterator[P]:
690
686
  """Iterate over all operation's parameters."""
691
687
  return chain(self.path_parameters, self.headers, self.cookies, self.query)
692
688
 
693
- def _lookup_container(self, location: str) -> Union[ParameterSet[P], PayloadAlternatives[P], None]:
689
+ def _lookup_container(self, location: str) -> ParameterSet[P] | PayloadAlternatives[P] | None:
694
690
  return {
695
691
  "path": self.path_parameters,
696
692
  "header": self.headers,
@@ -713,7 +709,7 @@ class APIOperation(Generic[P, C]):
713
709
  if container is not None:
714
710
  container.add(parameter)
715
711
 
716
- def get_parameter(self, name: str, location: str) -> Optional[P]:
712
+ def get_parameter(self, name: str, location: str) -> P | None:
717
713
  container = self._lookup_container(location)
718
714
  if container is not None:
719
715
  return container.get(name)
@@ -721,13 +717,16 @@ class APIOperation(Generic[P, C]):
721
717
 
722
718
  def as_strategy(
723
719
  self,
724
- hooks: Optional["HookDispatcher"] = None,
725
- auth_storage: Optional[AuthStorage] = None,
720
+ hooks: HookDispatcher | None = None,
721
+ auth_storage: AuthStorage | None = None,
726
722
  data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
723
+ generation_config: GenerationConfig | None = None,
727
724
  **kwargs: Any,
728
725
  ) -> st.SearchStrategy:
729
726
  """Turn this API operation into a Hypothesis strategy."""
730
- strategy = self.schema.get_case_strategy(self, hooks, auth_storage, data_generation_method, **kwargs)
727
+ strategy = self.schema.get_case_strategy(
728
+ self, hooks, auth_storage, data_generation_method, generation_config=generation_config, **kwargs
729
+ )
731
730
 
732
731
  def _apply_hooks(dispatcher: HookDispatcher, _strategy: st.SearchStrategy[Case]) -> st.SearchStrategy[Case]:
733
732
  context = HookContext(self)
@@ -750,17 +749,17 @@ class APIOperation(Generic[P, C]):
750
749
  strategy = _apply_hooks(hooks, strategy)
751
750
  return strategy
752
751
 
753
- def get_security_requirements(self) -> List[str]:
752
+ def get_security_requirements(self) -> list[str]:
754
753
  return self.schema.get_security_requirements(self)
755
754
 
756
- def get_strategies_from_examples(self) -> List[st.SearchStrategy[Case]]:
755
+ def get_strategies_from_examples(self) -> list[st.SearchStrategy[Case]]:
757
756
  """Get examples from the API operation."""
758
757
  return self.schema.get_strategies_from_examples(self)
759
758
 
760
- def get_stateful_tests(self, response: GenericResponse, stateful: Optional["Stateful"]) -> Sequence["StatefulTest"]:
759
+ def get_stateful_tests(self, response: GenericResponse, stateful: Stateful | None) -> Sequence[StatefulTest]:
761
760
  return self.schema.get_stateful_tests(response, self, stateful)
762
761
 
763
- def get_parameter_serializer(self, location: str) -> Optional[Callable]:
762
+ def get_parameter_serializer(self, location: str) -> Callable | None:
764
763
  """Get a function that serializes parameters for the given location.
765
764
 
766
765
  It handles serializing data into various `collectionFormat` options and similar.
@@ -768,13 +767,13 @@ class APIOperation(Generic[P, C]):
768
767
  """
769
768
  return self.schema.get_parameter_serializer(self, location)
770
769
 
771
- def prepare_multipart(self, form_data: FormData) -> Tuple[Optional[List], Optional[Dict[str, Any]]]:
770
+ def prepare_multipart(self, form_data: FormData) -> tuple[list | None, dict[str, Any] | None]:
772
771
  return self.schema.prepare_multipart(form_data, self)
773
772
 
774
- def get_request_payload_content_types(self) -> List[str]:
773
+ def get_request_payload_content_types(self) -> list[str]:
775
774
  return self.schema.get_request_payload_content_types(self)
776
775
 
777
- def partial_deepcopy(self) -> "APIOperation":
776
+ def partial_deepcopy(self) -> APIOperation:
778
777
  return self.__class__(
779
778
  path=self.path, # string, immutable
780
779
  method=self.method, # string, immutable
@@ -790,7 +789,7 @@ class APIOperation(Generic[P, C]):
790
789
  body=fast_deepcopy(self.body),
791
790
  )
792
791
 
793
- def clone(self, **components: Any) -> "APIOperation":
792
+ def clone(self, **components: Any) -> APIOperation:
794
793
  """Create a new instance of this API operation with updated components."""
795
794
  return self.__class__(
796
795
  path=self.path,
@@ -810,12 +809,12 @@ class APIOperation(Generic[P, C]):
810
809
  def make_case(
811
810
  self,
812
811
  *,
813
- path_parameters: Optional[PathParameters] = None,
814
- headers: Optional[Headers] = None,
815
- cookies: Optional[Cookies] = None,
816
- query: Optional[Query] = None,
817
- body: Union[Body, NotSet] = NOT_SET,
818
- media_type: Optional[str] = None,
812
+ path_parameters: PathParameters | None = None,
813
+ headers: Headers | None = None,
814
+ cookies: Cookies | None = None,
815
+ query: Query | None = None,
816
+ body: Body | NotSet = NOT_SET,
817
+ media_type: str | None = None,
819
818
  ) -> C:
820
819
  """Create a new example for this API operation.
821
820
 
@@ -837,7 +836,7 @@ class APIOperation(Generic[P, C]):
837
836
  path = self.path.replace("~", "~0").replace("/", "~1")
838
837
  return f"#/paths/{path}/{self.method}"
839
838
 
840
- def validate_response(self, response: GenericResponse) -> None:
839
+ def validate_response(self, response: GenericResponse) -> bool | None:
841
840
  """Validate API response for conformance.
842
841
 
843
842
  :raises CheckFailed: If the response does not conform to the API schema.
@@ -852,10 +851,10 @@ class APIOperation(Generic[P, C]):
852
851
  except CheckFailed:
853
852
  return False
854
853
 
855
- def get_raw_payload_schema(self, media_type: str) -> Optional[Dict[str, Any]]:
854
+ def get_raw_payload_schema(self, media_type: str) -> dict[str, Any] | None:
856
855
  return self.schema._get_payload_schema(self.definition.raw, media_type)
857
856
 
858
- def get_resolved_payload_schema(self, media_type: str) -> Optional[Dict[str, Any]]:
857
+ def get_resolved_payload_schema(self, media_type: str) -> dict[str, Any] | None:
859
858
  return self.schema._get_payload_schema(self.definition.resolved, media_type)
860
859
 
861
860
 
@@ -878,13 +877,13 @@ class Check:
878
877
 
879
878
  name: str
880
879
  value: Status
881
- response: Optional[GenericResponse]
880
+ response: GenericResponse | None
882
881
  elapsed: float
883
882
  example: Case
884
- message: Optional[str] = None
883
+ message: str | None = None
885
884
  # Failure-specific context
886
- context: Optional[FailureContext] = None
887
- request: Optional[requests.PreparedRequest] = None
885
+ context: FailureContext | None = None
886
+ request: requests.PreparedRequest | None = None
888
887
 
889
888
 
890
889
  @dataclass(repr=False)
@@ -893,11 +892,11 @@ class Request:
893
892
 
894
893
  method: str
895
894
  uri: str
896
- body: Optional[str]
895
+ body: str | None
897
896
  headers: Headers
898
897
 
899
898
  @classmethod
900
- def from_case(cls, case: Case, session: requests.Session) -> "Request":
899
+ def from_case(cls, case: Case, session: requests.Session) -> Request:
901
900
  """Create a new `Request` instance from `Case`."""
902
901
  import requests
903
902
 
@@ -908,7 +907,7 @@ class Request:
908
907
  return cls.from_prepared_request(prepared)
909
908
 
910
909
  @classmethod
911
- def from_prepared_request(cls, prepared: requests.PreparedRequest) -> "Request":
910
+ def from_prepared_request(cls, prepared: requests.PreparedRequest) -> Request:
912
911
  """A prepared request version is already stored in `requests.Response`."""
913
912
  body = prepared.body
914
913
 
@@ -933,15 +932,15 @@ class Response:
933
932
 
934
933
  status_code: int
935
934
  message: str
936
- headers: Dict[str, List[str]]
937
- body: Optional[str]
938
- encoding: Optional[str]
935
+ headers: dict[str, list[str]]
936
+ body: str | None
937
+ encoding: str | None
939
938
  http_version: str
940
939
  elapsed: float
941
940
  verify: bool
942
941
 
943
942
  @classmethod
944
- def from_requests(cls, response: requests.Response) -> "Response":
943
+ def from_requests(cls, response: requests.Response) -> Response:
945
944
  """Create a response from requests.Response."""
946
945
  headers = {name: response.raw.headers.getlist(name) for name in response.raw.headers.keys()}
947
946
  # Similar to http.client:319 (HTTP version detection in stdlib's `http` package)
@@ -966,7 +965,7 @@ class Response:
966
965
  )
967
966
 
968
967
  @classmethod
969
- def from_wsgi(cls, response: WSGIResponse, elapsed: float) -> "Response":
968
+ def from_wsgi(cls, response: WSGIResponse, elapsed: float) -> Response:
970
969
  """Create a response from WSGI response."""
971
970
  from .transports.responses import get_reason
972
971
 
@@ -975,7 +974,7 @@ class Response:
975
974
  # Note, this call ensures that `response.response` is a sequence, which is needed for comparison
976
975
  data = response.get_data()
977
976
  body = None if response.response == [] else serialize_payload(data)
978
- encoding: Optional[str]
977
+ encoding: str | None
979
978
  if body is not None:
980
979
  # Werkzeug <3.0 had `charset` attr, newer versions always have UTF-8
981
980
  encoding = response.mimetype_params.get("charset", getattr(response, "charset", "utf-8"))
@@ -999,15 +998,13 @@ class Interaction:
999
998
 
1000
999
  request: Request
1001
1000
  response: Response
1002
- checks: List[Check]
1001
+ checks: list[Check]
1003
1002
  status: Status
1004
1003
  data_generation_method: DataGenerationMethod
1005
1004
  recorded_at: str = field(default_factory=lambda: datetime.datetime.now().isoformat())
1006
1005
 
1007
1006
  @classmethod
1008
- def from_requests(
1009
- cls, case: Case, response: requests.Response, status: Status, checks: List[Check]
1010
- ) -> "Interaction":
1007
+ def from_requests(cls, case: Case, response: requests.Response, status: Status, checks: list[Check]) -> Interaction:
1011
1008
  return cls(
1012
1009
  request=Request.from_prepared_request(response.request),
1013
1010
  response=Response.from_requests(response),
@@ -1021,11 +1018,11 @@ class Interaction:
1021
1018
  cls,
1022
1019
  case: Case,
1023
1020
  response: WSGIResponse,
1024
- headers: Dict[str, Any],
1021
+ headers: dict[str, Any],
1025
1022
  elapsed: float,
1026
1023
  status: Status,
1027
- checks: List[Check],
1028
- ) -> "Interaction":
1024
+ checks: list[Check],
1025
+ ) -> Interaction:
1029
1026
  import requests
1030
1027
 
1031
1028
  session = requests.Session()
@@ -1048,16 +1045,18 @@ class TestResult:
1048
1045
  method: str
1049
1046
  path: str
1050
1047
  verbose_name: str
1051
- data_generation_method: List[DataGenerationMethod]
1052
- checks: List[Check] = field(default_factory=list)
1053
- errors: List[Exception] = field(default_factory=list)
1054
- interactions: List[Interaction] = field(default_factory=list)
1055
- logs: List[LogRecord] = field(default_factory=list)
1048
+ data_generation_method: list[DataGenerationMethod]
1049
+ checks: list[Check] = field(default_factory=list)
1050
+ errors: list[Exception] = field(default_factory=list)
1051
+ interactions: list[Interaction] = field(default_factory=list)
1052
+ logs: list[LogRecord] = field(default_factory=list)
1056
1053
  is_errored: bool = False
1057
1054
  is_flaky: bool = False
1058
1055
  is_skipped: bool = False
1056
+ skip_reason: str | None = None
1059
1057
  is_executed: bool = False
1060
- seed: Optional[int] = None
1058
+ # DEPRECATED: Seed is the same per test run
1059
+ seed: int | None = None
1061
1060
 
1062
1061
  def mark_errored(self) -> None:
1063
1062
  self.is_errored = True
@@ -1065,8 +1064,10 @@ class TestResult:
1065
1064
  def mark_flaky(self) -> None:
1066
1065
  self.is_flaky = True
1067
1066
 
1068
- def mark_skipped(self) -> None:
1067
+ def mark_skipped(self, exc: SkipTest | unittest.case.SkipTest | None) -> None:
1069
1068
  self.is_skipped = True
1069
+ if exc is not None:
1070
+ self.skip_reason = str(exc)
1070
1071
 
1071
1072
  def mark_executed(self) -> None:
1072
1073
  self.is_executed = True
@@ -1094,11 +1095,11 @@ class TestResult:
1094
1095
  self,
1095
1096
  name: str,
1096
1097
  example: Case,
1097
- response: Optional[GenericResponse],
1098
+ response: GenericResponse | None,
1098
1099
  elapsed: float,
1099
1100
  message: str,
1100
- context: Optional[FailureContext],
1101
- request: Optional[requests.PreparedRequest] = None,
1101
+ context: FailureContext | None,
1102
+ request: requests.PreparedRequest | None = None,
1102
1103
  ) -> Check:
1103
1104
  check = Check(
1104
1105
  name=name,
@@ -1117,7 +1118,7 @@ class TestResult:
1117
1118
  self.errors.append(exception)
1118
1119
 
1119
1120
  def store_requests_response(
1120
- self, case: Case, response: requests.Response, status: Status, checks: List[Check]
1121
+ self, case: Case, response: requests.Response, status: Status, checks: list[Check]
1121
1122
  ) -> None:
1122
1123
  self.interactions.append(Interaction.from_requests(case, response, status, checks))
1123
1124
 
@@ -1125,10 +1126,10 @@ class TestResult:
1125
1126
  self,
1126
1127
  case: Case,
1127
1128
  response: WSGIResponse,
1128
- headers: Dict[str, Any],
1129
+ headers: dict[str, Any],
1129
1130
  elapsed: float,
1130
1131
  status: Status,
1131
- checks: List[Check],
1132
+ checks: list[Check],
1132
1133
  ) -> None:
1133
1134
  self.interactions.append(Interaction.from_wsgi(case, response, headers, elapsed, status, checks))
1134
1135
 
@@ -1139,9 +1140,10 @@ class TestResultSet:
1139
1140
 
1140
1141
  __test__ = False
1141
1142
 
1142
- results: List[TestResult] = field(default_factory=list)
1143
- generic_errors: List[OperationSchemaError] = field(default_factory=list)
1144
- warnings: List[str] = field(default_factory=list)
1143
+ seed: int | None
1144
+ results: list[TestResult] = field(default_factory=list)
1145
+ generic_errors: list[OperationSchemaError] = field(default_factory=list)
1146
+ warnings: list[str] = field(default_factory=list)
1145
1147
 
1146
1148
  def __iter__(self) -> Iterator[TestResult]:
1147
1149
  return iter(self.results)
@@ -1186,9 +1188,9 @@ class TestResultSet:
1186
1188
  return self._count(lambda result: result.has_errors or result.is_errored) + len(self.generic_errors)
1187
1189
 
1188
1190
  @property
1189
- def total(self) -> Dict[str, Dict[Union[str, Status], int]]:
1191
+ def total(self) -> dict[str, dict[str | Status, int]]:
1190
1192
  """An aggregated statistic about test results."""
1191
- output: Dict[str, Dict[Union[str, Status], int]] = {}
1193
+ output: dict[str, dict[str | Status, int]] = {}
1192
1194
  for item in self.results:
1193
1195
  for check in item.checks:
1194
1196
  output.setdefault(check.name, Counter())