schemathesis 3.29.1__py3-none-any.whl → 3.30.0__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 (123) hide show
  1. schemathesis/__init__.py +3 -3
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +1 -3
  4. schemathesis/_hypothesis.py +6 -0
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +1 -0
  7. schemathesis/_rate_limiter.py +2 -1
  8. schemathesis/_xml.py +1 -0
  9. schemathesis/auths.py +4 -2
  10. schemathesis/checks.py +8 -5
  11. schemathesis/cli/__init__.py +8 -1
  12. schemathesis/cli/callbacks.py +3 -4
  13. schemathesis/cli/cassettes.py +6 -4
  14. schemathesis/cli/constants.py +2 -0
  15. schemathesis/cli/context.py +3 -0
  16. schemathesis/cli/debug.py +2 -1
  17. schemathesis/cli/handlers.py +1 -1
  18. schemathesis/cli/options.py +1 -0
  19. schemathesis/cli/output/default.py +50 -22
  20. schemathesis/cli/output/short.py +21 -10
  21. schemathesis/cli/sanitization.py +1 -0
  22. schemathesis/code_samples.py +1 -0
  23. schemathesis/constants.py +1 -0
  24. schemathesis/contrib/openapi/__init__.py +1 -1
  25. schemathesis/contrib/openapi/fill_missing_examples.py +2 -0
  26. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  27. schemathesis/contrib/unique_data.py +2 -1
  28. schemathesis/exceptions.py +40 -26
  29. schemathesis/experimental/__init__.py +14 -0
  30. schemathesis/extra/_aiohttp.py +1 -0
  31. schemathesis/extra/_server.py +1 -0
  32. schemathesis/extra/pytest_plugin.py +13 -24
  33. schemathesis/failures.py +32 -3
  34. schemathesis/filters.py +2 -1
  35. schemathesis/fixups/__init__.py +1 -0
  36. schemathesis/fixups/fast_api.py +2 -2
  37. schemathesis/fixups/utf8_bom.py +1 -2
  38. schemathesis/generation/__init__.py +2 -1
  39. schemathesis/hooks.py +3 -1
  40. schemathesis/internal/copy.py +19 -3
  41. schemathesis/internal/deprecation.py +1 -1
  42. schemathesis/internal/jsonschema.py +2 -1
  43. schemathesis/internal/result.py +1 -1
  44. schemathesis/internal/transformation.py +1 -0
  45. schemathesis/lazy.py +3 -2
  46. schemathesis/loaders.py +4 -2
  47. schemathesis/models.py +20 -5
  48. schemathesis/parameters.py +1 -0
  49. schemathesis/runner/__init__.py +1 -1
  50. schemathesis/runner/events.py +21 -4
  51. schemathesis/runner/impl/core.py +61 -33
  52. schemathesis/runner/impl/solo.py +2 -1
  53. schemathesis/runner/impl/threadpool.py +4 -0
  54. schemathesis/runner/probes.py +1 -1
  55. schemathesis/runner/serialization.py +1 -1
  56. schemathesis/sanitization.py +2 -0
  57. schemathesis/schemas.py +1 -4
  58. schemathesis/service/ci.py +1 -0
  59. schemathesis/service/client.py +7 -7
  60. schemathesis/service/events.py +2 -1
  61. schemathesis/service/extensions.py +5 -5
  62. schemathesis/service/hosts.py +1 -0
  63. schemathesis/service/metadata.py +2 -1
  64. schemathesis/service/models.py +2 -1
  65. schemathesis/service/report.py +3 -3
  66. schemathesis/service/serialization.py +54 -23
  67. schemathesis/service/usage.py +1 -0
  68. schemathesis/specs/graphql/_cache.py +1 -1
  69. schemathesis/specs/graphql/loaders.py +1 -1
  70. schemathesis/specs/graphql/nodes.py +1 -0
  71. schemathesis/specs/graphql/scalars.py +2 -2
  72. schemathesis/specs/graphql/schemas.py +7 -7
  73. schemathesis/specs/graphql/validation.py +1 -2
  74. schemathesis/specs/openapi/_hypothesis.py +17 -11
  75. schemathesis/specs/openapi/checks.py +102 -9
  76. schemathesis/specs/openapi/converter.py +2 -1
  77. schemathesis/specs/openapi/definitions.py +2 -1
  78. schemathesis/specs/openapi/examples.py +7 -9
  79. schemathesis/specs/openapi/expressions/__init__.py +29 -2
  80. schemathesis/specs/openapi/expressions/context.py +1 -1
  81. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  82. schemathesis/specs/openapi/expressions/lexer.py +19 -18
  83. schemathesis/specs/openapi/expressions/nodes.py +24 -4
  84. schemathesis/specs/openapi/expressions/parser.py +26 -5
  85. schemathesis/specs/openapi/filters.py +1 -0
  86. schemathesis/specs/openapi/links.py +35 -7
  87. schemathesis/specs/openapi/loaders.py +13 -11
  88. schemathesis/specs/openapi/negative/__init__.py +2 -1
  89. schemathesis/specs/openapi/negative/mutations.py +1 -0
  90. schemathesis/specs/openapi/parameters.py +1 -0
  91. schemathesis/specs/openapi/schemas.py +27 -38
  92. schemathesis/specs/openapi/security.py +1 -0
  93. schemathesis/specs/openapi/serialization.py +1 -0
  94. schemathesis/specs/openapi/stateful/__init__.py +159 -70
  95. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  96. schemathesis/specs/openapi/stateful/types.py +13 -0
  97. schemathesis/specs/openapi/utils.py +1 -0
  98. schemathesis/specs/openapi/validation.py +1 -0
  99. schemathesis/stateful/__init__.py +4 -2
  100. schemathesis/stateful/config.py +66 -0
  101. schemathesis/stateful/context.py +93 -0
  102. schemathesis/stateful/events.py +209 -0
  103. schemathesis/stateful/runner.py +233 -0
  104. schemathesis/stateful/sink.py +68 -0
  105. schemathesis/stateful/state_machine.py +39 -22
  106. schemathesis/stateful/statistic.py +20 -0
  107. schemathesis/stateful/validation.py +66 -0
  108. schemathesis/targets.py +1 -0
  109. schemathesis/throttling.py +23 -3
  110. schemathesis/transports/__init__.py +28 -10
  111. schemathesis/transports/auth.py +1 -0
  112. schemathesis/transports/content_types.py +1 -1
  113. schemathesis/transports/headers.py +2 -1
  114. schemathesis/transports/responses.py +6 -4
  115. schemathesis/types.py +1 -0
  116. schemathesis/utils.py +1 -0
  117. {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/METADATA +1 -1
  118. schemathesis-3.30.0.dist-info/RECORD +150 -0
  119. schemathesis/specs/openapi/stateful/links.py +0 -94
  120. schemathesis-3.29.1.dist-info/RECORD +0 -141
  121. {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/WHEEL +0 -0
  122. {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/entry_points.txt +0 -0
  123. {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  import enum
3
4
  import json
4
5
  import re
@@ -14,11 +15,13 @@ from .failures import FailureContext
14
15
 
15
16
  if TYPE_CHECKING:
16
17
  import hypothesis.errors
17
- from jsonschema import RefResolutionError, ValidationError, SchemaError as JsonSchemaError
18
- from .transports.responses import GenericResponse
19
18
  from graphql.error import GraphQLFormattedError
19
+ from jsonschema import RefResolutionError, ValidationError
20
+ from jsonschema import SchemaError as JsonSchemaError
20
21
  from requests import RequestException
21
22
 
23
+ from .transports.responses import GenericResponse
24
+
22
25
 
23
26
  class CheckFailed(AssertionError):
24
27
  """Custom error type to distinguish from arbitrary AssertionError that may happen in the dependent libraries."""
@@ -97,52 +100,60 @@ def get_grouped_exception(prefix: str, *exceptions: AssertionError) -> type[Chec
97
100
  return _get_hashed_exception("GroupedException", f"{prefix}{message}")
98
101
 
99
102
 
100
- def get_server_error(status_code: int) -> type[CheckFailed]:
103
+ def get_server_error(prefix: str, status_code: int) -> type[CheckFailed]:
101
104
  """Return new exception for the Internal Server Error cases."""
102
- name = f"ServerError{status_code}"
105
+ name = f"ServerError{prefix}{status_code}"
103
106
  return get_exception(name)
104
107
 
105
108
 
106
- def get_status_code_error(status_code: int) -> type[CheckFailed]:
109
+ def get_status_code_error(prefix: str, status_code: int) -> type[CheckFailed]:
107
110
  """Return new exception for an unexpected status code."""
108
- name = f"StatusCodeError{status_code}"
111
+ name = f"StatusCodeError{prefix}{status_code}"
109
112
  return get_exception(name)
110
113
 
111
114
 
112
- def get_response_type_error(expected: str, received: str) -> type[CheckFailed]:
115
+ def get_response_type_error(prefix: str, expected: str, received: str) -> type[CheckFailed]:
113
116
  """Return new exception for an unexpected response type."""
114
- name = f"SchemaValidationError{expected}_{received}"
117
+ name = f"SchemaValidationError{prefix}{expected}_{received}"
115
118
  return get_exception(name)
116
119
 
117
120
 
118
- def get_malformed_media_type_error(media_type: str) -> type[CheckFailed]:
119
- name = f"MalformedMediaType{media_type}"
121
+ def get_malformed_media_type_error(prefix: str, media_type: str) -> type[CheckFailed]:
122
+ name = f"MalformedMediaType{prefix}{media_type}"
120
123
  return get_exception(name)
121
124
 
122
125
 
123
- def get_missing_content_type_error() -> type[CheckFailed]:
126
+ def get_missing_content_type_error(prefix: str) -> type[CheckFailed]:
124
127
  """Return new exception for a missing Content-Type header."""
125
- return get_exception("MissingContentTypeError")
128
+ return get_exception(f"MissingContentTypeError{prefix}")
126
129
 
127
130
 
128
- def get_schema_validation_error(exception: ValidationError) -> type[CheckFailed]:
131
+ def get_schema_validation_error(prefix: str, exception: ValidationError) -> type[CheckFailed]:
129
132
  """Return new exception for schema validation error."""
130
- return _get_hashed_exception("SchemaValidationError", str(exception))
133
+ return _get_hashed_exception(f"SchemaValidationError{prefix}", str(exception))
131
134
 
132
135
 
133
- def get_response_parsing_error(exception: JSONDecodeError) -> type[CheckFailed]:
136
+ def get_response_parsing_error(prefix: str, exception: JSONDecodeError) -> type[CheckFailed]:
134
137
  """Return new exception for response parsing error."""
135
- return _get_hashed_exception("ResponseParsingError", str(exception))
138
+ return _get_hashed_exception(f"ResponseParsingError{prefix}", str(exception))
136
139
 
137
140
 
138
- def get_headers_error(message: str) -> type[CheckFailed]:
141
+ def get_headers_error(prefix: str, message: str) -> type[CheckFailed]:
139
142
  """Return new exception for missing headers."""
140
- return _get_hashed_exception("MissingHeadersError", message)
143
+ return _get_hashed_exception(f"MissingHeadersError{prefix}", message)
144
+
145
+
146
+ def get_negative_rejection_error(prefix: str, status: int) -> type[CheckFailed]:
147
+ return _get_hashed_exception(f"AcceptedNegativeDataError{prefix}", str(status))
148
+
149
+
150
+ def get_use_after_free_error(free: str) -> type[CheckFailed]:
151
+ return _get_hashed_exception("UseAfterFreeError", free)
141
152
 
142
153
 
143
- def get_timeout_error(deadline: float | int) -> type[CheckFailed]:
154
+ def get_timeout_error(prefix: str, deadline: float | int) -> type[CheckFailed]:
144
155
  """Request took too long."""
145
- return _get_hashed_exception("TimeoutError", str(deadline))
156
+ return _get_hashed_exception(f"TimeoutError{prefix}", str(deadline))
146
157
 
147
158
 
148
159
  def get_unexpected_graphql_response_error(type_: type) -> type[CheckFailed]:
@@ -158,7 +169,7 @@ def get_grouped_graphql_error(errors: list[GraphQLFormattedError]) -> type[Check
158
169
  if "locations" in error:
159
170
  message += ";locations:"
160
171
  for location in sorted(error["locations"]):
161
- message += f"({location['line'],location['column']})"
172
+ message += f"({location['line'], location['column']})"
162
173
  if "path" in error:
163
174
  message += ";path:"
164
175
  for chunk in error["path"]:
@@ -541,7 +552,7 @@ def remove_ssl_line_number(text: str) -> str:
541
552
 
542
553
 
543
554
  def extract_requests_exception_details(exc: RequestException) -> tuple[str, list[str]]:
544
- from requests.exceptions import SSLError, ConnectionError, ChunkedEncodingError
555
+ from requests.exceptions import ChunkedEncodingError, ConnectionError, SSLError
545
556
  from urllib3.exceptions import MaxRetryError
546
557
 
547
558
  if isinstance(exc, SSLError):
@@ -552,11 +563,14 @@ def extract_requests_exception_details(exc: RequestException) -> tuple[str, list
552
563
  message = "Connection failed"
553
564
  inner = exc.args[0]
554
565
  if isinstance(inner, MaxRetryError) and inner.reason is not None:
555
- arg = str(inner.reason.args[0])
556
- if ":" not in arg:
557
- reason = arg
566
+ arg = inner.reason.args[0]
567
+ if isinstance(arg, str):
568
+ if ":" not in arg:
569
+ reason = arg
570
+ else:
571
+ _, reason = arg.split(":", maxsplit=1)
558
572
  else:
559
- _, reason = arg.split(":", maxsplit=1)
573
+ reason = f"Max retries exceeded with url: {inner.url}"
560
574
  extra = [reason.strip()]
561
575
  else:
562
576
  extra = [" ".join(map(str, inner.args))]
@@ -79,3 +79,17 @@ SCHEMA_ANALYSIS = GLOBAL_EXPERIMENTS.create_experiment(
79
79
  description="Analyzing API schemas via Schemathesis.io",
80
80
  discussion_url="https://github.com/schemathesis/schemathesis/discussions/2056",
81
81
  )
82
+ STATEFUL_TEST_RUNNER = GLOBAL_EXPERIMENTS.create_experiment(
83
+ name="stateful-test-runner",
84
+ verbose_name="New Stateful Test Runner",
85
+ env_var="STATEFUL_TEST_RUNNER",
86
+ description="State machine-based runner for stateful tests in CLI",
87
+ discussion_url="https://github.com/schemathesis/schemathesis/discussions/2262",
88
+ )
89
+ STATEFUL_ONLY = GLOBAL_EXPERIMENTS.create_experiment(
90
+ name="stateful-only",
91
+ verbose_name="Stateful Only",
92
+ env_var="STATEFUL_ONLY",
93
+ description="Run only stateful tests",
94
+ discussion_url="https://github.com/schemathesis/schemathesis/discussions/2262",
95
+ )
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  import asyncio
3
4
 
4
5
  from aiohttp import web
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  import threading
3
4
  from time import sleep
4
5
  from typing import Any, Callable
@@ -3,19 +3,18 @@ from __future__ import annotations
3
3
  import unittest
4
4
  from contextlib import contextmanager
5
5
  from functools import partial
6
- from typing import Any, Callable, Generator, Type, TypeVar, cast
6
+ from typing import Any, Callable, Generator, Type, cast
7
7
 
8
8
  import pytest
9
9
  from _pytest import fixtures, nodes
10
10
  from _pytest.config import hookimpl
11
11
  from _pytest.fixtures import FuncFixtureInfo
12
- from _pytest.nodes import Node
13
12
  from _pytest.python import Class, Function, FunctionDefinition, Metafunc, Module, PyCollector
14
13
  from hypothesis import reporting
15
14
  from hypothesis.errors import InvalidArgument, Unsatisfiable
16
15
  from jsonschema.exceptions import SchemaError
17
16
 
18
- from .._dependency_versions import IS_PYTEST_ABOVE_7, IS_PYTEST_ABOVE_8, IS_PYTEST_ABOVE_54
17
+ from .._dependency_versions import IS_PYTEST_ABOVE_7, IS_PYTEST_ABOVE_8
19
18
  from .._override import get_override_from_mark
20
19
  from ..constants import (
21
20
  GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE,
@@ -43,14 +42,6 @@ from ..utils import (
43
42
  validate_given_args,
44
43
  )
45
44
 
46
- T = TypeVar("T", bound=Node)
47
-
48
-
49
- def create(cls: type[T], *args: Any, **kwargs: Any) -> T:
50
- if IS_PYTEST_ABOVE_54:
51
- return cls.from_parent(*args, **kwargs) # type: ignore
52
- return cls(*args, **kwargs)
53
-
54
45
 
55
46
  class SchemathesisFunction(Function):
56
47
  def __init__(
@@ -155,7 +146,9 @@ class SchemathesisCase(PyCollector):
155
146
  name += f"[{error.full_path}]"
156
147
 
157
148
  cls = self._get_class_parent()
158
- definition: FunctionDefinition = create(FunctionDefinition, name=self.name, parent=self.parent, callobj=funcobj)
149
+ definition: FunctionDefinition = FunctionDefinition.from_parent(
150
+ name=self.name, parent=self.parent, callobj=funcobj
151
+ )
159
152
  fixturemanager = self.session._fixturemanager
160
153
  fixtureinfo = fixturemanager.getfixtureinfo(definition, funcobj, cls)
161
154
 
@@ -166,8 +159,7 @@ class SchemathesisCase(PyCollector):
166
159
  funcobj = partial(funcobj, self.parent.obj)
167
160
 
168
161
  if not metafunc._calls:
169
- yield create(
170
- SchemathesisFunction,
162
+ yield SchemathesisFunction.from_parent(
171
163
  name=name,
172
164
  parent=self.parent,
173
165
  callobj=funcobj,
@@ -181,10 +173,9 @@ class SchemathesisCase(PyCollector):
181
173
  fixtureinfo.prune_dependency_tree()
182
174
  for callspec in metafunc._calls:
183
175
  subname = f"{name}[{callspec.id}]"
184
- yield create(
185
- SchemathesisFunction,
176
+ yield SchemathesisFunction.from_parent(
177
+ self.parent,
186
178
  name=subname,
187
- parent=self.parent,
188
179
  callspec=callspec,
189
180
  callobj=funcobj,
190
181
  fixtureinfo=fixtureinfo,
@@ -236,7 +227,7 @@ def pytest_pycollect_makeitem(collector: nodes.Collector, name: str, obj: Any) -
236
227
  """Switch to a different collector if the test is parametrized marked by schemathesis."""
237
228
  outcome = yield
238
229
  if is_schemathesis_test(obj):
239
- outcome.force_result(create(SchemathesisCase, parent=collector, test_function=obj, name=name))
230
+ outcome.force_result(SchemathesisCase.from_parent(collector, test_function=obj, name=name))
240
231
  else:
241
232
  outcome.get_result()
242
233
 
@@ -261,7 +252,7 @@ def skip_unnecessary_hypothesis_output() -> Generator:
261
252
  yield
262
253
 
263
254
 
264
- @hookimpl(hookwrapper=True)
255
+ @hookimpl(wrapper=True)
265
256
  def pytest_pyfunc_call(pyfuncitem): # type:ignore
266
257
  """It is possible to have a Hypothesis exception in runtime.
267
258
 
@@ -278,10 +269,9 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
278
269
 
279
270
  __tracebackhide__ = True
280
271
  if isinstance(pyfuncitem, SchemathesisFunction):
281
- with skip_unnecessary_hypothesis_output():
282
- outcome = yield
283
272
  try:
284
- outcome.get_result()
273
+ with skip_unnecessary_hypothesis_output():
274
+ yield
285
275
  except InvalidArgument as exc:
286
276
  if "Inconsistent args" in str(exc) and "@example()" in str(exc):
287
277
  raise UsageError(GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE) from None
@@ -316,5 +306,4 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
316
306
  if invalid_headers is not None:
317
307
  raise InvalidHeadersExample.from_headers(invalid_headers) from None
318
308
  else:
319
- outcome = yield
320
- outcome.get_result()
309
+ yield
schemathesis/failures.py CHANGED
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
+
2
3
  import textwrap
3
4
  from dataclasses import dataclass
4
5
  from json import JSONDecodeError
5
- from typing import Any, TYPE_CHECKING
6
+ from typing import TYPE_CHECKING, Any
6
7
 
7
8
  if TYPE_CHECKING:
8
9
  from graphql.error import GraphQLFormattedError
@@ -46,11 +47,19 @@ class ValidationErrorContext(FailureContext):
46
47
 
47
48
  schema = textwrap.indent(truncated_json(exc.schema, max_lines=20), prefix=" ")
48
49
  value = textwrap.indent(truncated_json(exc.instance, max_lines=20), prefix=" ")
49
- message = f"{exc.message}\n\nSchema:\n\n{schema}\n\nValue:\n\n{value}"
50
+ schema_path = list(exc.absolute_schema_path)
51
+ if len(schema_path) > 1:
52
+ # Exclude the last segment, which is already in the schema
53
+ schema_title = "Schema at "
54
+ for segment in schema_path[:-1]:
55
+ schema_title += f"/{segment}"
56
+ else:
57
+ schema_title = "Schema"
58
+ message = f"{exc.message}\n\n{schema_title}:\n\n{schema}\n\nValue:\n\n{value}"
50
59
  return cls(
51
60
  message=message,
52
61
  validation_message=exc.message,
53
- schema_path=list(exc.absolute_schema_path),
62
+ schema_path=schema_path,
54
63
  schema=exc.schema,
55
64
  instance_path=list(exc.absolute_path),
56
65
  instance=exc.instance,
@@ -117,6 +126,26 @@ class UndefinedContentType(FailureContext):
117
126
  type: str = "undefined_content_type"
118
127
 
119
128
 
129
+ @dataclass(repr=False)
130
+ class AcceptedNegativeData(FailureContext):
131
+ """Response with negative data was accepted."""
132
+
133
+ message: str
134
+ title: str = "Accepted negative data"
135
+ type: str = "accepted_negative_data"
136
+
137
+
138
+ @dataclass(repr=False)
139
+ class UseAfterFree(FailureContext):
140
+ """Resource was used after a successful DELETE operation on it."""
141
+
142
+ message: str
143
+ free: str
144
+ usage: str
145
+ title: str = "Use after free"
146
+ type: str = "use_after_free"
147
+
148
+
120
149
  @dataclass(repr=False)
121
150
  class UndefinedStatusCode(FailureContext):
122
151
  """Response has a status code that is not defined in the schema."""
schemathesis/filters.py CHANGED
@@ -1,11 +1,12 @@
1
1
  """Filtering system that allows users to filter API operations based on certain criteria."""
2
2
 
3
3
  from __future__ import annotations
4
+
4
5
  import re
5
6
  from dataclasses import dataclass, field
6
7
  from functools import partial
7
8
  from types import SimpleNamespace
8
- from typing import TYPE_CHECKING, Callable, List, Union, Protocol
9
+ from typing import TYPE_CHECKING, Callable, List, Protocol, Union
9
10
 
10
11
  from .exceptions import UsageError
11
12
 
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import Iterable
3
4
 
4
5
  from . import fast_api, utf8_bom
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import Any
3
4
 
4
- from ..hooks import HookContext
5
+ from ..hooks import HookContext, register, unregister
5
6
  from ..hooks import is_installed as global_is_installed
6
- from ..hooks import register, unregister
7
7
  from ..internal.jsonschema import traverse_schema
8
8
 
9
9
 
@@ -1,9 +1,8 @@
1
1
  from typing import TYPE_CHECKING
2
2
 
3
3
  from ..constants import BOM_MARK
4
- from ..hooks import HookContext
4
+ from ..hooks import HookContext, register, unregister
5
5
  from ..hooks import is_installed as global_is_installed
6
- from ..hooks import register, unregister
7
6
 
8
7
  if TYPE_CHECKING:
9
8
  from ..models import Case
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
+
2
3
  import random
3
4
  from dataclasses import dataclass, field
4
5
  from enum import Enum
5
- from typing import Union, Iterable, TYPE_CHECKING
6
+ from typing import TYPE_CHECKING, Iterable, Union
6
7
 
7
8
  if TYPE_CHECKING:
8
9
  from hypothesis.strategies import SearchStrategy
schemathesis/hooks.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  import inspect
3
4
  from collections import defaultdict
4
5
  from copy import deepcopy
@@ -7,11 +8,12 @@ from enum import Enum, unique
7
8
  from functools import partial
8
9
  from typing import TYPE_CHECKING, Any, Callable, ClassVar, DefaultDict, cast
9
10
 
10
- from .types import GenericTest
11
11
  from .internal.deprecation import deprecated_property
12
+ from .types import GenericTest
12
13
 
13
14
  if TYPE_CHECKING:
14
15
  from hypothesis import strategies as st
16
+
15
17
  from .models import APIOperation, Case
16
18
  from .schemas import BaseSchema
17
19
  from .transports.responses import GenericResponse
@@ -2,12 +2,28 @@ from typing import Any
2
2
 
3
3
 
4
4
  def fast_deepcopy(value: Any) -> Any:
5
- """A specialized version of `deepcopy` that copies only `dict` and `list`.
5
+ """A specialized version of `deepcopy` that copies only `dict` and `list` and does unrolling.
6
6
 
7
7
  It is on average 3x faster than `deepcopy` and given the amount of calls, it is an important optimization.
8
8
  """
9
9
  if isinstance(value, dict):
10
- return {key: fast_deepcopy(v) for key, v in value.items()}
10
+ return {
11
+ k1: (
12
+ {k2: fast_deepcopy(v2) for k2, v2 in v1.items()}
13
+ if isinstance(v1, dict)
14
+ else [fast_deepcopy(v2) for v2 in v1]
15
+ if isinstance(v1, list)
16
+ else v1
17
+ )
18
+ for k1, v1 in value.items()
19
+ }
11
20
  if isinstance(value, list):
12
- return [fast_deepcopy(v) for v in value]
21
+ return [
22
+ {k2: fast_deepcopy(v2) for k2, v2 in v1.items()}
23
+ if isinstance(v1, dict)
24
+ else [fast_deepcopy(v2) for v2 in v1]
25
+ if isinstance(v1, list)
26
+ else v1
27
+ for v1 in value
28
+ ]
13
29
  return value
@@ -1,5 +1,5 @@
1
1
  import warnings
2
- from typing import Callable, Any
2
+ from typing import Any, Callable
3
3
 
4
4
 
5
5
  def _warn_deprecation(*, kind: str, thing: str, removed_in: str, replacement: str) -> None:
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
- from typing import overload, Dict, Union, Any, List, Callable
2
+
3
+ from typing import Any, Callable, Dict, List, Union, overload
3
4
 
4
5
  JsonValue = Union[Dict[str, Any], List, str, float, int]
5
6
 
@@ -1,4 +1,4 @@
1
- from typing import TypeVar, Generic, Union
1
+ from typing import Generic, TypeVar, Union
2
2
 
3
3
  T = TypeVar("T")
4
4
  E = TypeVar("E", bound=Exception)
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import Any
3
4
 
4
5
 
schemathesis/lazy.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from dataclasses import dataclass, field
3
4
  from inspect import signature
4
5
  from typing import Any, Callable, Generator
@@ -13,12 +14,12 @@ from pyrate_limiter import Limiter
13
14
  from pytest_subtests import SubTests, nullcontext
14
15
 
15
16
  from ._compat import MultipleFailures, get_interesting_origin
16
- from ._override import check_no_override_mark, CaseOverride, set_override_mark, get_override_from_mark
17
+ from ._override import CaseOverride, check_no_override_mark, get_override_from_mark, set_override_mark
17
18
  from .auths import AuthStorage
18
19
  from .code_samples import CodeSampleStyle
19
20
  from .constants import FLAKY_FAILURE_MESSAGE, NOT_SET
20
- from .generation import DataGenerationMethodInput, GenerationConfig
21
21
  from .exceptions import CheckFailed, OperationSchemaError, SkipTest, get_grouped_exception
22
+ from .generation import DataGenerationMethodInput, GenerationConfig
22
23
  from .hooks import HookDispatcher, HookScope
23
24
  from .internal.result import Ok
24
25
  from .models import APIOperation
schemathesis/loaders.py CHANGED
@@ -1,15 +1,17 @@
1
1
  from __future__ import annotations
2
+
2
3
  import re
3
4
  import sys
4
5
  from functools import lru_cache
5
- from typing import Callable, TypeVar, TYPE_CHECKING, TextIO, Any, BinaryIO
6
+ from typing import TYPE_CHECKING, Any, BinaryIO, Callable, TextIO, TypeVar
6
7
 
7
8
  from .exceptions import SchemaError, SchemaErrorType, extract_requests_exception_details
8
9
 
9
10
  if TYPE_CHECKING:
10
- from .transports.responses import GenericResponse
11
11
  import yaml
12
12
 
13
+ from .transports.responses import GenericResponse
14
+
13
15
  R = TypeVar("R", bound="GenericResponse")
14
16
 
15
17
 
schemathesis/models.py CHANGED
@@ -42,6 +42,7 @@ from .exceptions import (
42
42
  OperationSchemaError,
43
43
  SerializationNotPossible,
44
44
  SkipTest,
45
+ UsageError,
45
46
  deduplicate_failed_checks,
46
47
  get_grouped_exception,
47
48
  maybe_set_assertion_message,
@@ -54,7 +55,7 @@ from .internal.deprecation import deprecated_function, deprecated_property
54
55
  from .parameters import Parameter, ParameterSet, PayloadAlternatives
55
56
  from .sanitization import sanitize_request, sanitize_response
56
57
  from .serializers import Serializer
57
- from .transports import ASGITransport, RequestsTransport, WSGITransport, serialize_payload, deserialize_payload
58
+ from .transports import ASGITransport, RequestsTransport, WSGITransport, deserialize_payload, serialize_payload
58
59
  from .types import Body, Cookies, FormData, Headers, NotSet, PathParameters, Query
59
60
 
60
61
  if TYPE_CHECKING:
@@ -271,13 +272,14 @@ class Case:
271
272
  final_headers.setdefault(SCHEMATHESIS_TEST_CASE_HEADER, self.id)
272
273
  return final_headers
273
274
 
274
- def _get_serializer(self) -> Serializer | None:
275
+ def _get_serializer(self, media_type: str | None = None) -> Serializer | None:
275
276
  """Get a serializer for the payload, if there is any."""
276
- if self.media_type is not None:
277
- media_type = serializers.get_first_matching_media_type(self.media_type)
277
+ input_media_type = media_type or self.media_type
278
+ if input_media_type is not None:
279
+ media_type = serializers.get_first_matching_media_type(input_media_type)
278
280
  if media_type is None:
279
281
  # This media type is set manually. Otherwise, it should have been rejected during the data generation
280
- raise SerializationNotPossible.for_media_type(self.media_type)
282
+ raise SerializationNotPossible.for_media_type(input_media_type)
281
283
  # SAFETY: It is safe to assume that serializer will be found, because `media_type` returned above
282
284
  # is registered. This intentionally ignores cases with concurrent serializers registry modification.
283
285
  cls = cast(Type[serializers.Serializer], serializers.get(media_type))
@@ -694,6 +696,19 @@ class APIOperation(Generic[P, C]):
694
696
  def get_request_payload_content_types(self) -> list[str]:
695
697
  return self.schema.get_request_payload_content_types(self)
696
698
 
699
+ def _get_default_media_type(self) -> str:
700
+ # If the user wants to send payload, then there should be a media type, otherwise the payload is ignored
701
+ media_types = self.get_request_payload_content_types()
702
+ if len(media_types) == 1:
703
+ # The only available option
704
+ return media_types[0]
705
+ media_types_repr = ", ".join(media_types)
706
+ raise UsageError(
707
+ "Can not detect appropriate media type. "
708
+ "You can either specify one of the defined media types "
709
+ f"or pass any other media type available for serialization. Defined media types: {media_types_repr}"
710
+ )
711
+
697
712
  def partial_deepcopy(self) -> APIOperation:
698
713
  return self.__class__(
699
714
  path=self.path, # string, immutable
@@ -4,6 +4,7 @@ These are basic entities that describe what data could be sent to the API.
4
4
  """
5
5
 
6
6
  from __future__ import annotations
7
+
7
8
  from dataclasses import dataclass, field
8
9
  from typing import TYPE_CHECKING, Any, Generator, Generic, TypeVar
9
10
 
@@ -28,10 +28,10 @@ if TYPE_CHECKING:
28
28
 
29
29
  from ..models import CheckFunction
30
30
  from ..schemas import BaseSchema
31
+ from ..service.client import ServiceClient
31
32
  from ..stateful import Stateful
32
33
  from . import events
33
34
  from .impl import BaseRunner
34
- from ..service.client import ServiceClient
35
35
 
36
36
 
37
37
  @deprecated_function(removed_in="4.0", replacement="schemathesis.runner.from_schema")
@@ -1,21 +1,22 @@
1
1
  from __future__ import annotations
2
+
2
3
  import enum
3
4
  import threading
4
5
  import time
5
6
  from dataclasses import asdict, dataclass, field
6
- from typing import Any, TYPE_CHECKING
7
+ from typing import TYPE_CHECKING, Any
7
8
 
9
+ from ..exceptions import RuntimeErrorType, SchemaError, SchemaErrorType, format_exception
10
+ from ..generation import DataGenerationMethod
8
11
  from ..internal.datetime import current_datetime
9
12
  from ..internal.result import Result
10
- from ..generation import DataGenerationMethod
11
- from ..exceptions import SchemaError, SchemaErrorType, format_exception, RuntimeErrorType
12
13
  from .serialization import SerializedError, SerializedTestResult
13
14
 
14
-
15
15
  if TYPE_CHECKING:
16
16
  from ..models import APIOperation, Status, TestResult, TestResultSet
17
17
  from ..schemas import BaseSchema
18
18
  from ..service.models import AnalysisResult
19
+ from ..stateful import events
19
20
  from . import probes
20
21
 
21
22
 
@@ -287,6 +288,22 @@ class InternalError(ExecutionEvent):
287
288
  )
288
289
 
289
290
 
291
+ @dataclass
292
+ class StatefulEvent(ExecutionEvent):
293
+ """Represents an event originating from the state machine runner."""
294
+
295
+ data: events.StatefulEvent
296
+
297
+ __slots__ = ("data",)
298
+
299
+
300
+ @dataclass
301
+ class AfterStatefulExecution(ExecutionEvent):
302
+ """Happens after the stateful test run."""
303
+
304
+ result: SerializedTestResult
305
+
306
+
290
307
  @dataclass
291
308
  class Finished(ExecutionEvent):
292
309
  """The final event of the run.