schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__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 (229) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +26 -68
  3. schemathesis/checks.py +130 -60
  4. schemathesis/cli/__init__.py +5 -2105
  5. schemathesis/cli/commands/__init__.py +37 -0
  6. schemathesis/cli/commands/run/__init__.py +662 -0
  7. schemathesis/cli/commands/run/checks.py +80 -0
  8. schemathesis/cli/commands/run/context.py +117 -0
  9. schemathesis/cli/commands/run/events.py +30 -0
  10. schemathesis/cli/commands/run/executor.py +141 -0
  11. schemathesis/cli/commands/run/filters.py +202 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
  15. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1368 -0
  17. schemathesis/cli/commands/run/hypothesis.py +105 -0
  18. schemathesis/cli/commands/run/loaders.py +129 -0
  19. schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
  20. schemathesis/cli/constants.py +5 -58
  21. schemathesis/cli/core.py +17 -0
  22. schemathesis/cli/ext/fs.py +14 -0
  23. schemathesis/cli/ext/groups.py +55 -0
  24. schemathesis/cli/{options.py → ext/options.py} +37 -16
  25. schemathesis/cli/hooks.py +36 -0
  26. schemathesis/contrib/__init__.py +1 -3
  27. schemathesis/contrib/openapi/__init__.py +1 -3
  28. schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
  29. schemathesis/core/__init__.py +58 -0
  30. schemathesis/core/compat.py +25 -0
  31. schemathesis/core/control.py +2 -0
  32. schemathesis/core/curl.py +58 -0
  33. schemathesis/core/deserialization.py +65 -0
  34. schemathesis/core/errors.py +370 -0
  35. schemathesis/core/failures.py +315 -0
  36. schemathesis/core/fs.py +19 -0
  37. schemathesis/core/loaders.py +104 -0
  38. schemathesis/core/marks.py +66 -0
  39. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  40. schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
  41. schemathesis/core/output/sanitization.py +197 -0
  42. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  43. schemathesis/core/registries.py +31 -0
  44. schemathesis/core/transforms.py +113 -0
  45. schemathesis/core/transport.py +108 -0
  46. schemathesis/core/validation.py +38 -0
  47. schemathesis/core/version.py +7 -0
  48. schemathesis/engine/__init__.py +30 -0
  49. schemathesis/engine/config.py +59 -0
  50. schemathesis/engine/context.py +119 -0
  51. schemathesis/engine/control.py +36 -0
  52. schemathesis/engine/core.py +157 -0
  53. schemathesis/engine/errors.py +394 -0
  54. schemathesis/engine/events.py +243 -0
  55. schemathesis/engine/phases/__init__.py +66 -0
  56. schemathesis/{runner → engine/phases}/probes.py +49 -68
  57. schemathesis/engine/phases/stateful/__init__.py +66 -0
  58. schemathesis/engine/phases/stateful/_executor.py +301 -0
  59. schemathesis/engine/phases/stateful/context.py +85 -0
  60. schemathesis/engine/phases/unit/__init__.py +175 -0
  61. schemathesis/engine/phases/unit/_executor.py +322 -0
  62. schemathesis/engine/phases/unit/_pool.py +74 -0
  63. schemathesis/engine/recorder.py +246 -0
  64. schemathesis/errors.py +31 -0
  65. schemathesis/experimental/__init__.py +9 -40
  66. schemathesis/filters.py +7 -95
  67. schemathesis/generation/__init__.py +3 -3
  68. schemathesis/generation/case.py +190 -0
  69. schemathesis/generation/coverage.py +22 -22
  70. schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
  71. schemathesis/generation/hypothesis/builder.py +585 -0
  72. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  73. schemathesis/generation/hypothesis/given.py +66 -0
  74. schemathesis/generation/hypothesis/reporting.py +14 -0
  75. schemathesis/generation/hypothesis/strategies.py +16 -0
  76. schemathesis/generation/meta.py +115 -0
  77. schemathesis/generation/modes.py +28 -0
  78. schemathesis/generation/overrides.py +96 -0
  79. schemathesis/generation/stateful/__init__.py +20 -0
  80. schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
  81. schemathesis/generation/targets.py +69 -0
  82. schemathesis/graphql/__init__.py +15 -0
  83. schemathesis/graphql/checks.py +109 -0
  84. schemathesis/graphql/loaders.py +131 -0
  85. schemathesis/hooks.py +17 -62
  86. schemathesis/openapi/__init__.py +13 -0
  87. schemathesis/openapi/checks.py +387 -0
  88. schemathesis/openapi/generation/__init__.py +0 -0
  89. schemathesis/openapi/generation/filters.py +63 -0
  90. schemathesis/openapi/loaders.py +178 -0
  91. schemathesis/pytest/__init__.py +5 -0
  92. schemathesis/pytest/control_flow.py +7 -0
  93. schemathesis/pytest/lazy.py +273 -0
  94. schemathesis/pytest/loaders.py +12 -0
  95. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
  96. schemathesis/python/__init__.py +0 -0
  97. schemathesis/python/asgi.py +12 -0
  98. schemathesis/python/wsgi.py +12 -0
  99. schemathesis/schemas.py +456 -228
  100. schemathesis/specs/graphql/__init__.py +0 -1
  101. schemathesis/specs/graphql/_cache.py +1 -2
  102. schemathesis/specs/graphql/scalars.py +5 -3
  103. schemathesis/specs/graphql/schemas.py +122 -123
  104. schemathesis/specs/graphql/validation.py +11 -17
  105. schemathesis/specs/openapi/__init__.py +6 -1
  106. schemathesis/specs/openapi/_cache.py +1 -2
  107. schemathesis/specs/openapi/_hypothesis.py +97 -134
  108. schemathesis/specs/openapi/checks.py +238 -219
  109. schemathesis/specs/openapi/converter.py +4 -4
  110. schemathesis/specs/openapi/definitions.py +1 -1
  111. schemathesis/specs/openapi/examples.py +22 -20
  112. schemathesis/specs/openapi/expressions/__init__.py +11 -15
  113. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  114. schemathesis/specs/openapi/expressions/nodes.py +33 -32
  115. schemathesis/specs/openapi/formats.py +3 -2
  116. schemathesis/specs/openapi/links.py +123 -299
  117. schemathesis/specs/openapi/media_types.py +10 -12
  118. schemathesis/specs/openapi/negative/__init__.py +2 -1
  119. schemathesis/specs/openapi/negative/mutations.py +3 -2
  120. schemathesis/specs/openapi/parameters.py +8 -6
  121. schemathesis/specs/openapi/patterns.py +1 -1
  122. schemathesis/specs/openapi/references.py +11 -51
  123. schemathesis/specs/openapi/schemas.py +177 -191
  124. schemathesis/specs/openapi/security.py +1 -1
  125. schemathesis/specs/openapi/serialization.py +10 -6
  126. schemathesis/specs/openapi/stateful/__init__.py +97 -91
  127. schemathesis/transport/__init__.py +104 -0
  128. schemathesis/transport/asgi.py +26 -0
  129. schemathesis/transport/prepare.py +99 -0
  130. schemathesis/transport/requests.py +221 -0
  131. schemathesis/{_xml.py → transport/serialization.py} +69 -7
  132. schemathesis/transport/wsgi.py +165 -0
  133. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
  134. schemathesis-4.0.0a2.dist-info/RECORD +151 -0
  135. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
  136. schemathesis/_compat.py +0 -74
  137. schemathesis/_dependency_versions.py +0 -19
  138. schemathesis/_hypothesis.py +0 -559
  139. schemathesis/_override.py +0 -50
  140. schemathesis/_rate_limiter.py +0 -7
  141. schemathesis/cli/context.py +0 -75
  142. schemathesis/cli/debug.py +0 -27
  143. schemathesis/cli/handlers.py +0 -19
  144. schemathesis/cli/junitxml.py +0 -124
  145. schemathesis/cli/output/__init__.py +0 -1
  146. schemathesis/cli/output/default.py +0 -936
  147. schemathesis/cli/output/short.py +0 -59
  148. schemathesis/cli/reporting.py +0 -79
  149. schemathesis/cli/sanitization.py +0 -26
  150. schemathesis/code_samples.py +0 -151
  151. schemathesis/constants.py +0 -56
  152. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  153. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  154. schemathesis/contrib/unique_data.py +0 -41
  155. schemathesis/exceptions.py +0 -571
  156. schemathesis/extra/_aiohttp.py +0 -28
  157. schemathesis/extra/_flask.py +0 -13
  158. schemathesis/extra/_server.py +0 -18
  159. schemathesis/failures.py +0 -277
  160. schemathesis/fixups/__init__.py +0 -37
  161. schemathesis/fixups/fast_api.py +0 -41
  162. schemathesis/fixups/utf8_bom.py +0 -28
  163. schemathesis/generation/_methods.py +0 -44
  164. schemathesis/graphql.py +0 -3
  165. schemathesis/internal/__init__.py +0 -7
  166. schemathesis/internal/checks.py +0 -84
  167. schemathesis/internal/copy.py +0 -32
  168. schemathesis/internal/datetime.py +0 -5
  169. schemathesis/internal/deprecation.py +0 -38
  170. schemathesis/internal/diff.py +0 -15
  171. schemathesis/internal/extensions.py +0 -27
  172. schemathesis/internal/jsonschema.py +0 -36
  173. schemathesis/internal/transformation.py +0 -26
  174. schemathesis/internal/validation.py +0 -34
  175. schemathesis/lazy.py +0 -474
  176. schemathesis/loaders.py +0 -122
  177. schemathesis/models.py +0 -1341
  178. schemathesis/parameters.py +0 -90
  179. schemathesis/runner/__init__.py +0 -605
  180. schemathesis/runner/events.py +0 -389
  181. schemathesis/runner/impl/__init__.py +0 -3
  182. schemathesis/runner/impl/context.py +0 -104
  183. schemathesis/runner/impl/core.py +0 -1246
  184. schemathesis/runner/impl/solo.py +0 -80
  185. schemathesis/runner/impl/threadpool.py +0 -391
  186. schemathesis/runner/serialization.py +0 -544
  187. schemathesis/sanitization.py +0 -252
  188. schemathesis/serializers.py +0 -328
  189. schemathesis/service/__init__.py +0 -18
  190. schemathesis/service/auth.py +0 -11
  191. schemathesis/service/ci.py +0 -202
  192. schemathesis/service/client.py +0 -133
  193. schemathesis/service/constants.py +0 -38
  194. schemathesis/service/events.py +0 -61
  195. schemathesis/service/extensions.py +0 -224
  196. schemathesis/service/hosts.py +0 -111
  197. schemathesis/service/metadata.py +0 -71
  198. schemathesis/service/models.py +0 -258
  199. schemathesis/service/report.py +0 -255
  200. schemathesis/service/serialization.py +0 -173
  201. schemathesis/service/usage.py +0 -66
  202. schemathesis/specs/graphql/loaders.py +0 -364
  203. schemathesis/specs/openapi/expressions/context.py +0 -16
  204. schemathesis/specs/openapi/loaders.py +0 -708
  205. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  206. schemathesis/specs/openapi/stateful/types.py +0 -14
  207. schemathesis/specs/openapi/validation.py +0 -26
  208. schemathesis/stateful/__init__.py +0 -147
  209. schemathesis/stateful/config.py +0 -97
  210. schemathesis/stateful/context.py +0 -135
  211. schemathesis/stateful/events.py +0 -274
  212. schemathesis/stateful/runner.py +0 -309
  213. schemathesis/stateful/sink.py +0 -68
  214. schemathesis/stateful/statistic.py +0 -22
  215. schemathesis/stateful/validation.py +0 -100
  216. schemathesis/targets.py +0 -77
  217. schemathesis/transports/__init__.py +0 -359
  218. schemathesis/transports/asgi.py +0 -7
  219. schemathesis/transports/auth.py +0 -38
  220. schemathesis/transports/headers.py +0 -36
  221. schemathesis/transports/responses.py +0 -57
  222. schemathesis/types.py +0 -44
  223. schemathesis/utils.py +0 -164
  224. schemathesis-3.39.7.dist-info/RECORD +0 -160
  225. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  226. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  227. /schemathesis/{internal → core}/result.py +0 -0
  228. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
  229. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -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
