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
@@ -1,15 +1,16 @@
1
+ from __future__ import annotations
1
2
  import ctypes
2
3
  import queue
3
4
  import threading
4
5
  import time
5
6
  from dataclasses import dataclass
6
7
  from queue import Queue
7
- from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Union, cast
8
+ from typing import Any, Callable, Generator, Iterable, cast
8
9
 
9
10
  import hypothesis
10
11
 
11
12
  from ..._hypothesis import create_test
12
- from ...generation import DataGenerationMethod
13
+ from ...generation import DataGenerationMethod, GenerationConfig
13
14
  from ...internal.result import Ok
14
15
  from ...models import CheckFunction, TestResultSet
15
16
  from ...stateful import Feedback, Stateful
@@ -30,11 +31,12 @@ def _run_task(
30
31
  targets: Iterable[Target],
31
32
  data_generation_methods: Iterable[DataGenerationMethod],
32
33
  settings: hypothesis.settings,
33
- seed: Optional[int],
34
+ generation_config: GenerationConfig,
35
+ seed: int | None,
34
36
  results: TestResultSet,
35
- stateful: Optional[Stateful],
37
+ stateful: Stateful | None,
36
38
  stateful_recursion_limit: int,
37
- headers: Optional[Dict[str, Any]] = None,
39
+ headers: dict[str, Any] | None = None,
38
40
  **kwargs: Any,
39
41
  ) -> None:
40
42
  as_strategy_kwargs = {}
