schemathesis 3.39.15__py3-none-any.whl → 4.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. schemathesis/__init__.py +41 -79
  2. schemathesis/auths.py +111 -122
  3. schemathesis/checks.py +169 -60
  4. schemathesis/cli/__init__.py +15 -2117
  5. schemathesis/cli/commands/__init__.py +85 -0
  6. schemathesis/cli/commands/data.py +10 -0
  7. schemathesis/cli/commands/run/__init__.py +590 -0
  8. schemathesis/cli/commands/run/context.py +204 -0
  9. schemathesis/cli/commands/run/events.py +60 -0
  10. schemathesis/cli/commands/run/executor.py +157 -0
  11. schemathesis/cli/commands/run/filters.py +53 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
  15. schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1628 -0
  17. schemathesis/cli/commands/run/loaders.py +114 -0
  18. schemathesis/cli/commands/run/validation.py +246 -0
  19. schemathesis/cli/constants.py +5 -58
  20. schemathesis/cli/core.py +19 -0
  21. schemathesis/cli/ext/fs.py +16 -0
  22. schemathesis/cli/ext/groups.py +84 -0
  23. schemathesis/cli/{options.py → ext/options.py} +36 -34
  24. schemathesis/config/__init__.py +189 -0
  25. schemathesis/config/_auth.py +51 -0
  26. schemathesis/config/_checks.py +268 -0
  27. schemathesis/config/_diff_base.py +99 -0
  28. schemathesis/config/_env.py +21 -0
  29. schemathesis/config/_error.py +156 -0
  30. schemathesis/config/_generation.py +149 -0
  31. schemathesis/config/_health_check.py +24 -0
  32. schemathesis/config/_operations.py +327 -0
  33. schemathesis/config/_output.py +171 -0
  34. schemathesis/config/_parameters.py +19 -0
  35. schemathesis/config/_phases.py +187 -0
  36. schemathesis/config/_projects.py +527 -0
  37. schemathesis/config/_rate_limit.py +17 -0
  38. schemathesis/config/_report.py +120 -0
  39. schemathesis/config/_validator.py +9 -0
  40. schemathesis/config/_warnings.py +25 -0
  41. schemathesis/config/schema.json +885 -0
  42. schemathesis/core/__init__.py +67 -0
  43. schemathesis/core/compat.py +32 -0
  44. schemathesis/core/control.py +2 -0
  45. schemathesis/core/curl.py +58 -0
  46. schemathesis/core/deserialization.py +65 -0
  47. schemathesis/core/errors.py +459 -0
  48. schemathesis/core/failures.py +315 -0
  49. schemathesis/core/fs.py +19 -0
  50. schemathesis/core/hooks.py +20 -0
  51. schemathesis/core/loaders.py +104 -0
  52. schemathesis/core/marks.py +66 -0
  53. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  54. schemathesis/core/output/__init__.py +46 -0
  55. schemathesis/core/output/sanitization.py +54 -0
  56. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  57. schemathesis/core/registries.py +31 -0
  58. schemathesis/core/transforms.py +113 -0
  59. schemathesis/core/transport.py +223 -0
  60. schemathesis/core/validation.py +54 -0
  61. schemathesis/core/version.py +7 -0
  62. schemathesis/engine/__init__.py +28 -0
  63. schemathesis/engine/context.py +118 -0
  64. schemathesis/engine/control.py +36 -0
  65. schemathesis/engine/core.py +169 -0
  66. schemathesis/engine/errors.py +464 -0
  67. schemathesis/engine/events.py +258 -0
  68. schemathesis/engine/phases/__init__.py +88 -0
  69. schemathesis/{runner → engine/phases}/probes.py +52 -68
  70. schemathesis/engine/phases/stateful/__init__.py +68 -0
  71. schemathesis/engine/phases/stateful/_executor.py +356 -0
  72. schemathesis/engine/phases/stateful/context.py +85 -0
  73. schemathesis/engine/phases/unit/__init__.py +212 -0
  74. schemathesis/engine/phases/unit/_executor.py +416 -0
  75. schemathesis/engine/phases/unit/_pool.py +82 -0
  76. schemathesis/engine/recorder.py +247 -0
  77. schemathesis/errors.py +43 -0
  78. schemathesis/filters.py +17 -98
  79. schemathesis/generation/__init__.py +5 -33
  80. schemathesis/generation/case.py +317 -0
  81. schemathesis/generation/coverage.py +282 -175
  82. schemathesis/generation/hypothesis/__init__.py +36 -0
  83. schemathesis/generation/hypothesis/builder.py +800 -0
  84. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  85. schemathesis/generation/hypothesis/given.py +66 -0
  86. schemathesis/generation/hypothesis/reporting.py +14 -0
  87. schemathesis/generation/hypothesis/strategies.py +16 -0
  88. schemathesis/generation/meta.py +115 -0
  89. schemathesis/generation/metrics.py +93 -0
  90. schemathesis/generation/modes.py +20 -0
  91. schemathesis/generation/overrides.py +116 -0
  92. schemathesis/generation/stateful/__init__.py +37 -0
  93. schemathesis/generation/stateful/state_machine.py +278 -0
  94. schemathesis/graphql/__init__.py +15 -0
  95. schemathesis/graphql/checks.py +109 -0
  96. schemathesis/graphql/loaders.py +284 -0
  97. schemathesis/hooks.py +80 -101
  98. schemathesis/openapi/__init__.py +13 -0
  99. schemathesis/openapi/checks.py +455 -0
  100. schemathesis/openapi/generation/__init__.py +0 -0
  101. schemathesis/openapi/generation/filters.py +72 -0
  102. schemathesis/openapi/loaders.py +313 -0
  103. schemathesis/pytest/__init__.py +5 -0
  104. schemathesis/pytest/control_flow.py +7 -0
  105. schemathesis/pytest/lazy.py +281 -0
  106. schemathesis/pytest/loaders.py +36 -0
  107. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
  108. schemathesis/python/__init__.py +0 -0
  109. schemathesis/python/asgi.py +12 -0
  110. schemathesis/python/wsgi.py +12 -0
  111. schemathesis/schemas.py +537 -273
  112. schemathesis/specs/graphql/__init__.py +0 -1
  113. schemathesis/specs/graphql/_cache.py +1 -2
  114. schemathesis/specs/graphql/scalars.py +42 -6
  115. schemathesis/specs/graphql/schemas.py +141 -137
  116. schemathesis/specs/graphql/validation.py +11 -17
  117. schemathesis/specs/openapi/__init__.py +6 -1
  118. schemathesis/specs/openapi/_cache.py +1 -2
  119. schemathesis/specs/openapi/_hypothesis.py +142 -156
  120. schemathesis/specs/openapi/checks.py +368 -257
  121. schemathesis/specs/openapi/converter.py +4 -4
  122. schemathesis/specs/openapi/definitions.py +1 -1
  123. schemathesis/specs/openapi/examples.py +23 -21
  124. schemathesis/specs/openapi/expressions/__init__.py +31 -19
  125. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  126. schemathesis/specs/openapi/expressions/lexer.py +1 -1
  127. schemathesis/specs/openapi/expressions/nodes.py +36 -41
  128. schemathesis/specs/openapi/expressions/parser.py +1 -1
  129. schemathesis/specs/openapi/formats.py +35 -7
  130. schemathesis/specs/openapi/media_types.py +53 -12
  131. schemathesis/specs/openapi/negative/__init__.py +7 -4
  132. schemathesis/specs/openapi/negative/mutations.py +6 -5
  133. schemathesis/specs/openapi/parameters.py +7 -10
  134. schemathesis/specs/openapi/patterns.py +94 -31
  135. schemathesis/specs/openapi/references.py +12 -53
  136. schemathesis/specs/openapi/schemas.py +238 -308
  137. schemathesis/specs/openapi/security.py +1 -1
  138. schemathesis/specs/openapi/serialization.py +12 -6
  139. schemathesis/specs/openapi/stateful/__init__.py +268 -133
  140. schemathesis/specs/openapi/stateful/control.py +87 -0
  141. schemathesis/specs/openapi/stateful/links.py +209 -0
  142. schemathesis/transport/__init__.py +142 -0
  143. schemathesis/transport/asgi.py +26 -0
  144. schemathesis/transport/prepare.py +124 -0
  145. schemathesis/transport/requests.py +244 -0
  146. schemathesis/{_xml.py → transport/serialization.py} +69 -11
  147. schemathesis/transport/wsgi.py +171 -0
  148. schemathesis-4.0.0.dist-info/METADATA +204 -0
  149. schemathesis-4.0.0.dist-info/RECORD +164 -0
  150. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
  152. schemathesis/_compat.py +0 -74
  153. schemathesis/_dependency_versions.py +0 -19
  154. schemathesis/_hypothesis.py +0 -712
  155. schemathesis/_override.py +0 -50
  156. schemathesis/_patches.py +0 -21
  157. schemathesis/_rate_limiter.py +0 -7
  158. schemathesis/cli/callbacks.py +0 -466
  159. schemathesis/cli/cassettes.py +0 -561
  160. schemathesis/cli/context.py +0 -75
  161. schemathesis/cli/debug.py +0 -27
  162. schemathesis/cli/handlers.py +0 -19
  163. schemathesis/cli/junitxml.py +0 -124
  164. schemathesis/cli/output/__init__.py +0 -1
  165. schemathesis/cli/output/default.py +0 -920
  166. schemathesis/cli/output/short.py +0 -59
  167. schemathesis/cli/reporting.py +0 -79
  168. schemathesis/cli/sanitization.py +0 -26
  169. schemathesis/code_samples.py +0 -151
  170. schemathesis/constants.py +0 -54
  171. schemathesis/contrib/__init__.py +0 -11
  172. schemathesis/contrib/openapi/__init__.py +0 -11
  173. schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
  174. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  175. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  176. schemathesis/contrib/unique_data.py +0 -41
  177. schemathesis/exceptions.py +0 -571
  178. schemathesis/experimental/__init__.py +0 -109
  179. schemathesis/extra/_aiohttp.py +0 -28
  180. schemathesis/extra/_flask.py +0 -13
  181. schemathesis/extra/_server.py +0 -18
  182. schemathesis/failures.py +0 -284
  183. schemathesis/fixups/__init__.py +0 -37
  184. schemathesis/fixups/fast_api.py +0 -41
  185. schemathesis/fixups/utf8_bom.py +0 -28
  186. schemathesis/generation/_methods.py +0 -44
  187. schemathesis/graphql.py +0 -3
  188. schemathesis/internal/__init__.py +0 -7
  189. schemathesis/internal/checks.py +0 -86
  190. schemathesis/internal/copy.py +0 -32
  191. schemathesis/internal/datetime.py +0 -5
  192. schemathesis/internal/deprecation.py +0 -37
  193. schemathesis/internal/diff.py +0 -15
  194. schemathesis/internal/extensions.py +0 -27
  195. schemathesis/internal/jsonschema.py +0 -36
  196. schemathesis/internal/output.py +0 -68
  197. schemathesis/internal/transformation.py +0 -26
  198. schemathesis/internal/validation.py +0 -34
  199. schemathesis/lazy.py +0 -474
  200. schemathesis/loaders.py +0 -122
  201. schemathesis/models.py +0 -1341
  202. schemathesis/parameters.py +0 -90
  203. schemathesis/runner/__init__.py +0 -605
  204. schemathesis/runner/events.py +0 -389
  205. schemathesis/runner/impl/__init__.py +0 -3
  206. schemathesis/runner/impl/context.py +0 -88
  207. schemathesis/runner/impl/core.py +0 -1280
  208. schemathesis/runner/impl/solo.py +0 -80
  209. schemathesis/runner/impl/threadpool.py +0 -391
  210. schemathesis/runner/serialization.py +0 -544
  211. schemathesis/sanitization.py +0 -252
  212. schemathesis/serializers.py +0 -328
  213. schemathesis/service/__init__.py +0 -18
  214. schemathesis/service/auth.py +0 -11
  215. schemathesis/service/ci.py +0 -202
  216. schemathesis/service/client.py +0 -133
  217. schemathesis/service/constants.py +0 -38
  218. schemathesis/service/events.py +0 -61
  219. schemathesis/service/extensions.py +0 -224
  220. schemathesis/service/hosts.py +0 -111
  221. schemathesis/service/metadata.py +0 -71
  222. schemathesis/service/models.py +0 -258
  223. schemathesis/service/report.py +0 -255
  224. schemathesis/service/serialization.py +0 -173
  225. schemathesis/service/usage.py +0 -66
  226. schemathesis/specs/graphql/loaders.py +0 -364
  227. schemathesis/specs/openapi/expressions/context.py +0 -16
  228. schemathesis/specs/openapi/links.py +0 -389
  229. schemathesis/specs/openapi/loaders.py +0 -707
  230. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  231. schemathesis/specs/openapi/stateful/types.py +0 -14
  232. schemathesis/specs/openapi/validation.py +0 -26
  233. schemathesis/stateful/__init__.py +0 -147
  234. schemathesis/stateful/config.py +0 -97
  235. schemathesis/stateful/context.py +0 -135
  236. schemathesis/stateful/events.py +0 -274
  237. schemathesis/stateful/runner.py +0 -309
  238. schemathesis/stateful/sink.py +0 -68
  239. schemathesis/stateful/state_machine.py +0 -328
  240. schemathesis/stateful/statistic.py +0 -22
  241. schemathesis/stateful/validation.py +0 -100
  242. schemathesis/targets.py +0 -77
  243. schemathesis/transports/__init__.py +0 -369
  244. schemathesis/transports/asgi.py +0 -7
  245. schemathesis/transports/auth.py +0 -38
  246. schemathesis/transports/headers.py +0 -36
  247. schemathesis/transports/responses.py +0 -57
  248. schemathesis/types.py +0 -44
  249. schemathesis/utils.py +0 -164
  250. schemathesis-3.39.15.dist-info/METADATA +0 -293
  251. schemathesis-3.39.15.dist-info/RECORD +0 -160
  252. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  253. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  254. /schemathesis/{internal → core}/result.py +0 -0
  255. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -1,27 +0,0 @@
