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
@@ -1,6 +1,7 @@
1
1
  import click
2
2
 
3
3
  from ...runner import events
4
+ from ...stateful import events as stateful_events
4
5
  from ..context import ExecutionContext
5
6
  from ..handlers import EventHandler
6
7
  from . import default
@@ -15,7 +16,13 @@ def handle_after_execution(context: ExecutionContext, event: events.AfterExecuti
15
16
  context.operations_processed += 1
16
17
  context.results.append(event.result)
17
18
  context.hypothesis_output.extend(event.hypothesis_output)
18
- default.display_execution_result(context, event)
19
+ default.display_execution_result(context, event.status.value)
20
+
21
+
22
+ def handle_stateful_event(context: ExecutionContext, event: events.StatefulEvent) -> None:
23
+ if isinstance(event.data, stateful_events.RunStarted):
24
+ click.echo()
25
+ default.handle_stateful_event(context, event)
19
26
 
20
27
 
21
28
  class ShortOutputStyleHandler(EventHandler):
@@ -26,23 +33,27 @@ class ShortOutputStyleHandler(EventHandler):
26
33
  """
27
34
  if isinstance(event, events.Initialized):
28
35
  default.handle_initialized(context, event)
29
- if isinstance(event, events.BeforeProbing):
36
+ elif isinstance(event, events.BeforeProbing):
30
37
  default.handle_before_probing(context, event)
31
- if isinstance(event, events.AfterProbing):
38
+ elif isinstance(event, events.AfterProbing):
32
39
  default.handle_after_probing(context, event)
33
- if isinstance(event, events.BeforeAnalysis):
40
+ elif isinstance(event, events.BeforeAnalysis):
34
41
  default.handle_before_analysis(context, event)
35
- if isinstance(event, events.AfterAnalysis):
42
+ elif isinstance(event, events.AfterAnalysis):
36
43
  default.handle_after_analysis(context, event)
37
- if isinstance(event, events.BeforeExecution):
44
+ elif isinstance(event, events.BeforeExecution):
38
45
  handle_before_execution(context, event)
39
- if isinstance(event, events.AfterExecution):
46
+ elif isinstance(event, events.AfterExecution):
40
47
  handle_after_execution(context, event)
41
- if isinstance(event, events.Finished):
48
+ elif isinstance(event, events.Finished):
42
49
  if context.operations_count == context.operations_processed:
43
50
  click.echo()
44
51
  default.handle_finished(context, event)
45
- if isinstance(event, events.Interrupted):
52
+ elif isinstance(event, events.Interrupted):
46
53
  default.handle_interrupted(context, event)
47
- if isinstance(event, events.InternalError):
54
+ elif isinstance(event, events.InternalError):
48
55
  default.handle_internal_error(context, event)
56
+ elif isinstance(event, events.StatefulEvent):
57
+ handle_stateful_event(context, event)
58
+ elif isinstance(event, events.AfterStatefulExecution):
59
+ default.handle_after_stateful_execution(context, event)
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from dataclasses import dataclass
3
4
  from typing import TYPE_CHECKING
4
5
 
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from enum import Enum
3
4
  from functools import lru_cache
4
5
  from shlex import quote
schemathesis/constants.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from importlib import metadata
2
+
2
3
  from .types import NotSet
3
4
 
4
5
  try:
@@ -1,4 +1,4 @@
1
- from . import formats, fill_missing_examples
1
+ from . import fill_missing_examples, formats
2
2
 
3
3
 
4
4
  def install() -> None:
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
4
+
3
5
  from ...hooks import HookContext, register, unregister
4
6
 
5
7
  if TYPE_CHECKING:
@@ -3,9 +3,10 @@ FORMAT_NAME = "uuid"
3
3
 
4
4
 
5
5
  def install() -> None:
6
- from ....specs import openapi
7
6
  from hypothesis import strategies as st
8
7
 
8
+ from ....specs import openapi
9
+
9
10
  openapi.format(FORMAT_NAME, st.uuids().map(str))
10
11
 
11
12
 
@@ -6,9 +6,10 @@ from typing import TYPE_CHECKING
6
6
  from ..hooks import HookContext, register, unregister
7
7
 
8
8
  if TYPE_CHECKING:
9
- from ..models import Case
10
9
  from hypothesis import strategies as st
11
10
 
11
+ from ..models import Case
12
+
12
13
 
13
14
  def install() -> None:
14
15
  warnings.warn(
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
+
2
3
  import enum
3
- import json
4
4
  import re
5
5
  import traceback
6
6
  from dataclasses import dataclass, field
@@ -11,14 +11,17 @@ from typing import TYPE_CHECKING, Any, Callable, Generator, NoReturn
11
11
 
12
12
  from .constants import SERIALIZERS_SUGGESTION_MESSAGE
13
13
  from .failures import FailureContext
14
+ from .internal.output import truncate_json
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
+
141
145
 
146
+ def get_negative_rejection_error(prefix: str, status: int) -> type[CheckFailed]:
147
+ return _get_hashed_exception(f"AcceptedNegativeDataError{prefix}", str(status))
142
148
 
143
- def get_timeout_error(deadline: float | int) -> type[CheckFailed]:
149
+
150
+ def get_use_after_free_error(free: str) -> type[CheckFailed]:
151
+ return _get_hashed_exception("UseAfterFreeError", free)
152
+
153
+
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"]:
@@ -196,7 +207,7 @@ class OperationSchemaError(Exception):
196
207
  message = "Invalid schema definition"
197
208
  error_path = " -> ".join(str(entry) for entry in error.path) or "[root]"
198
209
  message += f"\n\nLocation:\n {error_path}"
199
- instance = truncated_json(error.instance)
210
+ instance = truncate_json(error.instance)
200
211
  message += f"\n\nProblematic definition:\n{instance}"
201
212
  message += "\n\nError details:\n "
202
213
  # This default message contains the instance which we already printed
@@ -289,39 +300,6 @@ class InvalidHeadersExample(OperationSchemaError):
289
300
  return cls(message)
290
301
 
291
302
 
292
- def truncated_json(data: Any, max_lines: int = 10, max_width: int = 80) -> str:
293
- # Convert JSON to string with indentation
294
- indent = 4
295
- serialized = json.dumps(data, indent=indent)
296
-
297
- # Split string by lines
298
-
299
- lines = [line[: max_width - 3] + "..." if len(line) > max_width else line for line in serialized.split("\n")]
300
-
301
- if len(lines) <= max_lines:
302
- return "\n".join(lines)
303
-
304
- truncated_lines = lines[: max_lines - 1]
305
- indentation = " " * indent
306
- truncated_lines.append(f"{indentation}// Output truncated...")
307
- truncated_lines.append(lines[-1])
308
-
309
- return "\n".join(truncated_lines)
310
-
311
-
312
- MAX_PAYLOAD_SIZE = 512
313
-
314
-
315
- def prepare_response_payload(payload: str, max_size: int = MAX_PAYLOAD_SIZE) -> str:
316
- if payload.endswith("\r\n"):
317
- payload = payload[:-2]
318
- elif payload.endswith("\n"):
319
- payload = payload[:-1]
320
- if len(payload) > max_size:
321
- payload = payload[:max_size] + " // Output truncated..."
322
- return payload
323
-
324
-
325
303
  class DeadlineExceeded(Exception):
326
304
  """Test took too long to run."""
327
305
 
@@ -541,7 +519,7 @@ def remove_ssl_line_number(text: str) -> str:
541
519
 
542
520
 
543
521
  def extract_requests_exception_details(exc: RequestException) -> tuple[str, list[str]]:
544
- from requests.exceptions import SSLError, ConnectionError, ChunkedEncodingError
522
+ from requests.exceptions import ChunkedEncodingError, ConnectionError, SSLError
545
523
  from urllib3.exceptions import MaxRetryError
546
524
 
547
525
  if isinstance(exc, SSLError):
@@ -552,11 +530,14 @@ def extract_requests_exception_details(exc: RequestException) -> tuple[str, list
552
530
  message = "Connection failed"
553
531
  inner = exc.args[0]
554
532
  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
533
+ arg = inner.reason.args[0]
534
+ if isinstance(arg, str):
535
+ if ":" not in arg:
536
+ reason = arg
537
+ else:
538
+ _, reason = arg.split(":", maxsplit=1)
558
539
  else:
559
- _, reason = arg.split(":", maxsplit=1)
540
+ reason = f"Max retries exceeded with url: {inner.url}"
560
541
  extra = [reason.strip()]
561
542
  else:
562
543
  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,11 @@
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
7
+
8
+ from schemathesis.internal.output import OutputConfig
6
9
 
7
10
  if TYPE_CHECKING:
8
11
  from graphql.error import GraphQLFormattedError
@@ -41,16 +44,27 @@ class ValidationErrorContext(FailureContext):
41
44
  return ("/".join(map(str, self.schema_path)),)
42
45
 
43
46
  @classmethod
44
- def from_exception(cls, exc: ValidationError) -> ValidationErrorContext:
45
- from .exceptions import truncated_json
46
-
47
- schema = textwrap.indent(truncated_json(exc.schema, max_lines=20), prefix=" ")
48
- 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}"
47
+ def from_exception(
48
+ cls, exc: ValidationError, *, output_config: OutputConfig | None = None
49
+ ) -> ValidationErrorContext:
50
+ from .internal.output import truncate_json
51
+
52
+ output_config = OutputConfig.from_parent(output_config, max_lines=20)
53
+ schema = textwrap.indent(truncate_json(exc.schema, config=output_config), prefix=" ")
54
+ value = textwrap.indent(truncate_json(exc.instance, config=output_config), prefix=" ")
55
+ schema_path = list(exc.absolute_schema_path)
56
+ if len(schema_path) > 1:
57
+ # Exclude the last segment, which is already in the schema
58
+ schema_title = "Schema at "
59
+ for segment in schema_path[:-1]:
60
+ schema_title += f"/{segment}"
61
+ else:
62
+ schema_title = "Schema"
63
+ message = f"{exc.message}\n\n{schema_title}:\n\n{schema}\n\nValue:\n\n{value}"
50
64
  return cls(
51
65
  message=message,
52
66
  validation_message=exc.message,
53
- schema_path=list(exc.absolute_schema_path),
67
+ schema_path=schema_path,
54
68
  schema=exc.schema,
55
69
  instance_path=list(exc.absolute_path),
56
70
  instance=exc.instance,
@@ -117,6 +131,26 @@ class UndefinedContentType(FailureContext):
117
131
  type: str = "undefined_content_type"
118
132
 
119
133
 
134
+ @dataclass(repr=False)
135
+ class AcceptedNegativeData(FailureContext):
136
+ """Response with negative data was accepted."""
137
+
138
+ message: str
139
+ title: str = "Accepted negative data"
140
+ type: str = "accepted_negative_data"
141
+
142
+
143
+ @dataclass(repr=False)
144
+ class UseAfterFree(FailureContext):
145
+ """Resource was used after a successful DELETE operation on it."""
146
+
147
+ message: str
148
+ free: str
149
+ usage: str
150
+ title: str = "Use after free"
151
+ type: str = "use_after_free"
152
+
153
+
120
154
  @dataclass(repr=False)
121
155
  class UndefinedStatusCode(FailureContext):
122
156
  """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