schemathesis 3.25.5__py3-none-any.whl → 3.39.7__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 (146) hide show
  1. schemathesis/__init__.py +6 -6
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +4 -2
  4. schemathesis/_hypothesis.py +369 -56
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +5 -4
  7. schemathesis/_patches.py +21 -0
  8. schemathesis/_rate_limiter.py +7 -0
  9. schemathesis/_xml.py +75 -22
  10. schemathesis/auths.py +78 -16
  11. schemathesis/checks.py +21 -9
  12. schemathesis/cli/__init__.py +793 -448
  13. schemathesis/cli/__main__.py +4 -0
  14. schemathesis/cli/callbacks.py +58 -13
  15. schemathesis/cli/cassettes.py +233 -47
  16. schemathesis/cli/constants.py +8 -2
  17. schemathesis/cli/context.py +24 -4
  18. schemathesis/cli/debug.py +2 -1
  19. schemathesis/cli/handlers.py +4 -1
  20. schemathesis/cli/junitxml.py +103 -22
  21. schemathesis/cli/options.py +15 -4
  22. schemathesis/cli/output/default.py +286 -115
  23. schemathesis/cli/output/short.py +25 -6
  24. schemathesis/cli/reporting.py +79 -0
  25. schemathesis/cli/sanitization.py +6 -0
  26. schemathesis/code_samples.py +5 -3
  27. schemathesis/constants.py +1 -0
  28. schemathesis/contrib/openapi/__init__.py +1 -1
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
  30. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  31. schemathesis/contrib/unique_data.py +3 -3
  32. schemathesis/exceptions.py +76 -65
  33. schemathesis/experimental/__init__.py +35 -0
  34. schemathesis/extra/_aiohttp.py +1 -0
  35. schemathesis/extra/_flask.py +4 -1
  36. schemathesis/extra/_server.py +1 -0
  37. schemathesis/extra/pytest_plugin.py +17 -25
  38. schemathesis/failures.py +77 -9
  39. schemathesis/filters.py +185 -8
  40. schemathesis/fixups/__init__.py +1 -0
  41. schemathesis/fixups/fast_api.py +2 -2
  42. schemathesis/fixups/utf8_bom.py +1 -2
  43. schemathesis/generation/__init__.py +20 -36
  44. schemathesis/generation/_hypothesis.py +59 -0
  45. schemathesis/generation/_methods.py +44 -0
  46. schemathesis/generation/coverage.py +931 -0
  47. schemathesis/graphql.py +0 -1
  48. schemathesis/hooks.py +89 -12
  49. schemathesis/internal/checks.py +84 -0
  50. schemathesis/internal/copy.py +22 -3
  51. schemathesis/internal/deprecation.py +6 -2
  52. schemathesis/internal/diff.py +15 -0
  53. schemathesis/internal/extensions.py +27 -0
  54. schemathesis/internal/jsonschema.py +2 -1
  55. schemathesis/internal/output.py +68 -0
  56. schemathesis/internal/result.py +1 -1
  57. schemathesis/internal/transformation.py +11 -0
  58. schemathesis/lazy.py +138 -25
  59. schemathesis/loaders.py +7 -5
  60. schemathesis/models.py +323 -213
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +72 -22
  63. schemathesis/runner/events.py +86 -6
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +447 -187
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/{cli → runner}/probes.py +37 -25
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +17 -4
  73. schemathesis/service/ci.py +1 -0
  74. schemathesis/service/client.py +39 -6
  75. schemathesis/service/events.py +5 -1
  76. schemathesis/service/extensions.py +224 -0
  77. schemathesis/service/hosts.py +6 -2
  78. schemathesis/service/metadata.py +25 -0
  79. schemathesis/service/models.py +211 -2
  80. schemathesis/service/report.py +6 -6
  81. schemathesis/service/serialization.py +60 -71
  82. schemathesis/service/usage.py +1 -0
  83. schemathesis/specs/graphql/_cache.py +26 -0
  84. schemathesis/specs/graphql/loaders.py +25 -5
  85. schemathesis/specs/graphql/nodes.py +1 -0
  86. schemathesis/specs/graphql/scalars.py +2 -2
  87. schemathesis/specs/graphql/schemas.py +130 -100
  88. schemathesis/specs/graphql/validation.py +1 -2
  89. schemathesis/specs/openapi/__init__.py +1 -0
  90. schemathesis/specs/openapi/_cache.py +123 -0
  91. schemathesis/specs/openapi/_hypothesis.py +79 -61
  92. schemathesis/specs/openapi/checks.py +504 -25
  93. schemathesis/specs/openapi/converter.py +31 -4
  94. schemathesis/specs/openapi/definitions.py +10 -17
  95. schemathesis/specs/openapi/examples.py +143 -31
  96. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  97. schemathesis/specs/openapi/expressions/context.py +1 -1
  98. schemathesis/specs/openapi/expressions/extractors.py +26 -0
  99. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  100. schemathesis/specs/openapi/expressions/nodes.py +29 -6
  101. schemathesis/specs/openapi/expressions/parser.py +26 -5
  102. schemathesis/specs/openapi/formats.py +44 -0
  103. schemathesis/specs/openapi/links.py +125 -42
  104. schemathesis/specs/openapi/loaders.py +77 -36
  105. schemathesis/specs/openapi/media_types.py +34 -0
  106. schemathesis/specs/openapi/negative/__init__.py +6 -3
  107. schemathesis/specs/openapi/negative/mutations.py +21 -6
  108. schemathesis/specs/openapi/parameters.py +39 -25
  109. schemathesis/specs/openapi/patterns.py +137 -0
  110. schemathesis/specs/openapi/references.py +37 -7
  111. schemathesis/specs/openapi/schemas.py +368 -242
  112. schemathesis/specs/openapi/security.py +25 -7
  113. schemathesis/specs/openapi/serialization.py +1 -0
  114. schemathesis/specs/openapi/stateful/__init__.py +198 -70
  115. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  116. schemathesis/specs/openapi/stateful/types.py +14 -0
  117. schemathesis/specs/openapi/utils.py +6 -1
  118. schemathesis/specs/openapi/validation.py +1 -0
  119. schemathesis/stateful/__init__.py +35 -21
  120. schemathesis/stateful/config.py +97 -0
  121. schemathesis/stateful/context.py +135 -0
  122. schemathesis/stateful/events.py +274 -0
  123. schemathesis/stateful/runner.py +309 -0
  124. schemathesis/stateful/sink.py +68 -0
  125. schemathesis/stateful/state_machine.py +67 -38
  126. schemathesis/stateful/statistic.py +22 -0
  127. schemathesis/stateful/validation.py +100 -0
  128. schemathesis/targets.py +33 -1
  129. schemathesis/throttling.py +25 -5
  130. schemathesis/transports/__init__.py +354 -0
  131. schemathesis/transports/asgi.py +7 -0
  132. schemathesis/transports/auth.py +25 -2
  133. schemathesis/transports/content_types.py +3 -1
  134. schemathesis/transports/headers.py +2 -1
  135. schemathesis/transports/responses.py +9 -4
  136. schemathesis/types.py +9 -0
  137. schemathesis/utils.py +11 -16
  138. schemathesis-3.39.7.dist-info/METADATA +293 -0
  139. schemathesis-3.39.7.dist-info/RECORD +160 -0
  140. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
  141. schemathesis/specs/openapi/filters.py +0 -49
  142. schemathesis/specs/openapi/stateful/links.py +0 -92
  143. schemathesis-3.25.5.dist-info/METADATA +0 -356
  144. schemathesis-3.25.5.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from itertools import groupby