- }
schemathesis/loaders.py DELETED
@@ -1,122 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- import sys
5
- from functools import lru_cache
6
- from typing import TYPE_CHECKING, Any, BinaryIO, Callable, TextIO, TypeVar
7
-
8
- from .exceptions import SchemaError, SchemaErrorType, extract_requests_exception_details
9
-
10
- if TYPE_CHECKING:
11
- import yaml
12
-
13
- from .transports.responses import GenericResponse
14
-
15
- R = TypeVar("R", bound="GenericResponse")
16
-
17
-
18
- def load_schema_from_url(loader: Callable[[], R]) -> R:
19
- import requests
20
-
21
- try:
22
- response = loader()
23
- except requests.RequestException as exc:
24
- url = exc.request.url if exc.request is not None else None
25
- if isinstance(exc, requests.exceptions.SSLError):
26
- type_ = SchemaErrorType.CONNECTION_SSL
27
- elif isinstance(exc, requests.exceptions.ConnectionError):
28
- type_ = SchemaErrorType.CONNECTION_OTHER
29
- else:
30
- type_ = SchemaErrorType.NETWORK_OTHER
31
- message, extras = extract_requests_exception_details(exc)
32
- raise SchemaError(message=message, type=type_, url=url, response=exc.response, extras=extras) from exc
33
- _raise_for_status(response)
34
- return response
35
-
36
-
37
- def _raise_for_status(response: GenericResponse) -> None:
38
- from .transports.responses import get_reason
39
-
40
- status_code = response.status_code
41
- reason = get_reason(status_code)
42
- if status_code >= 500:
43
- message = f"Failed to load schema due to server error (HTTP {status_code} {reason})"
44
- type_ = SchemaErrorType.HTTP_SERVER_ERROR
45
- elif status_code >= 400:
46
- message = f"Failed to load schema due to client error (HTTP {status_code} {reason})"
47
- if status_code == 403:
48
- type_ = SchemaErrorType.HTTP_FORBIDDEN
49
- elif status_code == 404:
50
- type_ = SchemaErrorType.HTTP_NOT_FOUND
51
- else:
52
- type_ = SchemaErrorType.HTTP_CLIENT_ERROR
53
- else:
54
- return
55
- raise SchemaError(message=message, type=type_, url=response.request.url, response=response, extras=[])
56
-
57
-
58
- def load_app(path: str) -> Any:
59
- """Import an application from a string."""
60
- path, name = ([*re.split(":(?![\\\\/])", path, maxsplit=1), ""])[:2]
61
- __import__(path)
62
- # accessing the module from sys.modules returns a proper module, while `__import__`
63
- # may return a parent module (system dependent)
64
- module = sys.modules[path]
65
- return getattr(module, name)
66
-
67
-
68
- @lru_cache
69
- def get_yaml_loader() -> type[yaml.SafeLoader]:
70
- """Create a YAML loader, that doesn't parse specific tokens into Python objects."""
71
- import yaml
72
-
73
- try:
74
- from yaml import CSafeLoader as SafeLoader
75
- except ImportError:
76
- from yaml import SafeLoader # type: ignore
77
-
78
- cls: type[yaml.SafeLoader] = type("YAMLLoader", (SafeLoader,), {})
79
- cls.yaml_implicit_resolvers = {
80
- key: [(tag, regexp) for tag, regexp in mapping if tag != "tag:yaml.org,2002:timestamp"]
81
- for key, mapping in cls.yaml_implicit_resolvers.copy().items()
82
- }
83
-
84
- # Fix pyyaml scientific notation parse bug
85
- # See PR: https://github.com/yaml/pyyaml/pull/174 for upstream fix
86
- cls.add_implicit_resolver( # type: ignore
87
- "tag:yaml.org,2002:float",
88
- re.compile(
89
- r"""^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+]?[0-9]+)?
90
- |[-+]?(?:[0-9][0-9_]*)(?:[eE][-+]?[0-9]+)
91
- |\.[0-9_]+(?:[eE][-+]?[0-9]+)?
92
- |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*
93
- |[-+]?\.(?:inf|Inf|INF)
94
- |\.(?:nan|NaN|NAN))$""",
95
- re.VERBOSE,
96
- ),
97
- list("-+0123456789."),
98
- )
99
-
100
- def construct_mapping(self: SafeLoader, node: yaml.Node, deep: bool = False) -> dict[str, Any]:
101
- if isinstance(node, yaml.MappingNode):
102
- self.flatten_mapping(node) # type: ignore
103
- mapping = {}
104
- for key_node, value_node in node.value:
105
- # If the key has a tag different from `str` - use its string value.
106
- # With this change all integer keys or YAML 1.1 boolean-ish values like "on" / "off" will not be cast to
107
- # a different type
108
- if key_node.tag != "tag:yaml.org,2002:str":
109
- key = key_node.value
110
- else:
111
- key = self.construct_object(key_node, deep) # type: ignore
112
- mapping[key] = self.construct_object(value_node, deep) # type: ignore
113
- return mapping
114
-
115
- cls.construct_mapping = construct_mapping # type: ignore
116
- return cls
117
-
118
-
119
- def load_yaml(stream: str | bytes | TextIO | BinaryIO) -> Any:
120
- import yaml
121
-
122
- return yaml.load(stream, get_yaml_loader())