schemathesis 3.29.2__py3-none-any.whl → 3.30.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 (125) 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 +28 -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 +5 -0
  16. schemathesis/cli/debug.py +2 -1
  17. schemathesis/cli/handlers.py +1 -1
  18. schemathesis/cli/junitxml.py +5 -4
  19. schemathesis/cli/options.py +1 -0
  20. schemathesis/cli/output/default.py +56 -24
  21. schemathesis/cli/output/short.py +21 -10
  22. schemathesis/cli/sanitization.py +1 -0
  23. schemathesis/code_samples.py +1 -0
  24. schemathesis/constants.py +1 -0
  25. schemathesis/contrib/openapi/__init__.py +1 -1
  26. schemathesis/contrib/openapi/fill_missing_examples.py +2 -0
  27. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  28. schemathesis/contrib/unique_data.py +2 -1
  29. schemathesis/exceptions.py +42 -61
  30. schemathesis/experimental/__init__.py +14 -0
  31. schemathesis/extra/_aiohttp.py +1 -0
  32. schemathesis/extra/_server.py +1 -0
  33. schemathesis/extra/pytest_plugin.py +13 -24
  34. schemathesis/failures.py +42 -8
  35. schemathesis/filters.py +2 -1
  36. schemathesis/fixups/__init__.py +1 -0
  37. schemathesis/fixups/fast_api.py +2 -2
  38. schemathesis/fixups/utf8_bom.py +1 -2
  39. schemathesis/generation/__init__.py +2 -1
  40. schemathesis/hooks.py +3 -1
  41. schemathesis/internal/copy.py +19 -3
  42. schemathesis/internal/deprecation.py +1 -1
  43. schemathesis/internal/jsonschema.py +2 -1
  44. schemathesis/internal/output.py +68 -0
  45. schemathesis/internal/result.py +1 -1
  46. schemathesis/internal/transformation.py +1 -0
  47. schemathesis/lazy.py +11 -2
  48. schemathesis/loaders.py +4 -2
  49. schemathesis/models.py +22 -7
  50. schemathesis/parameters.py +1 -0
  51. schemathesis/runner/__init__.py +1 -1
  52. schemathesis/runner/events.py +22 -4
  53. schemathesis/runner/impl/core.py +69 -33
  54. schemathesis/runner/impl/solo.py +2 -1
  55. schemathesis/runner/impl/threadpool.py +4 -0
  56. schemathesis/runner/probes.py +1 -1
  57. schemathesis/runner/serialization.py +1 -1
  58. schemathesis/sanitization.py +2 -0
  59. schemathesis/schemas.py +7 -4
  60. schemathesis/service/ci.py +1 -0
  61. schemathesis/service/client.py +7 -7
  62. schemathesis/service/events.py +2 -1
  63. schemathesis/service/extensions.py +5 -5
  64. schemathesis/service/hosts.py +1 -0
  65. schemathesis/service/metadata.py +2 -1
  66. schemathesis/service/models.py +2 -1
  67. schemathesis/service/report.py +3 -3
  68. schemathesis/service/serialization.py +62 -23
  69. schemathesis/service/usage.py +1 -0
  70. schemathesis/specs/graphql/_cache.py +1 -1
  71. schemathesis/specs/graphql/loaders.py +17 -1
  72. schemathesis/specs/graphql/nodes.py +1 -0
  73. schemathesis/specs/graphql/scalars.py +2 -2
  74. schemathesis/specs/graphql/schemas.py +7 -7
  75. schemathesis/specs/graphql/validation.py +1 -2
  76. schemathesis/specs/openapi/_hypothesis.py +17 -11
  77. schemathesis/specs/openapi/checks.py +102 -9
  78. schemathesis/specs/openapi/converter.py +2 -1
  79. schemathesis/specs/openapi/definitions.py +2 -1
  80. schemathesis/specs/openapi/examples.py +7 -9
  81. schemathesis/specs/openapi/expressions/__init__.py +29 -2
  82. schemathesis/specs/openapi/expressions/context.py +1 -1
  83. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  84. schemathesis/specs/openapi/expressions/lexer.py +19 -18
  85. schemathesis/specs/openapi/expressions/nodes.py +24 -4
  86. schemathesis/specs/openapi/expressions/parser.py +26 -5
  87. schemathesis/specs/openapi/filters.py +1 -0
  88. schemathesis/specs/openapi/links.py +35 -7
  89. schemathesis/specs/openapi/loaders.py +31 -11
  90. schemathesis/specs/openapi/negative/__init__.py +2 -1
  91. schemathesis/specs/openapi/negative/mutations.py +1 -0
  92. schemathesis/specs/openapi/parameters.py +1 -0
  93. schemathesis/specs/openapi/schemas.py +28 -39
  94. schemathesis/specs/openapi/security.py +1 -0
  95. schemathesis/specs/openapi/serialization.py +1 -0
  96. schemathesis/specs/openapi/stateful/__init__.py +159 -70
  97. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  98. schemathesis/specs/openapi/stateful/types.py +13 -0
  99. schemathesis/specs/openapi/utils.py +1 -0
  100. schemathesis/specs/openapi/validation.py +1 -0
  101. schemathesis/stateful/__init__.py +4 -2
  102. schemathesis/stateful/config.py +66 -0
  103. schemathesis/stateful/context.py +103 -0
  104. schemathesis/stateful/events.py +215 -0
  105. schemathesis/stateful/runner.py +238 -0
  106. schemathesis/stateful/sink.py +68 -0
  107. schemathesis/stateful/state_machine.py +39 -22
  108. schemathesis/stateful/statistic.py +20 -0
  109. schemathesis/stateful/validation.py +66 -0
  110. schemathesis/targets.py +1 -0
  111. schemathesis/throttling.py +23 -3
  112. schemathesis/transports/__init__.py +28 -10
  113. schemathesis/transports/auth.py +1 -0
  114. schemathesis/transports/content_types.py +1 -1
  115. schemathesis/transports/headers.py +2 -1
  116. schemathesis/transports/responses.py +6 -4
  117. schemathesis/types.py +1 -0
  118. schemathesis/utils.py +1 -0
  119. {schemathesis-3.29.2.dist-info → schemathesis-3.30.1.dist-info}/METADATA +3 -3
  120. schemathesis-3.30.1.dist-info/RECORD +151 -0
  121. schemathesis/specs/openapi/stateful/links.py +0 -92
  122. schemathesis-3.29.2.dist-info/RECORD +0 -141
  123. {schemathesis-3.29.2.dist-info → schemathesis-3.30.1.dist-info}/WHEEL +0 -0
  124. {schemathesis-3.29.2.dist-info → schemathesis-3.30.1.dist-info}/entry_points.txt +0 -0
  125. {schemathesis-3.29.2.dist-info → schemathesis-3.30.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass, replace
5
+ from typing import Any
6
+
7
+ TRUNCATED = "// Output truncated..."
8
+ MAX_PAYLOAD_SIZE = 512
9
+ MAX_LINES = 10
10
+ MAX_WIDTH = 80
11
+
12
+
13
+ @dataclass
14
+ class OutputConfig:
15
+ """Options for configuring various aspects of Schemathesis output."""
16
+
17
+ truncate: bool = True
18
+ max_payload_size: int = MAX_PAYLOAD_SIZE
19
+ max_lines: int = MAX_LINES
20
+ max_width: int = MAX_WIDTH
21
+
22
+ @classmethod
23
+ def from_parent(cls, parent: OutputConfig | None = None, **changes: Any) -> OutputConfig:
24
+ parent = parent or OutputConfig()
25
+ return parent.replace(**changes)
26
+
27
+ def replace(self, **changes: Any) -> OutputConfig:
28
+ """Create a new instance with updated values."""
29
+ return replace(self, **changes)
30
+
31
+
32
+ def truncate_json(data: Any, *, config: OutputConfig | None = None) -> str:
33
+ config = config or OutputConfig()
34
+ # Convert JSON to string with indentation
35
+ indent = 4
36
+ serialized = json.dumps(data, indent=indent)
37
+ if not config.truncate:
38
+ return serialized
39
+
40
+ # Split string by lines
41
+
42
+ lines = [
43
+ line[: config.max_width - 3] + "..." if len(line) > config.max_width else line
44
+ for line in serialized.split("\n")
45
+ ]
46
+
47
+ if len(lines) <= config.max_lines:
48
+ return "\n".join(lines)
49
+
50
+ truncated_lines = lines[: config.max_lines - 1]
51
+ indentation = " " * indent
52
+ truncated_lines.append(f"{indentation}{TRUNCATED}")
53
+ truncated_lines.append(lines[-1])
54
+
55
+ return "\n".join(truncated_lines)
56
+
57
+
58
+ def prepare_response_payload(payload: str, *, config: OutputConfig | None = None) -> str:
59
+ if payload.endswith("\r\n"):
60
+ payload = payload[:-2]
61
+ elif payload.endswith("\n"):
62
+ payload = payload[:-1]
63
+ config = config or OutputConfig()
64
+ if not config.truncate:
65
+ return payload
66
+ if len(payload) > config.max_payload_size:
67
+ payload = payload[: config.max_payload_size] + f" {TRUNCATED}"
68
+ return payload
@@ -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,13 +14,14 @@ 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
24
+ from .internal.output import OutputConfig
23
25
  from .internal.result import Ok
24
26
  from .models import APIOperation
25
27
  from .schemas import BaseSchema
@@ -51,6 +53,7 @@ class LazySchema:
51
53
  skip_deprecated_operations: bool = False
52
54
  data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET
53
55
  generation_config: GenerationConfig | NotSet = NOT_SET
56
+ output_config: OutputConfig | NotSet = NOT_SET
54
57
  code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
55
58
  rate_limiter: Limiter | None = None
56
59
  sanitize_output: bool = True
@@ -68,6 +71,7 @@ class LazySchema:
68
71
  skip_deprecated_operations: bool | NotSet = NOT_SET,
69
72
  data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
70
73
  generation_config: GenerationConfig | NotSet = NOT_SET,
74
+ output_config: OutputConfig | NotSet = NOT_SET,
71
75
  code_sample_style: str | NotSet = NOT_SET,
72
76
  ) -> Callable:
73
77
  if method is NOT_SET:
@@ -82,6 +86,8 @@ class LazySchema:
82
86
  data_generation_methods = self.data_generation_methods
83
87
  if generation_config is NOT_SET:
84
88
  generation_config = self.generation_config
89
+ if output_config is NOT_SET:
90
+ output_config = self.output_config
85
91
  if isinstance(code_sample_style, str):
86
92
  _code_sample_style = CodeSampleStyle.from_str(code_sample_style)
87
93
  else:
@@ -121,6 +127,7 @@ class LazySchema:
121
127
  skip_deprecated_operations=skip_deprecated_operations,
122
128
  data_generation_methods=data_generation_methods,
123
129
  generation_config=generation_config,
130
+ output_config=output_config,
124
131
  code_sample_style=_code_sample_style,
125
132
  app=self.app,
126
133
  rate_limiter=self.rate_limiter,
@@ -325,6 +332,7 @@ def get_schema(
325
332
  skip_deprecated_operations: bool | NotSet = NOT_SET,
326
333
  data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
327
334
  generation_config: GenerationConfig | NotSet = NOT_SET,
335
+ output_config: OutputConfig | NotSet = NOT_SET,
328
336
  code_sample_style: CodeSampleStyle,
329
337
  rate_limiter: Limiter | None,
330
338
  sanitize_output: bool,
@@ -347,6 +355,7 @@ def get_schema(
347
355
  skip_deprecated_operations=skip_deprecated_operations,
348
356
  data_generation_methods=data_generation_methods,
349
357
  generation_config=generation_config,
358
+ output_config=output_config,
350
359
  code_sample_style=code_sample_style,
351
360
  rate_limiter=rate_limiter,
352
361
  sanitize_output=sanitize_output,
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,19 +42,20 @@ 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,
48
- prepare_response_payload,
49
49
  )
50
50
  from .generation import DataGenerationMethod, GenerationConfig, generate_random_case_id
51
51
  from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, dispatch
52
52
  from .internal.copy import fast_deepcopy
53
53
  from .internal.deprecation import deprecated_function, deprecated_property
54
+ from .internal.output import prepare_response_payload
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))
@@ -418,7 +420,7 @@ class Case:
418
420
  if not payload:
419
421
  formatted += "\n\n <EMPTY>"
420
422
  else:
421
- payload = prepare_response_payload(payload)
423
+ payload = prepare_response_payload(payload, config=self.operation.schema.output_config)
422
424
  payload = textwrap.indent(f"\n`{payload}`", prefix=" ")
423
425
  formatted += f"\n{payload}"
424
426
  code_sample_style = (
@@ -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,23 @@ 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
+ status: Status
305
+ result: SerializedTestResult
306
+
307
+
290
308
  @dataclass
291
309
  class Finished(ExecutionEvent):
292
310
  """The final event of the run.
@@ -21,9 +21,7 @@ from jsonschema.exceptions import SchemaError as JsonSchemaError
21
21
  from jsonschema.exceptions import ValidationError
22
22
  from requests.auth import HTTPDigestAuth, _basic_auth_str
23
23
 
24
- from schemathesis.transports import RequestsTransport
25
-
26
- from ... import failures, hooks
24
+ from ... import experimental, failures, hooks
27
25
  from ..._compat import MultipleFailures
28
26
  from ..._hypothesis import (
29
27
  get_invalid_example_headers_mark,
@@ -65,7 +63,10 @@ from ...service import extensions
65
63
  from ...service.models import AnalysisResult, AnalysisSuccess
66
64
  from ...specs.openapi import formats
67
65
  from ...stateful import Feedback, Stateful
66
+ from ...stateful import events as stateful_events
67
+ from ...stateful import runner as stateful_runner
68
68
  from ...targets import Target, TargetContext
69
+ from ...transports import RequestsTransport, prepare_timeout
69
70
  from ...types import RawAuth, RequestCert
70
71
  from ...utils import capture_hypothesis_output
71
72
  from .. import probes
@@ -191,7 +192,9 @@ class BaseRunner:
191
192
  return
192
193
 
193
194
  try:
194
- yield from self._execute(results, stop_event)
195
+ if not experimental.STATEFUL_ONLY.is_enabled:
196
+ yield from self._execute(results, stop_event)
197
+ yield from self._run_stateful_tests(results)
195
198
  except KeyboardInterrupt:
196
199
  yield events.Interrupted()
197
200
 
@@ -211,6 +214,40 @@ class BaseRunner:
211
214
  ) -> Generator[events.ExecutionEvent, None, None]:
212
215
  raise NotImplementedError
213
216
 
217
+ def _run_stateful_tests(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
218
+ # Run new-style stateful tests
219
+ if self.stateful is not None and experimental.STATEFUL_TEST_RUNNER.is_enabled and self.schema.links_count > 0:
220
+ result = TestResult(
221
+ method="",
222
+ path="",
223
+ verbose_name="Stateful tests",
224
+ data_generation_method=self.schema.data_generation_methods,
225
+ )
226
+ config = stateful_runner.StatefulTestRunnerConfig(
227
+ checks=tuple(self.checks),
228
+ headers=self.headers or {},
229
+ hypothesis_settings=self.hypothesis_settings,
230
+ exit_first=self.exit_first,
231
+ request_timeout=self.request_timeout,
232
+ )
233
+ state_machine = self.schema.as_state_machine()
234
+ runner = state_machine.runner(config=config)
235
+ status = Status.success
236
+ for stateful_event in runner.execute():
237
+ if isinstance(stateful_event, stateful_events.SuiteFinished):
238
+ if stateful_event.failures and status != Status.error:
239
+ status = Status.failure
240
+ for failure in stateful_event.failures:
241
+ result.checks.append(failure)
242
+ elif isinstance(stateful_event, stateful_events.Errored):
243
+ status = Status.error
244
+ yield events.StatefulEvent(data=stateful_event)
245
+ results.append(result)
246
+ yield events.AfterStatefulExecution(
247
+ status=status,
248
+ result=SerializedTestResult.from_test_result(result),
249
+ )
250
+
214
251
  def _run_tests(
215
252
  self,
216
253
  maker: Callable,
@@ -246,7 +283,10 @@ class BaseRunner:
246
283
  ):
247
284
  if isinstance(result, Ok):
248
285
  operation, test = result.ok()
249
- feedback = Feedback(self.stateful, operation)
286
+ if self.stateful is not None and not experimental.STATEFUL_TEST_RUNNER.is_enabled:
287
+ feedback = Feedback(self.stateful, operation)
288
+ else:
289
+ feedback = None
250
290
  # Track whether `BeforeExecution` was already emitted.
251
291
  # Schema error may happen before / after `BeforeExecution`, but it should be emitted only once
252
292
  # and the `AfterExecution` event should have the same correlation id as previous `BeforeExecution`
@@ -268,17 +308,18 @@ class BaseRunner:
268
308
  if isinstance(event, events.Interrupted):
269
309
  return
270
310
  # Additional tests, generated via the `feedback` instance
271
- yield from self._run_tests(
272
- feedback.get_stateful_tests,
273
- template,
274
- settings=settings,
275
- generation_config=generation_config,
276
- seed=seed,
277
- recursion_level=recursion_level + 1,
278
- results=results,
279
- headers=headers,
280
- **kwargs,
281
- )
311
+ if feedback is not None:
312
+ yield from self._run_tests(
313
+ feedback.get_stateful_tests,
314
+ template,
315
+ settings=settings,
316
+ generation_config=generation_config,
317
+ seed=seed,
318
+ recursion_level=recursion_level + 1,
319
+ results=results,
320
+ headers=headers,
321
+ **kwargs,
322
+ )
282
323
  except OperationSchemaError as exc:
283
324
  yield from handle_schema_error(
284
325
  exc,
@@ -788,7 +829,7 @@ def network_test(
788
829
  request_cert: RequestCert | None,
789
830
  store_interactions: bool,
790
831
  headers: dict[str, Any] | None,
791
- feedback: Feedback,
832
+ feedback: Feedback | None,
792
833
  max_response_time: int | None,
793
834
  data_generation_methods: list[DataGenerationMethod],
794
835
  dry_run: bool,
@@ -830,7 +871,7 @@ def _network_test(
830
871
  timeout: float | None,
831
872
  store_interactions: bool,
832
873
  headers: dict[str, Any] | None,
833
- feedback: Feedback,
874
+ feedback: Feedback | None,
834
875
  request_tls_verify: bool,
835
876
  request_proxy: str | None,
836
877
  request_cert: RequestCert | None,
@@ -877,7 +918,8 @@ def _network_test(
877
918
  status = Status.failure
878
919
  raise
879
920
  finally:
880
- feedback.add_test_case(case, response)
921
+ if feedback is not None:
922
+ feedback.add_test_case(case, response)
881
923
  if store_interactions:
882
924
  result.store_requests_response(case, response, status, check_results)
883
925
  return response
@@ -891,14 +933,6 @@ def get_session(auth: HTTPDigestAuth | RawAuth | None = None) -> Generator[reque
891
933
  yield session
892
934
 
893
935
 
894
- def prepare_timeout(timeout: int | None) -> float | None:
895
- """Request timeout is in milliseconds, but `requests` uses seconds."""
896
- output: int | float | None = timeout
897
- if timeout is not None:
898
- output = timeout / 1000
899
- return output
900
-
901
-
902
936
  def wsgi_test(
903
937
  case: Case,
904
938
  checks: Iterable[CheckFunction],
@@ -908,7 +942,7 @@ def wsgi_test(
908
942
  auth_type: str | None,
909
943
  headers: dict[str, Any] | None,
910
944
  store_interactions: bool,
911
- feedback: Feedback,
945
+ feedback: Feedback | None,
912
946
  max_response_time: int | None,
913
947
  data_generation_methods: list[DataGenerationMethod],
914
948
  dry_run: bool,
@@ -939,7 +973,7 @@ def _wsgi_test(
939
973
  result: TestResult,
940
974
  headers: dict[str, Any],
941
975
  store_interactions: bool,
942
- feedback: Feedback,
976
+ feedback: Feedback | None,
943
977
  max_response_time: int | None,
944
978
  ) -> WSGIResponse:
945
979
  from ...transports.responses import WSGIResponse
@@ -968,7 +1002,8 @@ def _wsgi_test(
968
1002
  status = Status.failure
969
1003
  raise
970
1004
  finally:
971
- feedback.add_test_case(case, response)
1005
+ if feedback is not None:
1006
+ feedback.add_test_case(case, response)
972
1007
  if store_interactions:
973
1008
  result.store_wsgi_response(case, response, headers, response.elapsed.total_seconds(), status, check_results)
974
1009
  return response
@@ -1001,7 +1036,7 @@ def asgi_test(
1001
1036
  result: TestResult,
1002
1037
  store_interactions: bool,
1003
1038
  headers: dict[str, Any] | None,
1004
- feedback: Feedback,
1039
+ feedback: Feedback | None,
1005
1040
  max_response_time: int | None,
1006
1041
  data_generation_methods: list[DataGenerationMethod],
1007
1042
  dry_run: bool,
@@ -1034,7 +1069,7 @@ def _asgi_test(
1034
1069
  result: TestResult,
1035
1070
  store_interactions: bool,
1036
1071
  headers: dict[str, Any] | None,
1037
- feedback: Feedback,
1072
+ feedback: Feedback | None,
1038
1073
  max_response_time: int | None,
1039
1074
  ) -> requests.Response:
1040
1075
  hook_context = HookContext(operation=case.operation)
@@ -1059,7 +1094,8 @@ def _asgi_test(
1059
1094
  status = Status.failure
1060
1095
  raise
1061
1096
  finally:
1062
- feedback.add_test_case(case, response)
1097
+ if feedback is not None:
1098
+ feedback.add_test_case(case, response)
1063
1099
  if store_interactions:
1064
1100
  result.store_requests_response(case, response, status, check_results)
1065
1101
  return response
@@ -1,11 +1,12 @@
1
1
  from __future__ import annotations
2
+
2
3
  import threading
3
4
  from dataclasses import dataclass
4
5
  from typing import Generator
5
6
 
6
7
  from ...models import TestResultSet
7
- from ...types import RequestCert
8
8
  from ...transports.auth import get_requests_auth
9
+ from ...types import RequestCert
9
10
  from .. import events
10
11
  from .core import BaseRunner, asgi_test, get_session, network_test, wsgi_test
11
12
 
@@ -1,13 +1,16 @@
1
1
  from __future__ import annotations
2
+
2
3
  import ctypes
3
4
  import queue
4
5
  import threading
5
6
  import time
7
+ import warnings
6
8
  from dataclasses import dataclass
7
9
  from queue import Queue
8
10
  from typing import Any, Callable, Generator, Iterable, cast
9
11
 
10
12
  import hypothesis
13
+ from hypothesis.errors import HypothesisWarning
11
14
 
12
15
  from ..._hypothesis import create_test
13
16
  from ...generation import DataGenerationMethod, GenerationConfig
@@ -39,6 +42,7 @@ def _run_task(
39
42
  headers: dict[str, Any] | None = None,
40
43
  **kwargs: Any,
41
44
  ) -> None:
45
+ warnings.filterwarnings("ignore", message="The recursion limit will not be reset", category=HypothesisWarning)
42
46
  as_strategy_kwargs = {}
43
47
  if headers is not None:
44
48
  as_strategy_kwargs["headers"] = {key: value for key, value in headers.items() if key.lower() != "user-agent"}
@@ -22,8 +22,8 @@ from ..transports.auth import get_requests_auth
22
22
  if TYPE_CHECKING:
23
23
  import requests
24
24
 
25
- from ..types import RequestCert
26
25
  from ..schemas import BaseSchema
26
+ from ..types import RequestCert
27
27
 
28
28
 
29
29
  HEADER_NAME = "X-Schemathesis-Probe"
@@ -28,7 +28,7 @@ from ..exceptions import (
28
28
  make_unique_by_key,
29
29
  )
30
30
  from ..models import Case, Check, Interaction, Request, Response, Status, TestResult
31
- from ..transports import serialize_payload, deserialize_payload
31
+ from ..transports import deserialize_payload, serialize_payload
32
32
 
33
33
  if TYPE_CHECKING:
34
34
  import hypothesis.errors
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  import threading
3
4
  from collections.abc import MutableMapping, MutableSequence
4
5
  from dataclasses import dataclass, replace
@@ -9,6 +10,7 @@ from .constants import NOT_SET
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from requests import PreparedRequest
13
+
12
14
  from .models import Case, CaseSource, Request
13
15
  from .runner.serialization import SerializedCase, SerializedCheck, SerializedInteraction
14
16
  from .transports.responses import GenericResponse
schemathesis/schemas.py CHANGED
@@ -44,6 +44,7 @@ from .generation import (
44
44
  GenerationConfig,
45
45
  )
46
46
  from .hooks import HookContext, HookDispatcher, HookScope, dispatch
47
+ from .internal.output import OutputConfig
47
48
  from .internal.result import Ok, Result
48
49
  from .models import APIOperation, Case
49
50
  from .stateful import Stateful, StatefulTest
@@ -94,6 +95,7 @@ class BaseSchema(Mapping):
94
95
  default_factory=lambda: list(DEFAULT_DATA_GENERATION_METHODS)
95
96
  )
96
97
  generation_config: GenerationConfig = field(default_factory=GenerationConfig)
98
+ output_config: OutputConfig = field(default_factory=OutputConfig)
97
99
  code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
98
100
  rate_limiter: Limiter | None = None
99
101
  sanitize_output: bool = True
@@ -286,6 +288,7 @@ class BaseSchema(Mapping):
286
288
  skip_deprecated_operations: bool | NotSet = NOT_SET,
287
289
  data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
288
290
  generation_config: GenerationConfig | NotSet = NOT_SET,
291
+ output_config: OutputConfig | NotSet = NOT_SET,
289
292
  code_sample_style: CodeSampleStyle | NotSet = NOT_SET,
290
293
  rate_limiter: Limiter | None = NOT_SET,
291
294
  sanitize_output: bool | NotSet | None = NOT_SET,
@@ -314,6 +317,8 @@ class BaseSchema(Mapping):
314
317
  data_generation_methods = self.data_generation_methods
315
318
  if generation_config is NOT_SET:
316
319
  generation_config = self.generation_config
320
+ if output_config is NOT_SET:
321
+ output_config = self.output_config
317
322
  if code_sample_style is NOT_SET:
318
323
  code_sample_style = self.code_sample_style
319
324
  if rate_limiter is NOT_SET:
@@ -337,6 +342,7 @@ class BaseSchema(Mapping):
337
342
  skip_deprecated_operations=skip_deprecated_operations, # type: ignore
338
343
  data_generation_methods=data_generation_methods, # type: ignore
339
344
  generation_config=generation_config, # type: ignore
345
+ output_config=output_config, # type: ignore
340
346
  code_sample_style=code_sample_style, # type: ignore
341
347
  rate_limiter=rate_limiter, # type: ignore
342
348
  sanitize_output=sanitize_output, # type: ignore
@@ -397,10 +403,7 @@ class BaseSchema(Mapping):
397
403
  raise NotImplementedError
398
404
 
399
405
  def as_state_machine(self) -> type[APIStateMachine]:
400
- """Create a state machine class.
401
-
402
- Use it for stateful testing.
403
- """
406
+ """Create a state machine class."""
404
407
  raise NotImplementedError
405
408
 
406
409
  def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  import enum
3
4
  import os
4
5
  from dataclasses import asdict, dataclass