4
+ from typing import TYPE_CHECKING, Callable, Generator, Iterator
5
+
6
+ import click
7
+
8
+ from ..exceptions import RuntimeErrorType
9
+ from ..runner.serialization import SerializedCheck, deduplicate_failures
10
+
11
+ if TYPE_CHECKING:
12
+ from ..code_samples import CodeSampleStyle
13
+
14
+ TEST_CASE_ID_TITLE = "Test Case ID"
15
+
16
+
17
+ def group_by_case(
18
+ checks: list[SerializedCheck], code_sample_style: CodeSampleStyle
19
+ ) -> Generator[tuple[str, Iterator[SerializedCheck]], None, None]:
20
+ checks = deduplicate_failures(checks)
21
+ checks = sorted(checks, key=lambda c: _by_unique_key(c, code_sample_style))
22
+ for (sample, _, _), gen in groupby(checks, lambda c: _by_unique_key(c, code_sample_style)):
23
+ yield (sample, gen)
24
+
25
+
26
+ def _by_unique_key(check: SerializedCheck, code_sample_style: CodeSampleStyle) -> tuple[str, int, str]:
27
+ return (
28
+ code_sample_style.generate(
29
+ method=check.example.method,
30
+ url=check.example.url,
31
+ body=check.example.deserialize_body(),
32
+ headers=check.example.headers,
33
+ verify=check.example.verify,
34
+ extra_headers=check.example.extra_headers,
35
+ ),
36
+ 0 if not check.response else check.response.status_code,
37
+ "SCHEMATHESIS-INTERNAL-NO-RESPONSE"
38
+ if not check.response
39
+ else check.response.body or "SCHEMATHESIS-INTERNAL-EMPTY-BODY",
40
+ )
41
+
42
+
43
+ def split_traceback(traceback: str) -> list[str]:
44
+ return [entry for entry in traceback.splitlines() if entry]
45
+
46
+
47
+ def bold(option: str) -> str:
48
+ return click.style(option, bold=True)
49
+
50
+
51
+ def get_runtime_error_suggestion(error_type: RuntimeErrorType, bold: Callable[[str], str] = bold) -> str | None:
52
+ DISABLE_SSL_SUGGESTION = f"Bypass SSL verification with {bold('`--request-tls-verify=false`')}."
53
+ DISABLE_SCHEMA_VALIDATION_SUGGESTION = (
54
+ f"Bypass validation using {bold('`--validate-schema=false`')}. Caution: May cause unexpected errors."
55
+ )
56
+
57
+ def _format_health_check_suggestion(label: str) -> str:
58
+ return f"Bypass this health check using {bold(f'`--hypothesis-suppress-health-check={label}`')}."
59
+
60
+ RUNTIME_ERROR_SUGGESTIONS = {
61
+ RuntimeErrorType.CONNECTION_SSL: DISABLE_SSL_SUGGESTION,
62
+ RuntimeErrorType.HYPOTHESIS_DEADLINE_EXCEEDED: (
63
+ f"Adjust the deadline using {bold('`--hypothesis-deadline=MILLIS`')} or "
64
+ f"disable with {bold('`--hypothesis-deadline=None`')}."
65
+ ),
66
+ RuntimeErrorType.HYPOTHESIS_UNSATISFIABLE: "Examine the schema for inconsistencies and consider simplifying it.",
67
+ RuntimeErrorType.SCHEMA_BODY_IN_GET_REQUEST: DISABLE_SCHEMA_VALIDATION_SUGGESTION,
68
+ RuntimeErrorType.SCHEMA_INVALID_REGULAR_EXPRESSION: "Ensure your regex is compatible with Python's syntax.\n"
69
+ "For guidance, visit: https://docs.python.org/3/library/re.html",
70
+ RuntimeErrorType.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Define a custom strategy for it.\n"
71
+ "For guidance, visit: https://schemathesis.readthedocs.io/en/stable/graphql.html#custom-scalars",
72
+ RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE: _format_health_check_suggestion("data_too_large"),
73
+ RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH: _format_health_check_suggestion("filter_too_much"),
74
+ RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW: _format_health_check_suggestion("too_slow"),
75
+ RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE: _format_health_check_suggestion(
76
+ "large_base_example"
77
+ ),
78
+ }
79
+ return RUNTIME_ERROR_SUGGESTIONS.get(error_type)
@@ -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
 
@@ -18,3 +19,8 @@ class SanitizationHandler(EventHandler):
18
19
  sanitize_serialized_check(check)
19
20
  for interaction in event.result.interactions:
20
21
  sanitize_serialized_interaction(interaction)
22
+ elif isinstance(event, events.AfterStatefulExecution):
23
+ for check in event.result.checks:
24
+ sanitize_serialized_check(check)
25
+ for interaction in event.result.interactions:
26
+ sanitize_serialized_interaction(interaction)
@@ -1,15 +1,17 @@
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
5
6
  from typing import TYPE_CHECKING
6
7
 
7
8
  from .constants import SCHEMATHESIS_TEST_CASE_HEADER
8
- from .types import Headers
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from requests.structures import CaseInsensitiveDict
12
12
 
13
+ from .types import Headers
14
+
13
15
 
14
16
  @lru_cache
15
17
  def get_excluded_headers() -> CaseInsensitiveDict:
@@ -119,9 +121,9 @@ def _generate_requests(
119
121
  url = _escape_single_quotes(url)
120
122
  command = f"requests.{method.lower()}('{url}'"
121
123
  if body:
122
- command += f", data={repr(body)}"
124
+ command += f", data={body!r}"
123
125
  if headers:
124
- command += f", headers={repr(headers)}"
126
+ command += f", headers={headers!r}"
125
127
  if not verify:
126
128
  command += ", verify=False"
127
129
  command += ")"
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:
@@ -16,7 +18,7 @@ def uninstall() -> None:
16
18
 
17
19
  def before_add_examples(context: HookContext, examples: list[Case]) -> None:
18
20
  if not examples and context.operation is not None:
19
- from ..._hypothesis import add_single_example
21
+ from ...generation import add_single_example
20
22
 
21
23
  strategy = context.operation.as_strategy()
22
24
  add_single_example(strategy, examples)
@@ -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,14 +6,14 @@ 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(
15
- "The `--contrib-unique-data` CLI option and the corresponding `schemathesis.contrib.unique_data` hook "
16
- "are **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
16
+ "The `schemathesis.contrib.unique_data` hook is **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
17
17
  "strategies are configurable on a per-example basis but this feature implies uniqueness across examples. "
18
18
  "This leads to cryptic error messages about external state and flaky test runs, "
19
19
  "therefore it will be removed in Schemathesis 4.0",
@@ -1,24 +1,28 @@
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
7
7
  from hashlib import sha1
8
- from json import JSONDecodeError
9
- from types import TracebackType
10
8
  from typing import TYPE_CHECKING, Any, Callable, Generator, NoReturn
11
9
 
12
10
  from .constants import SERIALIZERS_SUGGESTION_MESSAGE
13
- from .failures import FailureContext
11
+ from .internal.output import truncate_json
14
12
 
15
13
  if TYPE_CHECKING:
14
+ from json import JSONDecodeError
15
+ from types import TracebackType
16
+
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 .failures import FailureContext
24
+ from .transports.responses import GenericResponse
25
+
22
26
 
23
27
  class CheckFailed(AssertionError):
24
28
  """Custom error type to distinguish from arbitrary AssertionError that may happen in the dependent libraries."""
@@ -97,52 +101,72 @@ def get_grouped_exception(prefix: str, *exceptions: AssertionError) -> type[Chec
97
101
  return _get_hashed_exception("GroupedException", f"{prefix}{message}")
98
102
 
99
103
 
100
- def get_server_error(status_code: int) -> type[CheckFailed]:
104
+ def get_server_error(prefix: str, status_code: int) -> type[CheckFailed]:
101
105
  """Return new exception for the Internal Server Error cases."""
102
- name = f"ServerError{status_code}"
106
+ name = f"ServerError{prefix}{status_code}"
103
107
  return get_exception(name)
104
108
 
105
109
 
106
- def get_status_code_error(status_code: int) -> type[CheckFailed]:
110
+ def get_status_code_error(prefix: str, status_code: int) -> type[CheckFailed]:
107
111
  """Return new exception for an unexpected status code."""
108
- name = f"StatusCodeError{status_code}"
112
+ name = f"StatusCodeError{prefix}{status_code}"
109
113
  return get_exception(name)
110
114
 
111
115
 
112
- def get_response_type_error(expected: str, received: str) -> type[CheckFailed]:
116
+ def get_response_type_error(prefix: str, expected: str, received: str) -> type[CheckFailed]:
113
117
  """Return new exception for an unexpected response type."""
114
- name = f"SchemaValidationError{expected}_{received}"
118
+ name = f"SchemaValidationError{prefix}{expected}_{received}"
115
119
  return get_exception(name)
116
120
 
117
121
 
118
- def get_malformed_media_type_error(media_type: str) -> type[CheckFailed]:
119
- name = f"MalformedMediaType{media_type}"
122
+ def get_malformed_media_type_error(prefix: str, media_type: str) -> type[CheckFailed]:
123
+ name = f"MalformedMediaType{prefix}{media_type}"
120
124
  return get_exception(name)
121
125
 
122
126
 
123
- def get_missing_content_type_error() -> type[CheckFailed]:
127
+ def get_missing_content_type_error(prefix: str) -> type[CheckFailed]:
124
128
  """Return new exception for a missing Content-Type header."""
125
- return get_exception("MissingContentTypeError")
129
+ return get_exception(f"MissingContentTypeError{prefix}")
126
130
 
127
131
 
128
- def get_schema_validation_error(exception: ValidationError) -> type[CheckFailed]:
132
+ def get_schema_validation_error(prefix: str, exception: ValidationError) -> type[CheckFailed]:
129
133
  """Return new exception for schema validation error."""
130
- return _get_hashed_exception("SchemaValidationError", str(exception))
134
+ return _get_hashed_exception(f"SchemaValidationError{prefix}", str(exception))
131
135
 
132
136
 
133
- def get_response_parsing_error(exception: JSONDecodeError) -> type[CheckFailed]:
137
+ def get_response_parsing_error(prefix: str, exception: JSONDecodeError) -> type[CheckFailed]:
134
138
  """Return new exception for response parsing error."""
135
- return _get_hashed_exception("ResponseParsingError", str(exception))
139
+ return _get_hashed_exception(f"ResponseParsingError{prefix}", str(exception))
136
140
 
137
141
 
138
- def get_headers_error(message: str) -> type[CheckFailed]:
142
+ def get_headers_error(prefix: str, message: str) -> type[CheckFailed]:
139
143
  """Return new exception for missing headers."""
140
- return _get_hashed_exception("MissingHeadersError", message)
144
+ return _get_hashed_exception(f"MissingHeadersError{prefix}", message)
145
+
146
+
147
+ def get_negative_rejection_error(prefix: str, status: int) -> type[CheckFailed]:
148
+ return _get_hashed_exception(f"AcceptedNegativeDataError{prefix}", str(status))
149
+
141
150
 
151
+ def get_positive_acceptance_error(prefix: str, status: int) -> type[CheckFailed]:
152
+ return _get_hashed_exception(f"RejectedPositiveDataError{prefix}", str(status))
142
153
 
143
- def get_timeout_error(deadline: float | int) -> type[CheckFailed]:
154
+
155
+ def get_use_after_free_error(free: str) -> type[CheckFailed]:
156
+ return _get_hashed_exception("UseAfterFreeError", free)
157
+
158
+
159
+ def get_ensure_resource_availability_error(operation: str) -> type[CheckFailed]:
160
+ return _get_hashed_exception("EnsureResourceAvailabilityError", operation)
161
+
162
+
163
+ def get_ignored_auth_error(operation: str) -> type[CheckFailed]:
164
+ return _get_hashed_exception("IgnoredAuthError", operation)
165
+
166
+
167
+ def get_timeout_error(prefix: str, deadline: float | int) -> type[CheckFailed]:
144
168
  """Request took too long."""
145
- return _get_hashed_exception("TimeoutError", str(deadline))
169
+ return _get_hashed_exception(f"TimeoutError{prefix}", str(deadline))
146
170
 
147
171
 
148
172
  def get_unexpected_graphql_response_error(type_: type) -> type[CheckFailed]:
@@ -158,7 +182,7 @@ def get_grouped_graphql_error(errors: list[GraphQLFormattedError]) -> type[Check
158
182
  if "locations" in error:
159
183
  message += ";locations:"
160
184
  for location in sorted(error["locations"]):
161
- message += f"({location['line'],location['column']})"
185
+ message += f"({location['line'], location['column']})"
162
186
  if "path" in error:
163
187
  message += ";path:"
164
188
  for chunk in error["path"]:
@@ -196,7 +220,7 @@ class OperationSchemaError(Exception):
196
220
  message = "Invalid schema definition"
197
221
  error_path = " -> ".join(str(entry) for entry in error.path) or "[root]"
198
222
  message += f"\n\nLocation:\n {error_path}"
199
- instance = truncated_json(error.instance)
223
+ instance = truncate_json(error.instance)
200
224
  message += f"\n\nProblematic definition:\n{instance}"
201
225
  message += "\n\nError details:\n "
202
226
  # This default message contains the instance which we already printed
@@ -211,9 +235,11 @@ class OperationSchemaError(Exception):
211
235
  def from_reference_resolution_error(
212
236
  cls, error: RefResolutionError, path: str | None, method: str | None, full_path: str | None
213
237
  ) -> OperationSchemaError:
238
+ notes = getattr(error, "__notes__", [])
239
+ # Some exceptions don't have the actual reference in them, hence we add it manually via notes
240
+ pointer = f"'{notes[0]}'"
214
241
  message = "Unresolvable JSON pointer in the schema"
215
242
  # Get the pointer value from "Unresolvable JSON pointer: 'components/UnknownParameter'"
216
- pointer = str(error).split(": ", 1)[-1]
217
243
  message += f"\n\nError details:\n JSON pointer: {pointer}"
218
244
  message += "\n This typically means that the schema is referencing a component that doesn't exist."
219
245
  message += f"\n\n{SCHEMA_ERROR_SUGGESTION}"
@@ -289,39 +315,6 @@ class InvalidHeadersExample(OperationSchemaError):
289
315
  return cls(message)
290
316
 
291
317
 
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
318
  class DeadlineExceeded(Exception):
326
319
  """Test took too long to run."""
327
320
 
@@ -336,6 +329,12 @@ class DeadlineExceeded(Exception):
336
329
  )
337
330
 
338
331
 
332
+ class RecursiveReferenceError(Exception):
333
+ """Recursive reference is impossible to resolve due to current limitations."""
334
+
335
+ __module__ = "builtins"
336
+
337
+
339
338
  @enum.unique
340
339
  class RuntimeErrorType(str, enum.Enum):
341
340
  # Connection related issues
@@ -354,6 +353,7 @@ class RuntimeErrorType(str, enum.Enum):
354
353
 
355
354
  SCHEMA_BODY_IN_GET_REQUEST = "schema_body_in_get_request"
356
355
  SCHEMA_INVALID_REGULAR_EXPRESSION = "schema_invalid_regular_expression"
356
+ SCHEMA_UNSUPPORTED = "schema_unsupported"
357
357
  SCHEMA_GENERIC = "schema_generic"
358
358
 
359
359
  SERIALIZATION_NOT_POSSIBLE = "serialization_not_possible"
@@ -367,6 +367,7 @@ class RuntimeErrorType(str, enum.Enum):
367
367
  return self not in (
368
368
  RuntimeErrorType.SCHEMA_BODY_IN_GET_REQUEST,
369
369
  RuntimeErrorType.SCHEMA_INVALID_REGULAR_EXPRESSION,
370
+ RuntimeErrorType.SCHEMA_UNSUPPORTED,
370
371
  RuntimeErrorType.SCHEMA_GENERIC,
371
372
  RuntimeErrorType.SERIALIZATION_NOT_POSSIBLE,
372
373
  )
@@ -532,8 +533,14 @@ def remove_ssl_line_number(text: str) -> str:
532
533
  return re.sub(r"\(_ssl\.c:\d+\)", "", text)
533
534
 
534
535
 
536
+ def _clean_inner_request_message(message: Any) -> str:
537
+ if isinstance(message, str) and message.startswith("HTTPConnectionPool"):
538
+ return re.sub(r"HTTPConnectionPool\(.+?\): ", "", message).rstrip(".")
539
+ return str(message)
540
+
541
+
535
542
  def extract_requests_exception_details(exc: RequestException) -> tuple[str, list[str]]:
536
- from requests.exceptions import SSLError, ConnectionError, ChunkedEncodingError
543
+ from requests.exceptions import ChunkedEncodingError, ConnectionError, SSLError
537
544
  from urllib3.exceptions import MaxRetryError
538
545
 
539
546
  if isinstance(exc, SSLError):
@@ -544,13 +551,17 @@ def extract_requests_exception_details(exc: RequestException) -> tuple[str, list
544
551
  message = "Connection failed"
545
552
  inner = exc.args[0]
546
553
  if isinstance(inner, MaxRetryError) and inner.reason is not None:
547
- if ":" not in inner.reason.args[0]:
548
- reason = inner.reason.args[0]
554
+ arg = inner.reason.args[0]
555
+ if isinstance(arg, str):
556
+ if ":" not in arg:
557
+ reason = arg
558
+ else:
559
+ _, reason = arg.split(":", maxsplit=1)
549
560
  else:
550
- _, reason = inner.reason.args[0].split(":", maxsplit=1)
561
+ reason = f"Max retries exceeded with url: {inner.url}"
551
562
  extra = [reason.strip()]
552
563
  else:
553
- extra = [" ".join(map(str, inner.args))]
564
+ extra = [" ".join(map(_clean_inner_request_message, inner.args))]
554
565
  elif isinstance(exc, ChunkedEncodingError):
555
566
  message = "Connection broken. The server declared chunked encoding but sent an invalid chunk"
556
567
  extra = [str(exc.args[0].args[1])]
@@ -72,3 +72,38 @@ OPEN_API_3_1 = GLOBAL_EXPERIMENTS.create_experiment(
72
72
  description="Support for response validation",
73
73
  discussion_url="https://github.com/schemathesis/schemathesis/discussions/1822",
74
74
  )
75
+ SCHEMA_ANALYSIS = GLOBAL_EXPERIMENTS.create_experiment(
76
+ name="schema-analysis",
77
+ verbose_name="Schema Analysis",
78
+ env_var="SCHEMA_ANALYSIS",
79
+ description="Analyzing API schemas via Schemathesis.io",
80
+ discussion_url="https://github.com/schemathesis/schemathesis/discussions/2056",
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
+ )
96
+ COVERAGE_PHASE = GLOBAL_EXPERIMENTS.create_experiment(
97
+ name="coverage-phase",
98
+ verbose_name="Coverage phase",
99
+ env_var="COVERAGE_PHASE",
100
+ description="Generate covering test cases",
101
+ discussion_url="https://github.com/schemathesis/schemathesis/discussions/2418",
102
+ )
103
+ POSITIVE_DATA_ACCEPTANCE = GLOBAL_EXPERIMENTS.create_experiment(
104
+ name="positive_data_acceptance",
105
+ verbose_name="Positive Data Acceptance",
106
+ env_var="POSITIVE_DATA_ACCEPTANCE",
107
+ description="Verifying schema-conformant data is accepted",
108
+ discussion_url="https://github.com/schemathesis/schemathesis/discussions/2499",
109
+ )
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  import asyncio
3
4
 
4
5
  from aiohttp import web
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
- from flask import Flask
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  from . import _server
6
6
 
7
+ if TYPE_CHECKING:
8
+ from flask import Flask
9
+
7
10
 
8
11
  def run_server(app: Flask, port: int | None = None, timeout: float = 0.05) -> int:
9
12
  """Start a thread with the given aiohttp application."""
@@ -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,17 @@ 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 TYPE_CHECKING, 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
- from _pytest.fixtures import FuncFixtureInfo
12
- from _pytest.nodes import Node
13
11
  from _pytest.python import Class, Function, FunctionDefinition, Metafunc, Module, PyCollector
14
12
  from hypothesis import reporting
15
13
  from hypothesis.errors import InvalidArgument, Unsatisfiable
16
14
  from jsonschema.exceptions import SchemaError
17
15
 
18
- from .._dependency_versions import IS_PYTEST_ABOVE_7, IS_PYTEST_ABOVE_8, IS_PYTEST_ABOVE_54
16
+ from .._dependency_versions import IS_PYTEST_ABOVE_7, IS_PYTEST_ABOVE_8
19
17
  from .._override import get_override_from_mark
20
18
  from ..constants import (
21
19
  GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE,
@@ -31,7 +29,6 @@ from ..exceptions import (
31
29
  UsageError,
32
30
  )
33
31
  from ..internal.result import Ok, Result
34
- from ..models import APIOperation
35
32
  from ..utils import (
36
33
  PARAMETRIZE_MARKER,
37
34
  fail_on_no_matches,
@@ -43,13 +40,10 @@ from ..utils import (
43
40
  validate_given_args,
44
41
  )
45
42
 
46
- T = TypeVar("T", bound=Node)
43
+ if TYPE_CHECKING:
44
+ from _pytest.fixtures import FuncFixtureInfo
47
45
 
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)
46
+ from ..models import APIOperation
53
47
 
54
48
 
55
49
  class SchemathesisFunction(Function):
@@ -155,7 +149,9 @@ class SchemathesisCase(PyCollector):
155
149
  name += f"[{error.full_path}]"
156
150
 
157
151
  cls = self._get_class_parent()
158
- definition: FunctionDefinition = create(FunctionDefinition, name=self.name, parent=self.parent, callobj=funcobj)
152
+ definition: FunctionDefinition = FunctionDefinition.from_parent(
153
+ name=self.name, parent=self.parent, callobj=funcobj
154
+ )
159
155
  fixturemanager = self.session._fixturemanager
160
156
  fixtureinfo = fixturemanager.getfixtureinfo(definition, funcobj, cls)
161
157
 
@@ -166,8 +162,7 @@ class SchemathesisCase(PyCollector):
166
162
  funcobj = partial(funcobj, self.parent.obj)
167
163
 
168
164
  if not metafunc._calls:
169
- yield create(
170
- SchemathesisFunction,
165
+ yield SchemathesisFunction.from_parent(
171
166
  name=name,
172
167
  parent=self.parent,
173
168
  callobj=funcobj,
@@ -181,10 +176,9 @@ class SchemathesisCase(PyCollector):
181
176
  fixtureinfo.prune_dependency_tree()
182
177
  for callspec in metafunc._calls:
183
178
  subname = f"{name}[{callspec.id}]"
184
- yield create(
185
- SchemathesisFunction,
179
+ yield SchemathesisFunction.from_parent(
180
+ self.parent,
186
181
  name=subname,
187
- parent=self.parent,
188
182
  callspec=callspec,
189
183
  callobj=funcobj,
190
184
  fixtureinfo=fixtureinfo,
@@ -206,7 +200,7 @@ class SchemathesisCase(PyCollector):
206
200
  kwargs["_ispytest"] = True
207
201
  metafunc = Metafunc(definition, fixtureinfo, self.config, **kwargs)
208
202
  methods = []
209
- if hasattr(module, "pytest_generate_tests"):
203
+ if module is not None and hasattr(module, "pytest_generate_tests"):
210
204
  methods.append(module.pytest_generate_tests)
211
205
  if hasattr(cls, "pytest_generate_tests"):
212
206
  cls = cast(Type, cls)
@@ -236,7 +230,7 @@ def pytest_pycollect_makeitem(collector: nodes.Collector, name: str, obj: Any) -
236
230
  """Switch to a different collector if the test is parametrized marked by schemathesis."""
237
231
  outcome = yield
238
232
  if is_schemathesis_test(obj):
239
- outcome.force_result(create(SchemathesisCase, parent=collector, test_function=obj, name=name))
233
+ outcome.force_result(SchemathesisCase.from_parent(collector, test_function=obj, name=name))
240
234
  else:
241
235
  outcome.get_result()
242
236
 
@@ -261,7 +255,7 @@ def skip_unnecessary_hypothesis_output() -> Generator:
261
255
  yield
262
256
 
263
257
 
264
- @hookimpl(hookwrapper=True)
258
+ @hookimpl(wrapper=True)
265
259
  def pytest_pyfunc_call(pyfuncitem): # type:ignore
266
260
  """It is possible to have a Hypothesis exception in runtime.
267
261
 
@@ -278,10 +272,9 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
278
272
 
279
273
  __tracebackhide__ = True
280
274
  if isinstance(pyfuncitem, SchemathesisFunction):
281
- with skip_unnecessary_hypothesis_output():
282
- outcome = yield
283
275
  try:
284
- outcome.get_result()
276
+ with skip_unnecessary_hypothesis_output():
277
+ yield
285
278
  except InvalidArgument as exc:
286
279
  if "Inconsistent args" in str(exc) and "@example()" in str(exc):
287
280
  raise UsageError(GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE) from None
@@ -316,5 +309,4 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
316
309
  if invalid_headers is not None:
317
310
  raise InvalidHeadersExample.from_headers(invalid_headers) from None
318
311
  else:
319
- outcome = yield
320
- outcome.get_result()
312
+ yield