1
- import os
2
- from typing import Any, Callable
3
-
4
-
5
- class ExtensionLoadingError(ImportError):
6
- """Raised when an extension cannot be loaded."""
7
-
8
-
9
- def import_extension(path: str) -> Any:
10
- try:
11
- module, item = path.rsplit(".", 1)
12
- imported = __import__(module, fromlist=[item])
13
- return getattr(imported, item)
14
- except ValueError as exc:
15
- raise ExtensionLoadingError(f"Invalid path: {path}") from exc
16
- except (ImportError, AttributeError) as exc:
17
- raise ExtensionLoadingError(f"Could not import {path}") from exc
18
-
19
-
20
- def extensible(env_var: str) -> Callable[[Any], Any]:
21
- def decorator(item: Any) -> Any:
22
- path = os.getenv(env_var)
23
- if path is not None:
24
- return import_extension(path)
25
- return item
26
-
27
- return decorator
@@ -1,36 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any, Callable, Dict, List, Union, overload
4
-
5
- JsonValue = Union[Dict[str, Any], List, str, float, int]
6
-
7
-
8
- @overload
9
- def traverse_schema(schema: dict[str, Any], callback: Callable, *args: Any, **kwargs: Any) -> dict[str, Any]:
10
- pass
11
-
12
-
13
- @overload
14
- def traverse_schema(schema: list, callback: Callable, *args: Any, **kwargs: Any) -> list:
15
- pass
16
-
17
-
18
- @overload
19
- def traverse_schema(schema: str, callback: Callable, *args: Any, **kwargs: Any) -> str:
20
- pass
21
-
22
-
23
- @overload
24
- def traverse_schema(schema: float, callback: Callable, *args: Any, **kwargs: Any) -> float:
25
- pass
26
-
27
-
28
- def traverse_schema(schema: JsonValue, callback: Callable[..., dict[str, Any]], *args: Any, **kwargs: Any) -> JsonValue:
29
- """Apply callback recursively to the given schema."""
30
- if isinstance(schema, dict):
31
- schema = callback(schema, *args, **kwargs)
32
- for key, sub_item in schema.items():
33
- schema[key] = traverse_schema(sub_item, callback, *args, **kwargs)
34
- elif isinstance(schema, list):
35
- schema = [traverse_schema(sub_item, callback, *args, **kwargs) for sub_item in schema]
36
- return schema
@@ -1,68 +0,0 @@
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,26 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
-
5
- from ..constants import FALSE_VALUES, TRUE_VALUES
6
-
7
-
8
- def merge_recursively(a: dict[str, Any], b: dict[str, Any]) -> dict[str, Any]:
9
- """Merge two dictionaries recursively."""
10
- for key in b:
11
- if key in a:
12
- if isinstance(a[key], dict) and isinstance(b[key], dict):
13
- merge_recursively(a[key], b[key])
14
- else:
15
- a[key] = b[key]
16
- else:
17
- a[key] = b[key]
18
- return a
19
-
20
-
21
- def convert_boolean_string(value: str) -> str | bool:
22
- if value.lower() in TRUE_VALUES:
23
- return True
24
- if value.lower() in FALSE_VALUES:
25
- return False
26
- return value
@@ -1,34 +0,0 @@
1
- import pathlib
2
- import re
3
- from typing import Any
4
-
5
-
6
- def require_relative_url(url: str) -> None:
7
- """Raise an error if the URL is not relative."""
8
- from yarl import URL
9
-
10
- if URL(url).is_absolute():
11
- raise ValueError("Schema path should be relative for WSGI/ASGI loaders")
12
-
13
-
14
- def file_exists(path: str) -> bool:
15
- try:
16
- return pathlib.Path(path).is_file()
17
- except OSError:
18
- # For example, path could be too long
19
- return False
20
-
21
-
22
- def is_filename(value: str) -> bool:
23
- """Detect if the input string is a filename by checking its extension."""
24
- return bool(pathlib.Path(value).suffix)
25
-
26
-
27
- SURROGATE_PAIR_RE = re.compile(r"[\ud800-\udfff]")
28
- has_surrogate_pair = SURROGATE_PAIR_RE.search
29
-
30
-
31
- def is_illegal_surrogate(item: Any) -> bool:
32
- if isinstance(item, list):
33
- return any(isinstance(item_, str) and bool(has_surrogate_pair(item_)) for item_ in item)
34
- return isinstance(item, str) and bool(has_surrogate_pair(item))
schemathesis/lazy.py DELETED
@@ -1,474 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from contextlib import nullcontext
4
- from dataclasses import dataclass, field
5
- from inspect import signature
6
- from typing import TYPE_CHECKING, Any, Callable, Generator, Type
7
-
8
- import pytest
9
- from hypothesis.core import HypothesisHandle
10
- from hypothesis.errors import Flaky
11
- from hypothesis.internal.escalation import format_exception, get_trimmed_traceback
12
- from hypothesis.internal.reflection import impersonate
13
- from pytest_subtests import SubTests
14
-
15
- from ._compat import MultipleFailures, get_interesting_origin
16
- from ._override import CaseOverride, check_no_override_mark, get_override_from_mark, set_override_mark
17
- from .auths import AuthStorage
18
- from .code_samples import CodeSampleStyle
19
- from .constants import FLAKY_FAILURE_MESSAGE, NOT_SET
20
- from .exceptions import CheckFailed, OperationSchemaError, SkipTest, get_grouped_exception
21
- from .filters import FilterSet, FilterValue, MatcherFunc, RegexValue, filter_set_from_components, is_deprecated
22
- from .hooks import HookDispatcher, HookScope
23
- from .internal.deprecation import warn_filtration_arguments
24
- from .internal.result import Ok
25
- from .schemas import BaseSchema
26
- from .utils import (
27
- GivenInput,
28
- fail_on_no_matches,
29
- get_given_args,
30
- get_given_kwargs,
31
- given_proxy,
32
- is_given_applied,
33
- merge_given_args,
34
- validate_given_args,
35
- )
36
-
37
- if TYPE_CHECKING:
38
- from _pytest.fixtures import FixtureRequest
39
- from pyrate_limiter import Limiter
40
-
41
- from .generation import DataGenerationMethodInput, GenerationConfig
42
- from .internal.output import OutputConfig
43
- from .models import APIOperation
44
- from .types import Filter, GenericTest, NotSet
45
-
46
-
47
- @dataclass
48
- class LazySchema:
49
- fixture_name: str
50
- base_url: str | None | NotSet = NOT_SET
51
- app: Any = NOT_SET
52
- filter_set: FilterSet = field(default_factory=FilterSet)
53
- hooks: HookDispatcher = field(default_factory=lambda: HookDispatcher(scope=HookScope.SCHEMA))
54
- auth: AuthStorage = field(default_factory=AuthStorage)
55
- validate_schema: bool = True
56
- data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET
57
- generation_config: GenerationConfig | NotSet = NOT_SET
58
- output_config: OutputConfig | NotSet = NOT_SET
59
- code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
60
- rate_limiter: Limiter | None = None
61
- sanitize_output: bool = True
62
-
63
- def include(
64
- self,
65
- func: MatcherFunc | None = None,
66
- *,
67
- name: FilterValue | None = None,
68
- name_regex: str | None = None,
69
- method: FilterValue | None = None,
70
- method_regex: str | None = None,
71
- path: FilterValue | None = None,
72
- path_regex: str | None = None,
73
- tag: FilterValue | None = None,
74
- tag_regex: RegexValue | None = None,
75
- operation_id: FilterValue | None = None,
76
- operation_id_regex: RegexValue | None = None,
77
- ) -> LazySchema:
78
- """Include only operations that match the given filters."""
79
- filter_set = self.filter_set.clone()
80
- filter_set.include(
81
- func,
82
- name=name,
83
- name_regex=name_regex,
84
- method=method,
85
- method_regex=method_regex,
86
- path=path,
87
- path_regex=path_regex,
88
- tag=tag,
89
- tag_regex=tag_regex,
90
- operation_id=operation_id,
91
- operation_id_regex=operation_id_regex,
92
- )
93
- return self.__class__(
94
- fixture_name=self.fixture_name,
95
- base_url=self.base_url,
96
- app=self.app,
97
- hooks=self.hooks,
98
- auth=self.auth,
99
- validate_schema=self.validate_schema,
100
- data_generation_methods=self.data_generation_methods,
101
- generation_config=self.generation_config,
102
- output_config=self.output_config,
103
- code_sample_style=self.code_sample_style,
104
- rate_limiter=self.rate_limiter,
105
- sanitize_output=self.sanitize_output,
106
- filter_set=filter_set,
107
- )
108
-
109
- def exclude(
110
- self,
111
- func: MatcherFunc | None = None,
112
- *,
113
- name: FilterValue | None = None,
114
- name_regex: str | None = None,
115
- method: FilterValue | None = None,
116
- method_regex: str | None = None,
117
- path: FilterValue | None = None,
118
- path_regex: str | None = None,
119
- tag: FilterValue | None = None,
120
- tag_regex: RegexValue | None = None,
121
- operation_id: FilterValue | None = None,
122
- operation_id_regex: RegexValue | None = None,
123
- deprecated: bool = False,
124
- ) -> LazySchema:
125
- """Exclude operations that match the given filters."""
126
- filter_set = self.filter_set.clone()
127
- if deprecated:
128
- if func is None:
129
- func = is_deprecated
130
- else:
131
- filter_set.exclude(is_deprecated)
132
- filter_set.exclude(
133
- func,
134
- name=name,
135
- name_regex=name_regex,
136
- method=method,
137
- method_regex=method_regex,
138
- path=path,
139
- path_regex=path_regex,
140
- tag=tag,
141
- tag_regex=tag_regex,
142
- operation_id=operation_id,
143
- operation_id_regex=operation_id_regex,
144
- )
145
- return self.__class__(
146
- fixture_name=self.fixture_name,
147
- base_url=self.base_url,
148
- app=self.app,
149
- hooks=self.hooks,
150
- auth=self.auth,
151
- validate_schema=self.validate_schema,
152
- data_generation_methods=self.data_generation_methods,
153
- generation_config=self.generation_config,
154
- output_config=self.output_config,
155
- code_sample_style=self.code_sample_style,
156
- rate_limiter=self.rate_limiter,
157
- sanitize_output=self.sanitize_output,
158
- filter_set=filter_set,
159
- )
160
-
161
- def hook(self, hook: str | Callable) -> Callable:
162
- return self.hooks.register(hook)
163
-
164
- def parametrize(
165
- self,
166
- method: Filter | None = NOT_SET,
167
- endpoint: Filter | None = NOT_SET,
168
- tag: Filter | None = NOT_SET,
169
- operation_id: Filter | None = NOT_SET,
170
- validate_schema: bool | NotSet = NOT_SET,
171
- skip_deprecated_operations: bool | NotSet = NOT_SET,
172
- data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
173
- generation_config: GenerationConfig | NotSet = NOT_SET,
174
- output_config: OutputConfig | NotSet = NOT_SET,
175
- code_sample_style: str | NotSet = NOT_SET,
176
- ) -> Callable:
177
- for name in ("method", "endpoint", "tag", "operation_id", "skip_deprecated_operations"):
178
- value = locals()[name]
179
- if value is not NOT_SET:
180
- warn_filtration_arguments(name)
181
- if data_generation_methods is NOT_SET:
182
- data_generation_methods = self.data_generation_methods
183
- if generation_config is NOT_SET:
184
- generation_config = self.generation_config
185
- if output_config is NOT_SET:
186
- output_config = self.output_config
187
- if isinstance(code_sample_style, str):
188
- _code_sample_style = CodeSampleStyle.from_str(code_sample_style)
189
- else:
190
- _code_sample_style = self.code_sample_style
191
-
192
- def wrapper(test: Callable) -> Callable:
193
- if is_given_applied(test):
194
- # The user wrapped the test function with `@schema.given`
195
- # These args & kwargs go as extra to the underlying test generator
196
- given_args = get_given_args(test)
197
- given_kwargs = get_given_kwargs(test)
198
- test_function = validate_given_args(test, given_args, given_kwargs)
199
- if test_function is not None:
200
- return test_function
201
- given_kwargs = merge_given_args(test, given_args, given_kwargs)
202
- del given_args
203
- else:
204
- given_kwargs = {}
205
-
206
- def wrapped_test(request: FixtureRequest) -> None:
207
- """The actual test, which is executed by pytest."""
208
- __tracebackhide__ = True
209
- if hasattr(wrapped_test, "_schemathesis_hooks"):
210
- test._schemathesis_hooks = wrapped_test._schemathesis_hooks # type: ignore
211
- schema = get_schema(
212
- request=request,
213
- name=self.fixture_name,
214
- base_url=self.base_url,
215
- method=method,
216
- endpoint=endpoint,
217
- tag=tag,
218
- operation_id=operation_id,
219
- hooks=self.hooks,
220
- auth=self.auth if self.auth.providers is not None else NOT_SET,
221
- test_function=test,
222
- validate_schema=validate_schema,
223
- skip_deprecated_operations=skip_deprecated_operations,
224
- data_generation_methods=data_generation_methods,
225
- generation_config=generation_config,
226
- output_config=output_config,
227
- code_sample_style=_code_sample_style,
228
- app=self.app,
229
- rate_limiter=self.rate_limiter,
230
- sanitize_output=self.sanitize_output,
231
- filter_set=self.filter_set,
232
- )
233
- fixtures = get_fixtures(test, request, given_kwargs)
234
- # Changing the node id is required for better reporting - the method and path will appear there
235
- node_id = request.node._nodeid
236
- settings = getattr(wrapped_test, "_hypothesis_internal_use_settings", None)
237
-
238
- as_strategy_kwargs: Callable[[APIOperation], dict[str, Any]] | None = None
239
-
240
- override = get_override_from_mark(test)
241
- if override is not None:
242
-
243
- def as_strategy_kwargs(_operation: APIOperation) -> dict[str, Any]:
244
- nonlocal override
245
-
246
- return {
247
- location: entry for location, entry in override.for_operation(_operation).items() if entry
248
- }
249
-
250
- tests = list(
251
- schema.get_all_tests(
252
- test,
253
- settings,
254
- hooks=self.hooks,
255
- as_strategy_kwargs=as_strategy_kwargs,
256
- _given_kwargs=given_kwargs,
257
- )
258
- )
259
- if not tests:
260
- fail_on_no_matches(node_id)
261
- request.session.testscollected += len(tests)
262
- suspend_capture_ctx = _get_capturemanager(request)
263
- subtests = SubTests(request.node.ihook, suspend_capture_ctx, request)
264
- for result in tests:
265
- if isinstance(result, Ok):
266
- operation, sub_test = result.ok()
267
- subtests.item._nodeid = _get_node_name(node_id, operation)
268
- run_subtest(operation, fixtures, sub_test, subtests)
269
- else:
270
- _schema_error(subtests, result.err(), node_id)
271
- subtests.item._nodeid = node_id
272
-
273
- wrapped_test = pytest.mark.usefixtures(self.fixture_name)(wrapped_test)
274
- _copy_marks(test, wrapped_test)
275
-
276
- # Needed to prevent a failure when settings are applied to the test function
277
- wrapped_test.is_hypothesis_test = True # type: ignore
278
- wrapped_test.hypothesis = HypothesisHandle(test, wrapped_test, given_kwargs) # type: ignore
279
-
280
- return wrapped_test
281
-
282
- return wrapper
283
-
284
- def given(self, *args: GivenInput, **kwargs: GivenInput) -> Callable:
285
- return given_proxy(*args, **kwargs)
286
-
287
- def override(
288
- self,
289
- *,
290
- query: dict[str, str] | None = None,
291
- headers: dict[str, str] | None = None,
292
- cookies: dict[str, str] | None = None,
293
- path_parameters: dict[str, str] | None = None,
294
- ) -> Callable[[GenericTest], GenericTest]:
295
- """Override Open API parameters with fixed values."""
296
-
297
- def _add_override(test: GenericTest) -> GenericTest:
298
- check_no_override_mark(test)
299
- override = CaseOverride(
300
- query=query or {}, headers=headers or {}, cookies=cookies or {}, path_parameters=path_parameters or {}
301
- )
302
- set_override_mark(test, override)
303
- return test
304
-
305
- return _add_override
306
-
307
-
308
- def _copy_marks(source: Callable, target: Callable) -> None:
309
- marks = getattr(source, "pytestmark", [])
310
- # Pytest adds this attribute in `usefixtures`
311
- target.pytestmark.extend(marks) # type: ignore
312
-
313
-
314
- def _get_capturemanager(request: FixtureRequest) -> Generator | Type[nullcontext]:
315
- capturemanager = request.node.config.pluginmanager.get_plugin("capturemanager")
316
- if capturemanager is not None:
317
- return capturemanager.global_and_fixture_disabled
318
- return nullcontext
319
-
320
-
321
- def _get_node_name(node_id: str, operation: APIOperation) -> str:
322
- """Make a test node name. For example: test_api[GET /users]."""
323
- return f"{node_id}[{operation.method.upper()} {operation.full_path}]"
324
-
325
-
326
- def _get_partial_node_name(node_id: str, **kwargs: Any) -> str:
327
- """Make a test node name for failing tests caused by schema errors."""
328
- name = node_id
329
- if "method" in kwargs:
330
- name += f"[{kwargs['method']} {kwargs['path']}]"
331
- else:
332
- name += f"[{kwargs['path']}]"
333
- return name
334
-
335
-
336
- def run_subtest(
337
- operation: APIOperation,
338
- fixtures: dict[str, Any],
339
- sub_test: Callable,
340
- subtests: SubTests,
341
- ) -> None:
342
- """Run the given subtest with pytest fixtures."""
343
- __tracebackhide__ = True
344
-
345
- # Deduplicate found checks in case of Hypothesis finding multiple of them
346
- failed_checks = {}
347
- exceptions = []
348
- inner_test = sub_test.hypothesis.inner_test # type: ignore
349
-
350
- @impersonate(inner_test) # type: ignore
351
- def collecting_wrapper(*args: Any, **kwargs: Any) -> None:
352
- __tracebackhide__ = True
353
- try:
354
- inner_test(*args, **kwargs)
355
- except CheckFailed as failed:
356
- failed_checks[failed.__class__] = failed
357
- raise failed
358
- except Exception as exception:
359
- # Deduplicate it later, as it is more costly than for `CheckFailed`
360
- exceptions.append(exception)
361
- raise
362
-
363
- def get_exception_class() -> type[CheckFailed]:
364
- return get_grouped_exception("Lazy", *failed_checks.values())
365
-
366
- sub_test.hypothesis.inner_test = collecting_wrapper # type: ignore
367
-
368
- with subtests.test(verbose_name=operation.verbose_name):
369
- try:
370
- sub_test(**fixtures)
371
- except SkipTest as exc:
372
- pytest.skip(exc.args[0])
373
- except (MultipleFailures, CheckFailed) as exc:
374
- # Hypothesis doesn't report the underlying failures in these circumstances, hence we display them manually
375
- exc_class = get_exception_class()
376
- failures = "".join(f"{SEPARATOR} {failure.args[0]}" for failure in failed_checks.values())
377
- unique_exceptions = {get_interesting_origin(exception): exception for exception in exceptions}
378
- total_problems = len(failed_checks) + len(unique_exceptions)
379
- if total_problems == 1:
380
- raise
381
- message = f"Schemathesis found {total_problems} distinct sets of failures.{failures}"
382
- for exception in unique_exceptions.values():
383
- # Non-check exceptions
384
- message += f"{SEPARATOR}\n\n"
385
- tb = get_trimmed_traceback(exception)
386
- message += format_exception(exception, tb)
387
- raise exc_class(message, causes=tuple(failed_checks.values())).with_traceback(exc.__traceback__) from None
388
- except Flaky as exc:
389
- exc_class = get_exception_class()
390
- failure = next(iter(failed_checks.values()))
391
- message = f"{FLAKY_FAILURE_MESSAGE}{failure}"
392
- # The outer frame is the one for user's test function, take it as the root one
393
- traceback = exc.__traceback__.tb_next
394
- # The next one comes from Hypothesis internals - remove it
395
- traceback.tb_next = None
396
- raise exc_class(message, causes=tuple(failed_checks.values())).with_traceback(traceback) from None
397
-
398
-
399
- SEPARATOR = "\n===================="
400
-
401
-
402
- def _schema_error(subtests: SubTests, error: OperationSchemaError, node_id: str) -> None:
403
- """Run a failing test, that will show the underlying problem."""
404
- sub_test = error.as_failing_test_function()
405
- # `full_path` is always available in this case
406
- kwargs = {"path": error.full_path}
407
- if error.method:
408
- kwargs["method"] = error.method.upper()
409
- subtests.item._nodeid = _get_partial_node_name(node_id, **kwargs)
410
- __tracebackhide__ = True
411
- with subtests.test(**kwargs):
412
- sub_test()
413
-
414
-
415
- def get_schema(
416
- *,
417
- request: FixtureRequest,
418
- name: str,
419
- base_url: str | None | NotSet = None,
420
- method: Filter | None = None,
421
- endpoint: Filter | None = None,
422
- tag: Filter | None = None,
423
- operation_id: Filter | None = None,
424
- filter_set: FilterSet,
425
- app: Any = None,
426
- test_function: GenericTest,
427
- hooks: HookDispatcher,
428
- auth: AuthStorage | NotSet,
429
- validate_schema: bool | NotSet = NOT_SET,
430
- skip_deprecated_operations: bool | NotSet = NOT_SET,
431
- data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
432
- generation_config: GenerationConfig | NotSet = NOT_SET,
433
- output_config: OutputConfig | NotSet = NOT_SET,
434
- code_sample_style: CodeSampleStyle,
435
- rate_limiter: Limiter | None,
436
- sanitize_output: bool,
437
- ) -> BaseSchema:
438
- """Loads a schema from the fixture."""
439
- schema = request.getfixturevalue(name)
440
- if not isinstance(schema, BaseSchema):
441
- raise ValueError(f"The given schema must be an instance of BaseSchema, got: {type(schema)}")
442
-
443
- filter_set = filter_set_from_components(
444
- include=True,
445
- method=method,
446
- endpoint=endpoint,
447
- tag=tag,
448
- operation_id=operation_id,
449
- skip_deprecated_operations=skip_deprecated_operations,
450
- parent=schema.filter_set.merge(filter_set),
451
- )
452
- return schema.clone(
453
- base_url=base_url,
454
- filter_set=filter_set,
455
- app=app,
456
- test_function=test_function,
457
- hooks=schema.hooks.merge(hooks),
458
- auth=auth,
459
- validate_schema=validate_schema,
460
- data_generation_methods=data_generation_methods,
461
- generation_config=generation_config,
462
- output_config=output_config,
463
- code_sample_style=code_sample_style,
464
- rate_limiter=rate_limiter,
465
- sanitize_output=sanitize_output,
466
- )
467
-
468
-
469
- def get_fixtures(func: Callable, request: FixtureRequest, given_kwargs: dict[str, Any]) -> dict[str, Any]:
470
- """Load fixtures, needed for the test function."""
471
- sig = signature(func)
472
- return {
473
- name: request.getfixturevalue(name) for name in sig.parameters if name != "case" and name not in given_kwargs
474
- }