schemathesis 4.0.0a12__py3-none-any.whl → 4.0.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 (41) hide show
  1. schemathesis/__init__.py +9 -4
  2. schemathesis/auths.py +20 -30
  3. schemathesis/checks.py +5 -0
  4. schemathesis/cli/commands/run/__init__.py +9 -6
  5. schemathesis/cli/commands/run/handlers/output.py +13 -0
  6. schemathesis/cli/constants.py +1 -1
  7. schemathesis/config/_operations.py +16 -21
  8. schemathesis/config/_projects.py +5 -1
  9. schemathesis/core/errors.py +10 -17
  10. schemathesis/core/transport.py +81 -1
  11. schemathesis/engine/errors.py +1 -1
  12. schemathesis/generation/case.py +152 -28
  13. schemathesis/generation/hypothesis/builder.py +12 -12
  14. schemathesis/generation/overrides.py +11 -27
  15. schemathesis/generation/stateful/__init__.py +13 -0
  16. schemathesis/generation/stateful/state_machine.py +31 -108
  17. schemathesis/graphql/loaders.py +14 -4
  18. schemathesis/hooks.py +1 -4
  19. schemathesis/openapi/checks.py +82 -20
  20. schemathesis/openapi/generation/filters.py +9 -2
  21. schemathesis/openapi/loaders.py +14 -4
  22. schemathesis/pytest/lazy.py +4 -31
  23. schemathesis/pytest/plugin.py +21 -11
  24. schemathesis/schemas.py +153 -89
  25. schemathesis/specs/graphql/schemas.py +6 -6
  26. schemathesis/specs/openapi/_hypothesis.py +39 -14
  27. schemathesis/specs/openapi/checks.py +95 -34
  28. schemathesis/specs/openapi/expressions/nodes.py +1 -1
  29. schemathesis/specs/openapi/negative/__init__.py +5 -3
  30. schemathesis/specs/openapi/negative/mutations.py +2 -2
  31. schemathesis/specs/openapi/parameters.py +0 -3
  32. schemathesis/specs/openapi/schemas.py +6 -91
  33. schemathesis/specs/openapi/stateful/links.py +1 -63
  34. schemathesis/transport/requests.py +12 -1
  35. schemathesis/transport/serialization.py +0 -4
  36. schemathesis/transport/wsgi.py +7 -0
  37. {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/METADATA +8 -10
  38. {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/RECORD +41 -41
  39. {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/WHEEL +0 -0
  40. {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/entry_points.txt +0 -0
  41. {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py CHANGED
@@ -1,15 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from schemathesis import errors, graphql, openapi, pytest
4
- from schemathesis.auths import AuthContext, auth
4
+ from schemathesis.auths import AuthContext, AuthProvider, auth
5
5
  from schemathesis.checks import CheckContext, CheckFunction, check
6
+ from schemathesis.config import SchemathesisConfig as Config
6
7
  from schemathesis.core.transport import Response
7
8
  from schemathesis.core.version import SCHEMATHESIS_VERSION
8
- from schemathesis.generation import GenerationMode
9
+ from schemathesis.generation import GenerationMode, stateful
9
10
  from schemathesis.generation.case import Case
10
11
  from schemathesis.generation.metrics import MetricContext, MetricFunction, metric
11
12
  from schemathesis.hooks import HookContext, hook
12
- from schemathesis.schemas import BaseSchema
13
+ from schemathesis.schemas import APIOperation, BaseSchema
13
14
  from schemathesis.transport import SerializationContext, serializer
14
15
 
15
16
  __version__ = SCHEMATHESIS_VERSION
@@ -19,8 +20,11 @@ __all__ = [
19
20
  # Core data structures
20
21
  "Case",
21
22
  "Response",
22
- "GenerationMode",
23
+ "APIOperation",
23
24
  "BaseSchema",
25
+ "Config",
26
+ "GenerationMode",
27
+ "stateful",
24
28
  # Public errors
25
29
  "errors",
26
30
  # Spec or usage specific namespaces
@@ -37,6 +41,7 @@ __all__ = [
37
41
  # Auth
38
42
  "auth",
39
43
  "AuthContext",
44
+ "AuthProvider",
40
45
  # Targeted Property-based Testing
41
46
  "metric",
42
47
  "MetricContext",
schemathesis/auths.py CHANGED
@@ -69,22 +69,28 @@ CacheKeyFunction = Callable[["Case", "AuthContext"], Union[str, int]]
69
69
 
70
70
  @runtime_checkable
71
71
  class AuthProvider(Generic[Auth], Protocol):
72
- """Get authentication data for an API and set it on the generated test cases."""
72
+ """Protocol for implementing custom authentication in API tests."""
73
73
 
74
- def get(self, case: Case, context: AuthContext) -> Auth | None:
75
- """Get the authentication data.
74
+ def get(self, case: Case, ctx: AuthContext) -> Auth | None:
75
+ """Obtain authentication data for the test case.
76
+
77
+ Args:
78
+ case: Generated test case requiring authentication.
79
+ ctx: Authentication state and configuration.
80
+
81
+ Returns:
82
+ Authentication data (e.g., token, credentials) or `None`.
76
83
 
77
- :param Case case: Generated test case.
78
- :param AuthContext context: Holds state relevant for the authentication process.
79
- :return: Any authentication data you find useful for your use case. For example, it could be an access token.
80
84
  """
81
85
 
82
- def set(self, case: Case, data: Auth, context: AuthContext) -> None:
83
- """Set authentication data on a generated test case.
86
+ def set(self, case: Case, data: Auth, ctx: AuthContext) -> None:
87
+ """Apply authentication data to the test case.
88
+
89
+ Args:
90
+ case: Test case to modify.
91
+ data: Authentication data from the `get` method.
92
+ ctx: Authentication state and configuration.
84
93
 
85
- :param Optional[Auth] data: Authentication data you got from the ``get`` method.
86
- :param Case case: Generated test case.
87
- :param AuthContext context: Holds state relevant for the authentication process.
88
94
  """
89
95
 
90
96
 
@@ -333,8 +339,9 @@ class AuthStorage(Generic[Auth]):
333
339
  ) -> None:
334
340
  if not issubclass(provider_class, AuthProvider):
335
341
  raise TypeError(
336
- f"`{provider_class.__name__}` is not a valid auth provider. "
337
- f"Check `schemathesis.auths.AuthProvider` documentation for examples."
342
+ f"`{provider_class.__name__}` does not implement the `AuthProvider` protocol. "
343
+ f"Auth providers must have `get` and `set` methods. "
344
+ f"See `schemathesis.AuthProvider` documentation for examples."
338
345
  )
339
346
  provider: AuthProvider
340
347
  # Apply caching if desired
@@ -389,23 +396,6 @@ class AuthStorage(Generic[Auth]):
389
396
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
390
397
  cache_by_key: CacheKeyFunction | None = None,
391
398
  ) -> FilterableApplyAuth:
392
- """Register auth provider only on one test function.
393
-
394
- :param Type[AuthProvider] provider_class: Authentication provider class.
395
- :param Optional[int] refresh_interval: Cache duration in seconds.
396
-
397
- .. code-block:: python
398
-
399
- class Auth:
400
- ...
401
-
402
-
403
- @schema.auth(Auth)
404
- @schema.parametrize()
405
- def test_api(case):
406
- ...
407
-
408
- """
409
399
  filter_set = FilterSet()
410
400
 
411
401
  def wrapper(test: Callable) -> Callable:
schemathesis/checks.py CHANGED
@@ -91,6 +91,11 @@ class CheckContext:
91
91
  CHECKS = Registry[CheckFunction]()
92
92
 
93
93
 
94
+ def load_all_checks() -> None:
95
+ # NOTE: Trigger registering all Open API checks
96
+ from schemathesis.specs.openapi.checks import status_code_conformance # noqa: F401, F403
97
+
98
+
94
99
  def check(func: CheckFunction) -> CheckFunction:
95
100
  """Register a custom validation check to run against API responses.
96
101
 
@@ -6,7 +6,7 @@ from typing import Any, Callable
6
6
  import click
7
7
  from click.utils import LazyFile
8
8
 
9
- from schemathesis.checks import CHECKS
9
+ from schemathesis.checks import CHECKS, load_all_checks
10
10
  from schemathesis.cli.commands.run import executor, validation
11
11
  from schemathesis.cli.commands.run.filters import with_filters
12
12
  from schemathesis.cli.constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
@@ -30,8 +30,7 @@ from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
30
30
  from schemathesis.generation import GenerationMode
31
31
  from schemathesis.generation.metrics import METRICS, MetricFunction
32
32
 
33
- # NOTE: Need to explicitly import all registered checks
34
- from schemathesis.specs.openapi.checks import * # noqa: F401, F403
33
+ load_all_checks()
35
34
 
36
35
  COLOR_OPTIONS_INVALID_USAGE_MESSAGE = "Can't use `--no-color` and `--force-color` simultaneously"
37
36
 
@@ -477,10 +476,14 @@ def run(
477
476
  no_color: bool = False,
478
477
  **__kwargs: Any,
479
478
  ) -> None:
480
- """Run tests against an API using a specified SCHEMA.
479
+ """Generate and run property-based tests against your API.
481
480
 
482
- [Required] SCHEMA: Path to an OpenAPI (`.json`, `.yml`) or GraphQL SDL file, or a URL pointing to such specifications
483
- """
481
+ \b
482
+ LOCATION can be:
483
+ - Local file: ./openapi.json, ./schema.yaml, ./schema.graphql
484
+ - OpenAPI URL: https://api.example.com/openapi.json
485
+ - GraphQL URL: https://api.example.com/graphql/
486
+ """ # noqa: D301
484
487
  if no_color and force_color:
485
488
  raise click.UsageError(COLOR_OPTIONS_INVALID_USAGE_MESSAGE)
486
489
 
@@ -1039,6 +1039,7 @@ class OutputHandler(EventHandler):
1039
1039
  assert self.stateful_tests_manager is not None
1040
1040
  links_seen = {case.transition.id for case in event.recorder.cases.values() if case.transition is not None}
1041
1041
  self.stateful_tests_manager.update(links_seen, event.status)
1042
+ self._check_stateful_warnings(ctx, event)
1042
1043
 
1043
1044
  def _check_warnings(self, ctx: ExecutionContext, event: events.ScenarioFinished) -> None:
1044
1045
  statistic = aggregate_status_codes(event.recorder.interactions.values())
@@ -1090,6 +1091,18 @@ class OutputHandler(EventHandler):
1090
1091
  ):
1091
1092
  self.warnings.validation_mismatch.add(event.recorder.label)
1092
1093
 
1094
+ def _check_stateful_warnings(self, ctx: ExecutionContext, event: events.ScenarioFinished) -> None:
1095
+ # If stateful testing had successful responses for API operations that were marked with "missing_test_data"
1096
+ # warnings, then remove them from warnings
1097
+ for key, node in event.recorder.cases.items():
1098
+ if not self.warnings.missing_test_data:
1099
+ break
1100
+ if node.value.operation.label in self.warnings.missing_test_data and key in event.recorder.interactions:
1101
+ response = event.recorder.interactions[key].response
1102
+ if response is not None and response.status_code < 300:
1103
+ self.warnings.missing_test_data.remove(node.value.operation.label)
1104
+ continue
1105
+
1093
1106
  def _on_interrupted(self, event: events.Interrupted) -> None:
1094
1107
  from rich.padding import Padding
1095
1108
 
@@ -5,4 +5,4 @@ ISSUE_TRACKER_URL = (
5
5
  "https://github.com/schemathesis/schemathesis/issues/new?"
6
6
  "labels=Status%3A%20Needs%20Triage%2C+Type%3A+Bug&template=bug_report.md&title=%5BBUG%5D"
7
7
  )
8
- EXTENSIONS_DOCUMENTATION_URL = "https://schemathesis.readthedocs.io/en/stable/extending.html"
8
+ EXTENSIONS_DOCUMENTATION_URL = "https://schemathesis.readthedocs.io/en/stable/guides/extending/"
@@ -159,34 +159,23 @@ class OperationsConfig(DiffBase):
159
159
  if exclude_deprecated:
160
160
  exclude_set.include(is_deprecated)
161
161
 
162
- # Also update operations list for consistency with config structure
163
- if not include_set.is_empty():
164
- self.operations.insert(0, OperationConfig(filter_set=include_set, enabled=True))
165
- if not exclude_set.is_empty():
166
- self.operations.insert(0, OperationConfig(filter_set=exclude_set, enabled=False))
162
+ operations = list(self.operations)
167
163
 
168
164
  final = FilterSet()
169
165
 
170
- # Get a stable reference to operations
171
- operations = list(self.operations)
172
-
173
- # Define a closure that implements our priority logic
174
166
  def priority_filter(ctx: HasAPIOperation) -> bool:
175
167
  """Filter operations according to CLI and config priority."""
176
- # 1. CLI includes override everything if present
177
- if not include_set.is_empty():
178
- return include_set.match(ctx)
179
-
180
- # 2. CLI excludes take precedence over config
181
- if not exclude_set.is_empty() and exclude_set.match(ctx):
182
- return False
183
-
184
- # 3. Check config operations in priority order (first match wins)
185
168
  for op_config in operations:
186
- if op_config._filter_set.match(ctx):
187
- return op_config.enabled
169
+ if op_config._filter_set.match(ctx) and not op_config.enabled:
170
+ return False
171
+
172
+ if not include_set.is_empty():
173
+ if exclude_set.is_empty():
174
+ return include_set.match(ctx)
175
+ return include_set.match(ctx) and not exclude_set.match(ctx)
176
+ elif not exclude_set.is_empty():
177
+ return not exclude_set.match(ctx)
188
178
 
189
- # 4. Default to include if no rule matches
190
179
  return True
191
180
 
192
181
  # Add our priority function as the filter
@@ -278,24 +267,30 @@ class OperationConfig(DiffBase):
278
267
  @classmethod
279
268
  def from_dict(cls, data: dict[str, Any]) -> OperationConfig:
280
269
  filter_set = FilterSet()
270
+ seen = set()
281
271
  for key_suffix, arg_suffix in (("", ""), ("-regex", "_regex")):
282
272
  for attr, arg_name in FILTER_ATTRIBUTES:
283
273
  key = f"include-{attr}{key_suffix}"
284
274
  if key in data:
275
+ seen.add(key)
285
276
  with reraise_filter_error(attr):
286
277
  filter_set.include(**{f"{arg_name}{arg_suffix}": data[key]})
287
278
  key = f"exclude-{attr}{key_suffix}"
288
279
  if key in data:
280
+ seen.add(key)
289
281
  with reraise_filter_error(attr):
290
282
  filter_set.exclude(**{f"{arg_name}{arg_suffix}": data[key]})
291
283
  for key, method in (("include-by", filter_set.include), ("exclude-by", filter_set.exclude)):
292
284
  if key in data:
285
+ seen.add(key)
293
286
  expression = data[key]
294
287
  try:
295
288
  func = expression_to_filter_function(expression)
296
289
  method(func)
297
290
  except ValueError:
298
291
  raise ConfigError(f"Invalid filter expression: '{expression}'") from None
292
+ if not set(data) - seen:
293
+ raise ConfigError("Operation filters defined, but no settings are being overridden")
299
294
 
300
295
  return cls(
301
296
  filter_set=filter_set,
@@ -192,6 +192,7 @@ class ProjectConfig(DiffBase):
192
192
  tls_verify: bool | str | None = None,
193
193
  request_cert: str | None = None,
194
194
  request_cert_key: str | None = None,
195
+ parameters: dict[str, Any] | None = None,
195
196
  proxy: str | None = None,
196
197
  suppress_health_check: list[HealthCheck] | None = None,
197
198
  warnings: bool | list[SchemathesisWarning] | None = None,
@@ -235,6 +236,9 @@ class ProjectConfig(DiffBase):
235
236
  if proxy is not None:
236
237
  self.proxy = proxy
237
238
 
239
+ if parameters is not None:
240
+ self.parameters = parameters
241
+
238
242
  if suppress_health_check is not None:
239
243
  self.suppress_health_check = suppress_health_check
240
244
 
@@ -251,7 +255,7 @@ class ProjectConfig(DiffBase):
251
255
  return self.auth.basic
252
256
  return None
253
257
 
254
- def headers_for(self, *, operation: APIOperation | None = None) -> dict[str, str] | None:
258
+ def headers_for(self, *, operation: APIOperation | None = None) -> dict[str, str]:
255
259
  """Get explicitly configured headers."""
256
260
  headers = self.headers.copy() if self.headers else {}
257
261
  if operation is not None:
@@ -20,10 +20,16 @@ if TYPE_CHECKING:
20
20
 
21
21
 
22
22
  SCHEMA_ERROR_SUGGESTION = "Ensure that the definition complies with the OpenAPI specification"
23
- SERIALIZERS_SUGGESTION_MESSAGE = (
24
- "You can register your own serializer with `schemathesis.serializer` "
25
- "and Schemathesis will be able to make API calls with this media type. \n"
26
- "See https://schemathesis.readthedocs.io/en/stable/how.html#payload-serialization for more information."
23
+ SERIALIZERS_DOCUMENTATION_URL = "https://schemathesis.readthedocs.io/en/stable/guides/custom-serializers/"
24
+ SERIALIZERS_SUGGESTION_MESSAGE = f"Check your schema or add custom serializers: {SERIALIZERS_DOCUMENTATION_URL}"
25
+ SERIALIZATION_NOT_POSSIBLE_MESSAGE = f"No supported serializers for media types: {{}}\n{SERIALIZERS_SUGGESTION_MESSAGE}"
26
+ SERIALIZATION_FOR_TYPE_IS_NOT_POSSIBLE_MESSAGE = (
27
+ f"Cannot serialize to '{{}}' (unsupported media type)\n{SERIALIZERS_SUGGESTION_MESSAGE}"
28
+ )
29
+ RECURSIVE_REFERENCE_ERROR_MESSAGE = (
30
+ "Currently, Schemathesis can't generate data for this operation due to "
31
+ "recursive references in the operation definition. See more information in "
32
+ "this issue - https://github.com/schemathesis/schemathesis/issues/947"
27
33
  )
28
34
 
29
35
 
@@ -277,19 +283,6 @@ class UnboundPrefix(SerializationError):
277
283
  super().__init__(UNBOUND_PREFIX_MESSAGE_TEMPLATE.format(prefix=prefix))
278
284
 
279
285
 
280
- SERIALIZATION_NOT_POSSIBLE_MESSAGE = (
281
- f"Schemathesis can't serialize data to any of the defined media types: {{}} \n{SERIALIZERS_SUGGESTION_MESSAGE}"
282
- )
283
- SERIALIZATION_FOR_TYPE_IS_NOT_POSSIBLE_MESSAGE = (
284
- f"Schemathesis can't serialize data to {{}} \n{SERIALIZERS_SUGGESTION_MESSAGE}"
285
- )
286
- RECURSIVE_REFERENCE_ERROR_MESSAGE = (
287
- "Currently, Schemathesis can't generate data for this operation due to "
288
- "recursive references in the operation definition. See more information in "
289
- "this issue - https://github.com/schemathesis/schemathesis/issues/947"
290
- )
291
-
292
-
293
286
  class SerializationNotPossible(SerializationError):
294
287
  """Not possible to serialize data to specified media type(s).
295
288
 
@@ -7,7 +7,11 @@ from typing import TYPE_CHECKING, Any, Mapping
7
7
  from schemathesis.core.version import SCHEMATHESIS_VERSION
8
8
 
9
9
  if TYPE_CHECKING:
10
+ import httpx
10
11
  import requests
12
+ from werkzeug.test import TestResponse
13
+
14
+ from schemathesis.generation.overrides import Override
11
15
 
12
16
  USER_AGENT = f"schemathesis/{SCHEMATHESIS_VERSION}"
13
17
  DEFAULT_RESPONSE_TIMEOUT = 10
@@ -51,6 +55,7 @@ class Response:
51
55
  """HTTP protocol version ("1.0" or "1.1")."""
52
56
  encoding: str | None
53
57
  """Character encoding for text content, if detected."""
58
+ _override: Override | None
54
59
 
55
60
  __slots__ = (
56
61
  "status_code",
@@ -64,6 +69,7 @@ class Response:
64
69
  "http_version",
65
70
  "encoding",
66
71
  "_encoded_body",
72
+ "_override",
67
73
  )
68
74
 
69
75
  def __init__(
@@ -77,6 +83,7 @@ class Response:
77
83
  message: str = "",
78
84
  http_version: str = "1.1",
79
85
  encoding: str | None = None,
86
+ _override: Override | None = None,
80
87
  ):
81
88
  self.status_code = status_code
82
89
  self.headers = {key.lower(): value for key, value in headers.items()}
@@ -90,9 +97,24 @@ class Response:
90
97
  self.message = message
91
98
  self.http_version = http_version
92
99
  self.encoding = encoding
100
+ self._override = _override
101
+
102
+ @classmethod
103
+ def from_any(cls, response: Response | httpx.Response | requests.Response | TestResponse) -> Response:
104
+ import httpx
105
+ import requests
106
+ from werkzeug.test import TestResponse
107
+
108
+ if isinstance(response, requests.Response):
109
+ return Response.from_requests(response, verify=True)
110
+ elif isinstance(response, httpx.Response):
111
+ return Response.from_httpx(response, verify=True)
112
+ elif isinstance(response, TestResponse):
113
+ return Response.from_wsgi(response)
114
+ return response
93
115
 
94
116
  @classmethod
95
- def from_requests(cls, response: requests.Response, verify: bool) -> Response:
117
+ def from_requests(cls, response: requests.Response, verify: bool, _override: Override | None = None) -> Response:
96
118
  raw = response.raw
97
119
  raw_headers = raw.headers if raw is not None else {}
98
120
  headers = {name: response.raw.headers.getlist(name) for name in raw_headers.keys()}
@@ -109,6 +131,64 @@ class Response:
109
131
  encoding=response.encoding,
110
132
  http_version=http_version,
111
133
  verify=verify,
134
+ _override=_override,
135
+ )
136
+
137
+ @classmethod
138
+ def from_httpx(cls, response: httpx.Response, verify: bool) -> Response:
139
+ import requests
140
+
141
+ request = requests.Request(
142
+ method=response.request.method,
143
+ url=str(response.request.url),
144
+ headers=dict(response.request.headers),
145
+ params=dict(response.request.url.params),
146
+ data=response.request.content,
147
+ ).prepare()
148
+ return Response(
149
+ status_code=response.status_code,
150
+ headers={key: [value] for key, value in response.headers.items()},
151
+ content=response.content,
152
+ request=request,
153
+ elapsed=response.elapsed.total_seconds(),
154
+ message=response.reason_phrase,
155
+ encoding=response.encoding,
156
+ http_version=response.http_version,
157
+ verify=verify,
158
+ )
159
+
160
+ @classmethod
161
+ def from_wsgi(cls, response: TestResponse) -> Response:
162
+ import http.client
163
+
164
+ import requests
165
+
166
+ reason = http.client.responses.get(response.status_code, "Unknown")
167
+ data = response.get_data()
168
+ if response.response == []:
169
+ # Werkzeug <3.0 had `charset` attr, newer versions always have UTF-8
170
+ encoding = response.mimetype_params.get("charset", getattr(response, "charset", "utf-8"))
171
+ else:
172
+ encoding = None
173
+ request = requests.Request(
174
+ method=response.request.method,
175
+ url=str(response.request.url),
176
+ headers=dict(response.request.headers),
177
+ params=dict(response.request.args),
178
+ # Request body is not available
179
+ data=b"",
180
+ ).prepare()
181
+ return Response(
182
+ status_code=response.status_code,
183
+ headers={name: response.headers.getlist(name) for name in response.headers.keys()},
184
+ content=data,
185
+ request=request,
186
+ # Elapsed time is not available
187
+ elapsed=0.0,
188
+ message=reason,
189
+ encoding=encoding,
190
+ http_version="1.1",
191
+ verify=False,
112
192
  )
113
193
 
114
194
  @property
@@ -261,7 +261,7 @@ def get_runtime_error_suggestion(error_type: RuntimeErrorKind, bold: Callable[[s
261
261
  RuntimeErrorKind.SCHEMA_INVALID_REGULAR_EXPRESSION: "Ensure your regex is compatible with Python's syntax.\n"
262
262
  "For guidance, visit: https://docs.python.org/3/library/re.html",
263
263
  RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Define a custom strategy for it.\n"
264
- "For guidance, visit: https://schemathesis.readthedocs.io/en/stable/graphql.html#custom-scalars",
264
+ "For guidance, visit: https://schemathesis.readthedocs.io/en/stable/guides/graphql-custom-scalars/",
265
265
  RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE: _format_health_check_suggestion("data_too_large"),
266
266
  RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH: _format_health_check_suggestion("filter_too_much"),
267
267
  RuntimeErrorKind.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW: _format_health_check_suggestion("too_slow"),