@@ -44,7 +46,13 @@ def _run_task(
44
46
  def _run_tests(maker: Callable, recursion_level: int = 0) -> None:
45
47
  if recursion_level > stateful_recursion_limit:
46
48
  return
47
- for _result in maker(test_template, settings, seed, as_strategy_kwargs=as_strategy_kwargs):
49
+ for _result in maker(
50
+ test_template,
51
+ settings=settings,
52
+ generation_config=generation_config,
53
+ seed=seed,
54
+ as_strategy_kwargs=as_strategy_kwargs,
55
+ ):
48
56
  # `result` is always `Ok` here
49
57
  _operation, test = _result.ok()
50
58
  feedback = Feedback(stateful, _operation)
@@ -81,6 +89,7 @@ def _run_task(
81
89
  settings=settings,
82
90
  seed=seed,
83
91
  data_generation_methods=list(data_generation_methods),
92
+ generation_config=generation_config,
84
93
  as_strategy_kwargs=as_strategy_kwargs,
85
94
  )
86
95
  items = Ok((operation, test_function))
@@ -100,12 +109,13 @@ def thread_task(
100
109
  targets: Iterable[Target],
101
110
  data_generation_methods: Iterable[DataGenerationMethod],
102
111
  settings: hypothesis.settings,
103
- auth: Optional[RawAuth],
104
- auth_type: Optional[str],
105
- headers: Optional[Dict[str, Any]],
106
- seed: Optional[int],
112
+ generation_config: GenerationConfig,
113
+ auth: RawAuth | None,
114
+ auth_type: str | None,
115
+ headers: dict[str, Any] | None,
116
+ seed: int | None,
107
117
  results: TestResultSet,
108
- stateful: Optional[Stateful],
118
+ stateful: Stateful | None,
109
119
  stateful_recursion_limit: int,
110
120
  kwargs: Any,
111
121
  ) -> None:
@@ -124,6 +134,7 @@ def thread_task(
124
134
  targets,
125
135
  data_generation_methods,
126
136
  settings,
137
+ generation_config,
127
138
  seed,
128
139
  results,
129
140
  stateful=stateful,
@@ -142,9 +153,10 @@ def wsgi_thread_task(
142
153
  targets: Iterable[Target],
143
154
  data_generation_methods: Iterable[DataGenerationMethod],
144
155
  settings: hypothesis.settings,
145
- seed: Optional[int],
156
+ generation_config: GenerationConfig,
157
+ seed: int | None,
146
158
  results: TestResultSet,
147
- stateful: Optional[Stateful],
159
+ stateful: Stateful | None,
148
160
  stateful_recursion_limit: int,
149
161
  kwargs: Any,
150
162
  ) -> None:
@@ -157,6 +169,7 @@ def wsgi_thread_task(
157
169
  targets,
158
170
  data_generation_methods,
159
171
  settings,
172
+ generation_config,
160
173
  seed,
161
174
  results,
162
175
  stateful=stateful,
@@ -173,10 +186,11 @@ def asgi_thread_task(
173
186
  targets: Iterable[Target],
174
187
  data_generation_methods: Iterable[DataGenerationMethod],
175
188
  settings: hypothesis.settings,
176
- headers: Optional[Dict[str, Any]],
177
- seed: Optional[int],
189
+ generation_config: GenerationConfig,
190
+ headers: dict[str, Any] | None,
191
+ seed: int | None,
178
192
  results: TestResultSet,
179
- stateful: Optional[Stateful],
193
+ stateful: Stateful | None,
180
194
  stateful_recursion_limit: int,
181
195
  kwargs: Any,
182
196
  ) -> None:
@@ -189,6 +203,7 @@ def asgi_thread_task(
189
203
  targets,
190
204
  data_generation_methods,
191
205
  settings,
206
+ generation_config,
192
207
  seed,
193
208
  results,
194
209
  stateful=stateful,
@@ -208,8 +223,8 @@ class ThreadPoolRunner(BaseRunner):
208
223
  """Spread different tests among multiple worker threads."""
209
224
 
210
225
  workers_num: int = 2
211
- request_tls_verify: Union[bool, str] = True
212
- request_cert: Optional[RequestCert] = None
226
+ request_tls_verify: bool | str = True
227
+ request_cert: RequestCert | None = None
213
228
 
214
229
  def _execute(
215
230
  self, results: TestResultSet, stop_event: threading.Event
@@ -276,7 +291,7 @@ class ThreadPoolRunner(BaseRunner):
276
291
 
277
292
  def _init_workers(
278
293
  self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
279
- ) -> List[threading.Thread]:
294
+ ) -> list[threading.Thread]:
280
295
  """Initialize & start workers that will execute tests."""
281
296
  workers = [
282
297
  threading.Thread(
@@ -295,7 +310,7 @@ class ThreadPoolRunner(BaseRunner):
295
310
 
296
311
  def _get_worker_kwargs(
297
312
  self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
298
- ) -> Dict[str, Any]:
313
+ ) -> dict[str, Any]:
299
314
  return {
300
315
  "tasks_queue": tasks_queue,
301
316
  "events_queue": events_queue,
@@ -303,6 +318,7 @@ class ThreadPoolRunner(BaseRunner):
303
318
  "checks": self.checks,
304
319
  "targets": self.targets,
305
320
  "settings": self.hypothesis_settings,
321
+ "generation_config": self.generation_config,
306
322
  "auth": self.auth,
307
323
  "auth_type": self.auth_type,
308
324
  "headers": self.headers,
@@ -328,7 +344,7 @@ class ThreadPoolWSGIRunner(ThreadPoolRunner):
328
344
 
329
345
  def _get_worker_kwargs(
330
346
  self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
331
- ) -> Dict[str, Any]:
347
+ ) -> dict[str, Any]:
332
348
  return {
333
349
  "tasks_queue": tasks_queue,
334
350
  "events_queue": events_queue,
@@ -336,6 +352,7 @@ class ThreadPoolWSGIRunner(ThreadPoolRunner):
336
352
  "checks": self.checks,
337
353
  "targets": self.targets,
338
354
  "settings": self.hypothesis_settings,
355
+ "generation_config": self.generation_config,
339
356
  "seed": self.seed,
340
357
  "results": results,
341
358
  "stateful": self.stateful,
@@ -358,7 +375,7 @@ class ThreadPoolASGIRunner(ThreadPoolRunner):
358
375
 
359
376
  def _get_worker_kwargs(
360
377
  self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
361
- ) -> Dict[str, Any]:
378
+ ) -> dict[str, Any]:
362
379
  return {
363
380
  "tasks_queue": tasks_queue,
364
381
  "events_queue": events_queue,
@@ -366,6 +383,7 @@ class ThreadPoolASGIRunner(ThreadPoolRunner):
366
383
  "checks": self.checks,
367
384
  "targets": self.targets,
368
385
  "settings": self.hypothesis_settings,
386
+ "generation_config": self.generation_config,
369
387
  "headers": self.headers,
370
388
  "seed": self.seed,
371
389
  "results": results,
@@ -6,7 +6,7 @@ from __future__ import annotations
6
6
  import logging
7
7
  import re
8
8
  from dataclasses import dataclass, field
9
- from typing import Any, Dict, List, Optional, Set, Tuple, Union, TYPE_CHECKING
9
+ from typing import Any, TYPE_CHECKING, cast
10
10
 
11
11
  from ..transports import serialize_payload
12
12
  from ..code_samples import get_excluded_headers
@@ -21,6 +21,8 @@ from ..exceptions import (
21
21
  OperationSchemaError,
22
22
  BodyInGetRequestError,
23
23
  InvalidRegularExpression,
24
+ SerializationError,
25
+ UnboundPrefixError,
24
26
  )
25
27
  from ..models import Case, Check, Interaction, Request, Response, Status, TestResult
26
28
 
@@ -33,13 +35,13 @@ if TYPE_CHECKING:
33
35
  class SerializedCase:
34
36
  # Case data
35
37
  id: str
36
- path_parameters: Optional[Dict[str, Any]]
37
- headers: Optional[Dict[str, Any]]
38
- cookies: Optional[Dict[str, Any]]
39
- query: Optional[Dict[str, Any]]
40
- body: Optional[str]
41
- media_type: Optional[str]
42
- data_generation_method: Optional[str]
38
+ path_parameters: dict[str, Any] | None
39
+ headers: dict[str, Any] | None
40
+ cookies: dict[str, Any] | None
41
+ query: dict[str, Any] | None
42
+ body: str | None
43
+ media_type: str | None
44
+ data_generation_method: str | None
43
45
  # Operation data
44
46
  method: str
45
47
  url: str
@@ -48,10 +50,10 @@ class SerializedCase:
48
50
  # Transport info
49
51
  verify: bool
50
52
  # Headers coming from sources outside data generation
51
- extra_headers: Dict[str, Any]
53
+ extra_headers: dict[str, Any]
52
54
 
53
55
  @classmethod
54
- def from_case(cls, case: Case, headers: Optional[Dict[str, Any]], verify: bool) -> "SerializedCase":
56
+ def from_case(cls, case: Case, headers: dict[str, Any] | None, verify: bool) -> SerializedCase:
55
57
  # `headers` include not only explicitly provided headers but also ones added by hooks, custom auth, etc.
56
58
  request_data = case.prepare_code_sample_data(headers)
57
59
  serialized_body = _serialize_body(request_data.body)
@@ -75,7 +77,7 @@ class SerializedCase:
75
77
  )
76
78
 
77
79
 
78
- def _serialize_body(body: Optional[Union[str, bytes]]) -> Optional[str]:
80
+ def _serialize_body(body: str | bytes | None) -> str | None:
79
81
  if body is None:
80
82
  return None
81
83
  if isinstance(body, str):
@@ -90,18 +92,18 @@ class SerializedCheck:
90
92
  # Check result
91
93
  value: Status
92
94
  request: Request
93
- response: Optional[Response]
95
+ response: Response | None
94
96
  # Generated example
95
97
  example: SerializedCase
96
98
  # Message could be absent for plain `assert` statements
97
- message: Optional[str] = None
99
+ message: str | None = None
98
100
  # Failure-specific context
99
- context: Optional[FailureContext] = None
101
+ context: FailureContext | None = None
100
102
  # Cases & responses that were made before this one
101
- history: List["SerializedHistoryEntry"] = field(default_factory=list)
103
+ history: list[SerializedHistoryEntry] = field(default_factory=list)
102
104
 
103
105
  @classmethod
104
- def from_check(cls, check: Check) -> "SerializedCheck":
106
+ def from_check(cls, check: Check) -> SerializedCheck:
105
107
  import requests
106
108
  from ..transports.responses import WSGIResponse
107
109
 
@@ -113,7 +115,7 @@ class SerializedCheck:
113
115
  else:
114
116
  raise InternalError("Can not find request data")
115
117
 
116
- response: Optional[Response]
118
+ response: Response | None
117
119
  if isinstance(check.response, requests.Response):
118
120
  response = Response.from_requests(check.response)
119
121
  elif isinstance(check.response, WSGIResponse):
@@ -136,7 +138,7 @@ class SerializedCheck:
136
138
  )
137
139
 
138
140
 
139
- def _get_headers(headers: Union[Dict[str, Any], CaseInsensitiveDict]) -> Dict[str, str]:
141
+ def _get_headers(headers: dict[str, Any] | CaseInsensitiveDict) -> dict[str, str]:
140
142
  return {key: value[0] for key, value in headers.items() if key not in get_excluded_headers()}
141
143
 
142
144
 
@@ -146,7 +148,7 @@ class SerializedHistoryEntry:
146
148
  response: Response
147
149
 
148
150
 
149
- def get_serialized_history(case: Case) -> List[SerializedHistoryEntry]:
151
+ def get_serialized_history(case: Case) -> list[SerializedHistoryEntry]:
150
152
  import requests
151
153
 
152
154
  history = []
@@ -170,9 +172,9 @@ def get_serialized_history(case: Case) -> List[SerializedHistoryEntry]:
170
172
  @dataclass
171
173
  class SerializedError:
172
174
  type: RuntimeErrorType
173
- title: Optional[str]
174
- message: Optional[str]
175
- extras: List[str]
175
+ title: str | None
176
+ message: str | None
177
+ extras: list[str]
176
178
 
177
179
  # Exception info
178
180
  exception: str
@@ -182,11 +184,11 @@ class SerializedError:
182
184
  def with_exception(
183
185
  cls,
184
186
  type_: RuntimeErrorType,
185
- title: Optional[str],
186
- message: Optional[str],
187
- extras: List[str],
187
+ title: str | None,
188
+ message: str | None,
189
+ extras: list[str],
188
190
  exception: Exception,
189
- ) -> "SerializedError":
191
+ ) -> SerializedError:
190
192
  return cls(
191
193
  type=type_,
192
194
  title=title,
@@ -197,13 +199,13 @@ class SerializedError:
197
199
  )
198
200
 
199
201
  @classmethod
200
- def from_exception(cls, exception: Exception) -> "SerializedError":
202
+ def from_exception(cls, exception: Exception) -> SerializedError:
201
203
  import requests
202
204
  import hypothesis.errors
203
205
  from hypothesis import HealthCheck
204
206
 
205
207
  title = "Runtime Error"
206
- message: Optional[str]
208
+ message: str | None
207
209
  if isinstance(exception, requests.RequestException):
208
210
  if isinstance(exception, requests.exceptions.SSLError):
209
211
  type_ = RuntimeErrorType.CONNECTION_SSL
@@ -217,6 +219,13 @@ class SerializedError:
217
219
  type_ = RuntimeErrorType.HYPOTHESIS_DEADLINE_EXCEEDED
218
220
  message = str(exception).strip()
219
221
  extras = []
222
+ elif isinstance(exception, hypothesis.errors.InvalidArgument) and str(exception).startswith("Scalar "):
223
+ # Comes from `hypothesis-graphql`
224
+ scalar_name = _scalar_name_from_error(exception)
225
+ type_ = RuntimeErrorType.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR
226
+ message = f"Scalar type '{scalar_name}' is not recognized"
227
+ extras = []
228
+ title = "Unknown GraphQL Scalar"
220
229
  elif isinstance(exception, hypothesis.errors.Unsatisfiable):
221
230
  type_ = RuntimeErrorType.HYPOTHESIS_UNSATISFIABLE
222
231
  message = f"{exception}. Possible reasons:"
@@ -261,6 +270,15 @@ class SerializedError:
261
270
  message = exception.message
262
271
  extras = []
263
272
  title = "Schema Error"
273
+ elif isinstance(exception, SerializationError):
274
+ if isinstance(exception, UnboundPrefixError):
275
+ type_ = RuntimeErrorType.SERIALIZATION_UNBOUNDED_PREFIX
276
+ title = "XML serialization error"
277
+ else:
278
+ title = "Serialization not possible"
279
+ type_ = RuntimeErrorType.SERIALIZATION_NOT_POSSIBLE
280
+ message = str(exception)
281
+ extras = []
264
282
  else:
265
283
  type_ = RuntimeErrorType.UNCLASSIFIED
266
284
  message = str(exception)
@@ -290,7 +308,7 @@ Consider revising the schema to more accurately represent typical use cases
290
308
  or applying constraints to reduce the data size."""
291
309
 
292
310
 
293
- def _health_check_from_error(exception: hypothesis.errors.FailedHealthCheck) -> Optional[hypothesis.HealthCheck]:
311
+ def _health_check_from_error(exception: hypothesis.errors.FailedHealthCheck) -> hypothesis.HealthCheck | None:
294
312
  from hypothesis import HealthCheck
295
313
 
296
314
  match = re.search(r"add HealthCheck\.(\w+) to the suppress_health_check ", str(exception))
@@ -304,16 +322,23 @@ def _health_check_from_error(exception: hypothesis.errors.FailedHealthCheck) ->
304
322
  return None
305
323
 
306
324
 
325
+ def _scalar_name_from_error(exception: hypothesis.errors.InvalidArgument) -> str:
326
+ # This one is always available as the format is checked upfront
327
+ match = re.search(r"Scalar '(\w+)' is not supported", str(exception))
328
+ match = cast(re.Match, match)
329
+ return match.group(1)
330
+
331
+
307
332
  @dataclass
308
333
  class SerializedInteraction:
309
334
  request: Request
310
335
  response: Response
311
- checks: List[SerializedCheck]
336
+ checks: list[SerializedCheck]
312
337
  status: Status
313
338
  recorded_at: str
314
339
 
315
340
  @classmethod
316
- def from_interaction(cls, interaction: Interaction) -> "SerializedInteraction":
341
+ def from_interaction(cls, interaction: Interaction) -> SerializedInteraction:
317
342
  return cls(
318
343
  request=interaction.request,
319
344
  response=interaction.response,
@@ -334,15 +359,16 @@ class SerializedTestResult:
334
359
  is_errored: bool
335
360
  is_flaky: bool
336
361
  is_skipped: bool
337
- seed: Optional[int]
338
- data_generation_method: List[str]
339
- checks: List[SerializedCheck]
340
- logs: List[str]
341
- errors: List[SerializedError]
342
- interactions: List[SerializedInteraction]
362
+ skip_reason: str | None
363
+ seed: int | None
364
+ data_generation_method: list[str]
365
+ checks: list[SerializedCheck]
366
+ logs: list[str]
367
+ errors: list[SerializedError]
368
+ interactions: list[SerializedInteraction]
343
369
 
344
370
  @classmethod
345
- def from_test_result(cls, result: TestResult) -> "SerializedTestResult":
371
+ def from_test_result(cls, result: TestResult) -> SerializedTestResult:
346
372
  formatter = logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
347
373
  return cls(
348
374
  method=result.method,
@@ -354,6 +380,7 @@ class SerializedTestResult:
354
380
  is_errored=result.is_errored,
355
381
  is_flaky=result.is_flaky,
356
382
  is_skipped=result.is_skipped,
383
+ skip_reason=result.skip_reason,
357
384
  seed=result.seed,
358
385
  data_generation_method=[m.as_short_name() for m in result.data_generation_method],
359
386
  checks=[SerializedCheck.from_check(check) for check in result.checks],
@@ -363,9 +390,9 @@ class SerializedTestResult:
363
390
  )
364
391
 
365
392
 
366
- def deduplicate_failures(checks: List[SerializedCheck]) -> List[SerializedCheck]:
393
+ def deduplicate_failures(checks: list[SerializedCheck]) -> list[SerializedCheck]:
367
394
  """Return only unique checks that should be displayed in the output."""
368
- seen: Set[Tuple[Optional[str], ...]] = set()
395
+ seen: set[tuple[str | None, ...]] = set()
369
396
  unique_checks = []
370
397
  for check in reversed(checks):
371
398
  # There are also could be checks that didn't fail
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
  import threading
3
3
  from collections.abc import MutableMapping, MutableSequence
4
4
  from dataclasses import dataclass, replace
5
- from typing import TYPE_CHECKING, Any, FrozenSet, Optional, Union, cast
5
+ from typing import TYPE_CHECKING, Any, cast
6
6
  from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
7
7
 
8
8
  from .constants import NOT_SET
@@ -87,26 +87,26 @@ class Config:
87
87
  :param str replacement: The replacement string for sanitized values.
88
88
  """
89
89
 
90
- keys_to_sanitize: FrozenSet[str] = DEFAULT_KEYS_TO_SANITIZE
91
- sensitive_markers: FrozenSet[str] = DEFAULT_SENSITIVE_MARKERS
90
+ keys_to_sanitize: frozenset[str] = DEFAULT_KEYS_TO_SANITIZE
91
+ sensitive_markers: frozenset[str] = DEFAULT_SENSITIVE_MARKERS
92
92
  replacement: str = DEFAULT_REPLACEMENT
93
93
 
94
- def with_keys_to_sanitize(self, *keys: str) -> "Config":
94
+ def with_keys_to_sanitize(self, *keys: str) -> Config:
95
95
  """Create a new configuration with additional keys to sanitize."""
96
96
  new_keys_to_sanitize = self.keys_to_sanitize.union([key.lower() for key in keys])
97
97
  return replace(self, keys_to_sanitize=frozenset(new_keys_to_sanitize))
98
98
 
99
- def without_keys_to_sanitize(self, *keys: str) -> "Config":
99
+ def without_keys_to_sanitize(self, *keys: str) -> Config:
100
100
  """Create a new configuration without certain keys to sanitize."""
101
101
  new_keys_to_sanitize = self.keys_to_sanitize.difference([key.lower() for key in keys])
102
102
  return replace(self, keys_to_sanitize=frozenset(new_keys_to_sanitize))
103
103
 
104
- def with_sensitive_markers(self, *markers: str) -> "Config":
104
+ def with_sensitive_markers(self, *markers: str) -> Config:
105
105
  """Create a new configuration with additional sensitive markers."""
106
106
  new_sensitive_markers = self.sensitive_markers.union([key.lower() for key in markers])
107
107
  return replace(self, sensitive_markers=frozenset(new_sensitive_markers))
108
108
 
109
- def without_sensitive_markers(self, *markers: str) -> "Config":
109
+ def without_sensitive_markers(self, *markers: str) -> Config:
110
110
  """Create a new configuration without certain sensitive markers."""
111
111
  new_sensitive_markers = self.sensitive_markers.difference([key.lower() for key in markers])
112
112
  return replace(self, sensitive_markers=frozenset(new_sensitive_markers))
@@ -126,7 +126,7 @@ def configure(config: Config) -> None:
126
126
  _thread_local.default_sanitization_config = config
127
127
 
128
128
 
129
- def sanitize_value(item: Any, *, config: Optional[Config] = None) -> None:
129
+ def sanitize_value(item: Any, *, config: Config | None = None) -> None:
130
130
  """Sanitize sensitive values within a given item.
131
131
 
132
132
  This function is recursive and will sanitize sensitive data within nested
@@ -150,7 +150,7 @@ def sanitize_value(item: Any, *, config: Optional[Config] = None) -> None:
150
150
  sanitize_value(value, config=config)
151
151
 
152
152
 
153
- def sanitize_case(case: "Case", *, config: Optional[Config] = None) -> None:
153
+ def sanitize_case(case: Case, *, config: Config | None = None) -> None:
154
154
  """Sanitize sensitive values within a given case."""
155
155
  if case.path_parameters is not None:
156
156
  sanitize_value(case.path_parameters, config=config)
@@ -166,21 +166,21 @@ def sanitize_case(case: "Case", *, config: Optional[Config] = None) -> None:
166
166
  sanitize_history(case.source, config=config)
167
167
 
168
168
 
169
- def sanitize_history(source: "CaseSource", *, config: Optional[Config] = None) -> None:
169
+ def sanitize_history(source: CaseSource, *, config: Config | None = None) -> None:
170
170
  """Recursively sanitize history of case/response pairs."""
171
- current: Optional["CaseSource"] = source
171
+ current: CaseSource | None = source
172
172
  while current is not None:
173
173
  sanitize_case(current.case, config=config)
174
174
  sanitize_response(current.response, config=config)
175
175
  current = current.case.source
176
176
 
177
177
 
178
- def sanitize_response(response: "GenericResponse", *, config: Optional[Config] = None) -> None:
178
+ def sanitize_response(response: GenericResponse, *, config: Config | None = None) -> None:
179
179
  # Sanitize headers
180
180
  sanitize_value(response.headers, config=config)
181
181
 
182
182
 
183
- def sanitize_request(request: Union[PreparedRequest, "Request"], *, config: Optional[Config] = None) -> None:
183
+ def sanitize_request(request: PreparedRequest | Request, *, config: Config | None = None) -> None:
184
184
  from requests import PreparedRequest
185
185
 
186
186
  if isinstance(request, PreparedRequest) and request.url:
@@ -192,16 +192,14 @@ def sanitize_request(request: Union[PreparedRequest, "Request"], *, config: Opti
192
192
  sanitize_value(request.headers, config=config)
193
193
 
194
194
 
195
- def sanitize_output(
196
- case: "Case", response: Optional["GenericResponse"] = None, *, config: Optional[Config] = None
197
- ) -> None:
195
+ def sanitize_output(case: Case, response: GenericResponse | None = None, *, config: Config | None = None) -> None:
198
196
  sanitize_case(case, config=config)
199
197
  if response is not None:
200
198
  sanitize_response(response, config=config)
201
199
  sanitize_request(response.request, config=config)
202
200
 
203
201
 
204
- def sanitize_url(url: str, *, config: Optional[Config] = None) -> str:
202
+ def sanitize_url(url: str, *, config: Config | None = None) -> str:
205
203
  """Sanitize sensitive parts of a given URL.
206
204
 
207
205
  This function will sanitize the authority and query parameters in the URL.
@@ -226,7 +224,7 @@ def sanitize_url(url: str, *, config: Optional[Config] = None) -> str:
226
224
  return urlunsplit(sanitized_url_parts)
227
225
 
228
226
 
229
- def sanitize_serialized_check(check: "SerializedCheck", *, config: Optional[Config] = None) -> None:
227
+ def sanitize_serialized_check(check: SerializedCheck, *, config: Config | None = None) -> None:
230
228
  sanitize_request(check.request, config=config)
231
229
  response = check.response
232
230
  if response:
@@ -237,13 +235,13 @@ def sanitize_serialized_check(check: "SerializedCheck", *, config: Optional[Conf
237
235
  sanitize_value(entry.response.headers, config=config)
238
236
 
239
237
 
240
- def sanitize_serialized_case(case: "SerializedCase", *, config: Optional[Config] = None) -> None:
238
+ def sanitize_serialized_case(case: SerializedCase, *, config: Config | None = None) -> None:
241
239
  for value in (case.path_parameters, case.headers, case.cookies, case.query, case.extra_headers):
242
240
  if value is not None:
243
241
  sanitize_value(value, config=config)
244
242
 
245
243
 
246
- def sanitize_serialized_interaction(interaction: "SerializedInteraction", *, config: Optional[Config] = None) -> None:
244
+ def sanitize_serialized_interaction(interaction: SerializedInteraction, *, config: Config | None = None) -> None:
247
245
  sanitize_request(interaction.request, config=config)
248
246
  sanitize_value(interaction.response.headers, config=config)
249
247
  for check in interaction.checks: