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
@@ -0,0 +1,273 @@
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 pytest_subtests import SubTests
11
+
12
+ from schemathesis.core.errors import InvalidSchema
13
+ from schemathesis.core.result import Ok
14
+ from schemathesis.filters import FilterSet, FilterValue, MatcherFunc, RegexValue, is_deprecated
15
+ from schemathesis.generation.hypothesis.builder import get_all_tests
16
+ from schemathesis.generation.hypothesis.given import (
17
+ GivenArgsMark,
18
+ GivenInput,
19
+ GivenKwargsMark,
20
+ given_proxy,
21
+ is_given_applied,
22
+ merge_given_args,
23
+ validate_given_args,
24
+ )
25
+ from schemathesis.generation.overrides import Override, OverrideMark, check_no_override_mark
26
+ from schemathesis.pytest.control_flow import fail_on_no_matches
27
+ from schemathesis.schemas import BaseSchema
28
+
29
+ if TYPE_CHECKING:
30
+ from _pytest.fixtures import FixtureRequest
31
+
32
+ from schemathesis.schemas import APIOperation
33
+
34
+
35
+ @dataclass
36
+ class LazySchema:
37
+ fixture_name: str
38
+ filter_set: FilterSet = field(default_factory=FilterSet)
39
+
40
+ def include(
41
+ self,
42
+ func: MatcherFunc | None = None,
43
+ *,
44
+ name: FilterValue | None = None,
45
+ name_regex: str | None = None,
46
+ method: FilterValue | None = None,
47
+ method_regex: str | None = None,
48
+ path: FilterValue | None = None,
49
+ path_regex: str | None = None,
50
+ tag: FilterValue | None = None,
51
+ tag_regex: RegexValue | None = None,
52
+ operation_id: FilterValue | None = None,
53
+ operation_id_regex: RegexValue | None = None,
54
+ ) -> LazySchema:
55
+ """Include only operations that match the given filters."""
56
+ filter_set = self.filter_set.clone()
57
+ filter_set.include(
58
+ func,
59
+ name=name,
60
+ name_regex=name_regex,
61
+ method=method,
62
+ method_regex=method_regex,
63
+ path=path,
64
+ path_regex=path_regex,
65
+ tag=tag,
66
+ tag_regex=tag_regex,
67
+ operation_id=operation_id,
68
+ operation_id_regex=operation_id_regex,
69
+ )
70
+ return self.__class__(fixture_name=self.fixture_name, filter_set=filter_set)
71
+
72
+ def exclude(
73
+ self,
74
+ func: MatcherFunc | None = None,
75
+ *,
76
+ name: FilterValue | None = None,
77
+ name_regex: str | None = None,
78
+ method: FilterValue | None = None,
79
+ method_regex: str | None = None,
80
+ path: FilterValue | None = None,
81
+ path_regex: str | None = None,
82
+ tag: FilterValue | None = None,
83
+ tag_regex: RegexValue | None = None,
84
+ operation_id: FilterValue | None = None,
85
+ operation_id_regex: RegexValue | None = None,
86
+ deprecated: bool = False,
87
+ ) -> LazySchema:
88
+ """Exclude operations that match the given filters."""
89
+ filter_set = self.filter_set.clone()
90
+ if deprecated:
91
+ if func is None:
92
+ func = is_deprecated
93
+ else:
94
+ filter_set.exclude(is_deprecated)
95
+ filter_set.exclude(
96
+ func,
97
+ name=name,
98
+ name_regex=name_regex,
99
+ method=method,
100
+ method_regex=method_regex,
101
+ path=path,
102
+ path_regex=path_regex,
103
+ tag=tag,
104
+ tag_regex=tag_regex,
105
+ operation_id=operation_id,
106
+ operation_id_regex=operation_id_regex,
107
+ )
108
+ return self.__class__(fixture_name=self.fixture_name, filter_set=filter_set)
109
+
110
+ def parametrize(self) -> Callable:
111
+ def wrapper(test_func: Callable) -> Callable:
112
+ if is_given_applied(test_func):
113
+ # The user wrapped the test function with `@schema.given`
114
+ # These args & kwargs go as extra to the underlying test generator
115
+ given_args = GivenArgsMark.get(test_func)
116
+ given_kwargs = GivenKwargsMark.get(test_func)
117
+ assert given_args is not None
118
+ assert given_kwargs is not None
119
+ test_function = validate_given_args(test_func, given_args, given_kwargs)
120
+ if test_function is not None:
121
+ return test_function
122
+ given_kwargs = merge_given_args(test_func, given_args, given_kwargs)
123
+ del given_args
124
+ else:
125
+ given_kwargs = {}
126
+
127
+ def wrapped_test(request: FixtureRequest) -> None:
128
+ """The actual test, which is executed by pytest."""
129
+ __tracebackhide__ = True
130
+ schema = get_schema(
131
+ request=request,
132
+ name=self.fixture_name,
133
+ test_function=test_func,
134
+ filter_set=self.filter_set,
135
+ )
136
+ fixtures = get_fixtures(test_func, request, given_kwargs)
137
+ # Changing the node id is required for better reporting - the method and path will appear there
138
+ node_id = request.node._nodeid
139
+ settings = getattr(wrapped_test, "_hypothesis_internal_use_settings", None)
140
+
141
+ as_strategy_kwargs: Callable[[APIOperation], dict[str, Any]] | None = None
142
+
143
+ override = OverrideMark.get(test_func)
144
+ if override is not None:
145
+
146
+ def as_strategy_kwargs(_operation: APIOperation) -> dict[str, Any]:
147
+ nonlocal override
148
+
149
+ return {
150
+ location: entry for location, entry in override.for_operation(_operation).items() if entry
151
+ }
152
+
153
+ tests = list(
154
+ get_all_tests(
155
+ schema=schema,
156
+ test_func=test_func,
157
+ settings=settings,
158
+ generation_config=schema.generation_config,
159
+ as_strategy_kwargs=as_strategy_kwargs,
160
+ given_kwargs=given_kwargs,
161
+ )
162
+ )
163
+ if not tests:
164
+ fail_on_no_matches(node_id)
165
+ request.session.testscollected += len(tests)
166
+ suspend_capture_ctx = _get_capturemanager(request)
167
+ subtests = SubTests(request.node.ihook, suspend_capture_ctx, request)
168
+ for result in tests:
169
+ if isinstance(result, Ok):
170
+ operation, sub_test = result.ok()
171
+ subtests.item._nodeid = f"{node_id}[{operation.method.upper()} {operation.full_path}]"
172
+ run_subtest(operation, fixtures, sub_test, subtests)
173
+ else:
174
+ _schema_error(subtests, result.err(), node_id)
175
+ subtests.item._nodeid = node_id
176
+
177
+ wrapped_test = pytest.mark.usefixtures(self.fixture_name)(wrapped_test)
178
+ _copy_marks(test_func, wrapped_test)
179
+
180
+ # Needed to prevent a failure when settings are applied to the test function
181
+ wrapped_test.is_hypothesis_test = True # type: ignore
182
+ wrapped_test.hypothesis = HypothesisHandle(test_func, wrapped_test, given_kwargs) # type: ignore
183
+
184
+ return wrapped_test
185
+
186
+ return wrapper
187
+
188
+ def given(self, *args: GivenInput, **kwargs: GivenInput) -> Callable:
189
+ return given_proxy(*args, **kwargs)
190
+
191
+ def override(
192
+ self,
193
+ *,
194
+ query: dict[str, str] | None = None,
195
+ headers: dict[str, str] | None = None,
196
+ cookies: dict[str, str] | None = None,
197
+ path_parameters: dict[str, str] | None = None,
198
+ ) -> Callable[[Callable], Callable]:
199
+ """Override Open API parameters with fixed values."""
200
+
201
+ def _add_override(test: Callable) -> Callable:
202
+ check_no_override_mark(test)
203
+ override = Override(
204
+ query=query or {}, headers=headers or {}, cookies=cookies or {}, path_parameters=path_parameters or {}
205
+ )
206
+ OverrideMark.set(test, override)
207
+ return test
208
+
209
+ return _add_override
210
+
211
+
212
+ def _copy_marks(source: Callable, target: Callable) -> None:
213
+ marks = getattr(source, "pytestmark", [])
214
+ # Pytest adds this attribute in `usefixtures`
215
+ target.pytestmark.extend(marks) # type: ignore
216
+
217
+
218
+ def _get_capturemanager(request: FixtureRequest) -> Generator | Type[nullcontext]:
219
+ capturemanager = request.node.config.pluginmanager.get_plugin("capturemanager")
220
+ if capturemanager is not None:
221
+ return capturemanager.global_and_fixture_disabled
222
+ return nullcontext
223
+
224
+
225
+ def run_subtest(operation: APIOperation, fixtures: dict[str, Any], sub_test: Callable, subtests: SubTests) -> None:
226
+ """Run the given subtest with pytest fixtures."""
227
+ __tracebackhide__ = True
228
+
229
+ with subtests.test(label=operation.label):
230
+ sub_test(**fixtures)
231
+
232
+
233
+ SEPARATOR = "\n===================="
234
+
235
+
236
+ def _schema_error(subtests: SubTests, error: InvalidSchema, node_id: str) -> None:
237
+ """Run a failing test, that will show the underlying problem."""
238
+ sub_test = error.as_failing_test_function()
239
+ # `full_path` is always available in this case
240
+ kwargs = {"path": error.full_path}
241
+ if error.method:
242
+ kwargs["method"] = error.method.upper()
243
+ subtests.item._nodeid = _get_partial_node_name(node_id, **kwargs)
244
+ __tracebackhide__ = True
245
+ with subtests.test(**kwargs):
246
+ sub_test()
247
+
248
+
249
+ def _get_partial_node_name(node_id: str, **kwargs: Any) -> str:
250
+ """Make a test node name for failing tests caused by schema errors."""
251
+ name = node_id
252
+ if "method" in kwargs:
253
+ name += f"[{kwargs['method']} {kwargs['path']}]"
254
+ else:
255
+ name += f"[{kwargs['path']}]"
256
+ return name
257
+
258
+
259
+ def get_schema(*, request: FixtureRequest, name: str, filter_set: FilterSet, test_function: Callable) -> BaseSchema:
260
+ """Loads a schema from the fixture."""
261
+ schema = request.getfixturevalue(name)
262
+ if not isinstance(schema, BaseSchema):
263
+ raise ValueError(f"The given schema must be an instance of BaseSchema, got: {type(schema)}")
264
+
265
+ return schema.clone(filter_set=filter_set, test_function=test_function)
266
+
267
+
268
+ def get_fixtures(func: Callable, request: FixtureRequest, given_kwargs: dict[str, Any]) -> dict[str, Any]:
269
+ """Load fixtures, needed for the test function."""
270
+ sig = signature(func)
271
+ return {
272
+ name: request.getfixturevalue(name) for name in sig.parameters if name != "case" and name not in given_kwargs
273
+ }
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from schemathesis.pytest.lazy import LazySchema
7
+
8
+
9
+ def from_fixture(name: str) -> LazySchema:
10
+ from schemathesis.pytest.lazy import LazySchema
11
+
12
+ return LazySchema(name)
@@ -1,49 +1,59 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
3
4
  import unittest
4
- from contextlib import contextmanager
5
5
  from functools import partial
6
6
  from typing import TYPE_CHECKING, Any, Callable, Generator, Type, cast
7
7
 
8
8
  import pytest
9
- from _pytest import fixtures, nodes
9
+ from _pytest import nodes
10
10
  from _pytest.config import hookimpl
11
11
  from _pytest.python import Class, Function, FunctionDefinition, Metafunc, Module, PyCollector
12
- from hypothesis import reporting
13
12
  from hypothesis.errors import InvalidArgument, Unsatisfiable
14
13
  from jsonschema.exceptions import SchemaError
15
14
 
16
- from .._dependency_versions import IS_PYTEST_ABOVE_7, IS_PYTEST_ABOVE_8
17
- from .._override import get_override_from_mark
18
- from ..constants import (
19
- GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE,
15
+ from schemathesis.core.control import SkipTest
16
+ from schemathesis.core.errors import (
20
17
  RECURSIVE_REFERENCE_ERROR_MESSAGE,
21
18
  SERIALIZERS_SUGGESTION_MESSAGE,
22
- )
23
- from ..exceptions import (
19
+ IncorrectUsage,
24
20
  InvalidHeadersExample,
25
- InvalidRegularExpression,
26
- OperationSchemaError,
21
+ InvalidRegexPattern,
22
+ InvalidSchema,
27
23
  SerializationNotPossible,
28
- SkipTest,
29
- UsageError,
30
24
  )
31
- from ..internal.result import Ok, Result
32
- from ..utils import (
33
- PARAMETRIZE_MARKER,
34
- fail_on_no_matches,
35
- get_given_args,
36
- get_given_kwargs,
25
+ from schemathesis.core.marks import Mark
26
+ from schemathesis.core.result import Ok, Result
27
+ from schemathesis.generation.hypothesis.given import (
28
+ GivenArgsMark,
29
+ GivenKwargsMark,
37
30
  is_given_applied,
38
- is_schemathesis_test,
39
31
  merge_given_args,
40
32
  validate_given_args,
41
33
  )
34
+ from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
35
+ from schemathesis.generation.overrides import OverrideMark
36
+ from schemathesis.pytest.control_flow import fail_on_no_matches
37
+ from schemathesis.schemas import APIOperation
42
38
 
43
39
  if TYPE_CHECKING:
44
40
  from _pytest.fixtures import FuncFixtureInfo
45
41
 
46
- from ..models import APIOperation
42
+ from schemathesis.schemas import BaseSchema
43
+
44
+ GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE = (
45
+ "Unsupported test setup. Tests using `@schema.given` cannot be combined with explicit schema examples in the same "
46
+ "function. Separate these tests into distinct functions to avoid conflicts."
47
+ )
48
+
49
+
50
+ def _is_schema(value: object) -> bool:
51
+ from schemathesis.schemas import BaseSchema
52
+
53
+ return isinstance(value, BaseSchema)
54
+
55
+
56
+ SchemaHandleMark = Mark["BaseSchema"](attr_name="schema", check=_is_schema)
47
57
 
48
58
 
49
59
  class SchemathesisFunction(Function):
@@ -58,21 +68,15 @@ class SchemathesisFunction(Function):
58
68
  self.test_function = test_func
59
69
  self.test_name = test_name
60
70
 
61
- if not IS_PYTEST_ABOVE_7:
62
- # On pytest 7, `self.obj` is already `partial`
63
- def _getobj(self) -> partial:
64
- """Tests defined as methods require `self` as the first argument.
65
-
66
- This method is called only for this case.
67
- """
68
- return partial(self.obj, self.parent.obj) # type: ignore
69
-
70
71
 
71
72
  class SchemathesisCase(PyCollector):
72
- def __init__(self, test_function: Callable, *args: Any, **kwargs: Any) -> None:
73
- self.given_kwargs: dict[str, Any] | None
74
- given_args = get_given_args(test_function)
75
- given_kwargs = get_given_kwargs(test_function)
73
+ def __init__(self, test_function: Callable, schema: BaseSchema, *args: Any, **kwargs: Any) -> None:
74
+ self.given_kwargs: dict[str, Any]
75
+ given_args = GivenArgsMark.get(test_function)
76
+ given_kwargs = GivenKwargsMark.get(test_function)
77
+
78
+ assert given_args is not None
79
+ assert given_kwargs is not None
76
80
 
77
81
  def _init_with_valid_test(_test_function: Callable, _args: tuple, _kwargs: dict[str, Any]) -> None:
78
82
  self.test_function = _test_function
@@ -84,20 +88,18 @@ class SchemathesisCase(PyCollector):
84
88
  if failing_test is not None:
85
89
  self.test_function = failing_test
86
90
  self.is_invalid_test = True
87
- self.given_kwargs = None
91
+ self.given_kwargs = {}
88
92
  else:
89
93
  _init_with_valid_test(test_function, given_args, given_kwargs)
90
94
  else:
91
95
  _init_with_valid_test(test_function, given_args, given_kwargs)
92
- self.schemathesis_case = getattr(test_function, PARAMETRIZE_MARKER)
96
+ self.schema = schema
93
97
  super().__init__(*args, **kwargs)
94
98
 
95
99
  def _get_test_name(self, operation: APIOperation) -> str:
96
- return f"{self.name}[{operation.verbose_name}]"
100
+ return f"{self.name}[{operation.label}]"
97
101
 
98
- def _gen_items(
99
- self, result: Result[APIOperation, OperationSchemaError]
100
- ) -> Generator[SchemathesisFunction, None, None]:
102
+ def _gen_items(self, result: Result[APIOperation, InvalidSchema]) -> Generator[SchemathesisFunction, None, None]:
101
103
  """Generate all tests for the given API operation.
102
104
 
103
105
  Could produce more than one test item if
@@ -106,7 +108,7 @@ class SchemathesisCase(PyCollector):
106
108
  This implementation is based on the original one in pytest, but with slight adjustments
107
109
  to produce tests out of hypothesis ones.
108
110
  """
109
- from .._hypothesis import create_test
111
+ from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, create_test
110
112
 
111
113
  is_trio_test = False
112
114
  for mark in getattr(self.test_function, "pytestmark", []):
@@ -119,24 +121,29 @@ class SchemathesisCase(PyCollector):
119
121
  if self.is_invalid_test:
120
122
  funcobj = self.test_function
121
123
  else:
122
- override = get_override_from_mark(self.test_function)
123
- as_strategy_kwargs: dict | None
124
+ override = OverrideMark.get(self.test_function)
124
125
  if override is not None:
125
126
  as_strategy_kwargs = {}
126
127
  for location, entry in override.for_operation(operation).items():
127
128
  if entry:
128
129
  as_strategy_kwargs[location] = entry
129
130
  else:
130
- as_strategy_kwargs = None
131
+ as_strategy_kwargs = {}
131
132
  funcobj = create_test(
132
133
  operation=operation,
133
- test=self.test_function,
134
- _given_kwargs=self.given_kwargs,
135
- data_generation_methods=self.schemathesis_case.data_generation_methods,
136
- generation_config=self.schemathesis_case.generation_config,
137
- as_strategy_kwargs=as_strategy_kwargs,
138
- keep_async_fn=is_trio_test,
134
+ test_func=self.test_function,
135
+ config=HypothesisTestConfig(
136
+ given_kwargs=self.given_kwargs,
137
+ generation=self.schema.generation_config,
138
+ as_strategy_kwargs=as_strategy_kwargs,
139
+ ),
139
140
  )
141
+ if asyncio.iscoroutinefunction(self.test_function):
142
+ # `pytest-trio` expects a coroutine function
143
+ if is_trio_test:
144
+ funcobj.hypothesis.inner_test = self.test_function # type: ignore
145
+ else:
146
+ funcobj.hypothesis.inner_test = make_async_test(self.test_function) # type: ignore
140
147
  name = self._get_test_name(operation)
141
148
  else:
142
149
  error = result.err()
@@ -171,8 +178,6 @@ class SchemathesisCase(PyCollector):
171
178
  originalname=self.name,
172
179
  )
173
180
  else:
174
- if not IS_PYTEST_ABOVE_8:
175
- fixtures.add_funcarg_pseudo_fixture_def(self.parent, metafunc, fixturemanager) # type: ignore[arg-type]
176
181
  fixtureinfo.prune_dependency_tree()
177
182
  for callspec in metafunc._calls:
178
183
  subname = f"{name}[{callspec.id}]"
@@ -194,11 +199,8 @@ class SchemathesisCase(PyCollector):
194
199
  def _parametrize(self, cls: type | None, definition: FunctionDefinition, fixtureinfo: FuncFixtureInfo) -> Metafunc:
195
200
  parent = self.getparent(Module)
196
201
  module = parent.obj if parent is not None else parent
197
- kwargs = {"cls": cls, "module": module}
198
- if IS_PYTEST_ABOVE_7:
199
- # Avoiding `Metafunc` is quite problematic for now, as there are quite a lot of internals we rely on
200
- kwargs["_ispytest"] = True
201
- metafunc = Metafunc(definition, fixtureinfo, self.config, **kwargs)
202
+ # Avoiding `Metafunc` is quite problematic for now, as there are quite a lot of internals we rely on
203
+ metafunc = Metafunc(definition, fixtureinfo, self.config, cls=cls, module=module, _ispytest=True)
202
204
  methods = []
203
205
  if module is not None and hasattr(module, "pytest_generate_tests"):
204
206
  methods.append(module.pytest_generate_tests)
@@ -211,13 +213,7 @@ class SchemathesisCase(PyCollector):
211
213
  def collect(self) -> list[Function]: # type: ignore
212
214
  """Generate different test items for all API operations available in the given schema."""
213
215
  try:
214
- items = [
215
- item
216
- for operation in self.schemathesis_case.get_all_operations(
217
- hooks=getattr(self.test_function, "_schemathesis_hooks", None)
218
- )
219
- for item in self._gen_items(operation)
220
- ]
216
+ items = [item for operation in self.schema.get_all_operations() for item in self._gen_items(operation)]
221
217
  if not items:
222
218
  fail_on_no_matches(self.nodeid)
223
219
  return items
@@ -225,36 +221,31 @@ class SchemathesisCase(PyCollector):
225
221
  pytest.fail("Error during collection")
226
222
 
227
223
 
228
- @hookimpl(hookwrapper=True) # type:ignore # pragma: no mutate
224
+ def make_async_test(test: Callable) -> Callable:
225
+ def async_run(*args: Any, **kwargs: Any) -> None:
226
+ try:
227
+ loop = asyncio.get_event_loop()
228
+ except RuntimeError:
229
+ loop = asyncio.new_event_loop()
230
+ coro = test(*args, **kwargs)
231
+ future = asyncio.ensure_future(coro, loop=loop)
232
+ loop.run_until_complete(future)
233
+
234
+ return async_run
235
+
236
+
237
+ @hookimpl(hookwrapper=True) # type:ignore
229
238
  def pytest_pycollect_makeitem(collector: nodes.Collector, name: str, obj: Any) -> Generator[None, Any, None]:
230
239
  """Switch to a different collector if the test is parametrized marked by schemathesis."""
231
240
  outcome = yield
232
- if is_schemathesis_test(obj):
233
- outcome.force_result(SchemathesisCase.from_parent(collector, test_function=obj, name=name))
234
- else:
241
+ try:
242
+ schema = SchemaHandleMark.get(obj)
243
+ assert schema is not None
244
+ outcome.force_result(SchemathesisCase.from_parent(collector, test_function=obj, name=name, schema=schema))
245
+ except Exception:
235
246
  outcome.get_result()
236
247
 
237
248
 
238
- IGNORED_HYPOTHESIS_OUTPUT = ("Falsifying example",)
239
-
240
-
241
- def _should_ignore_entry(value: str) -> bool:
242
- return value.startswith(IGNORED_HYPOTHESIS_OUTPUT)
243
-
244
-
245
- def hypothesis_reporter(value: str) -> None:
246
- if _should_ignore_entry(value):
247
- return
248
- reporting.default(value)
249
-
250
-
251
- @contextmanager
252
- def skip_unnecessary_hypothesis_output() -> Generator:
253
- """Avoid printing Hypothesis output that is not necessary in Schemathesis' pytest plugin."""
254
- with reporting.with_reporter(hypothesis_reporter): # type: ignore
255
- yield
256
-
257
-
258
249
  @hookimpl(wrapper=True)
259
250
  def pytest_pyfunc_call(pyfuncitem): # type:ignore
260
251
  """It is possible to have a Hypothesis exception in runtime.
@@ -263,28 +254,28 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
263
254
  """
264
255
  from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
265
256
 
266
- from .._hypothesis import (
267
- get_invalid_example_headers_mark,
268
- get_invalid_regex_mark,
269
- get_non_serializable_mark,
270
- has_unsatisfied_example_mark,
257
+ from schemathesis.generation.hypothesis.builder import (
258
+ InvalidHeadersExampleMark,
259
+ InvalidRegexMark,
260
+ NonSerializableMark,
261
+ UnsatisfiableExampleMark,
271
262
  )
272
263
 
273
264
  __tracebackhide__ = True
274
265
  if isinstance(pyfuncitem, SchemathesisFunction):
275
266
  try:
276
- with skip_unnecessary_hypothesis_output():
267
+ with ignore_hypothesis_output():
277
268
  yield
278
269
  except InvalidArgument as exc:
279
270
  if "Inconsistent args" in str(exc) and "@example()" in str(exc):
280
- raise UsageError(GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE) from None
281
- raise OperationSchemaError(exc.args[0]) from None
271
+ raise IncorrectUsage(GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE) from None
272
+ raise InvalidSchema(exc.args[0]) from None
282
273
  except HypothesisRefResolutionError:
283
274
  pytest.skip(RECURSIVE_REFERENCE_ERROR_MESSAGE)
284
275
  except (SkipTest, unittest.SkipTest) as exc:
285
- if has_unsatisfied_example_mark(pyfuncitem.obj):
276
+ if UnsatisfiableExampleMark.is_set(pyfuncitem.obj):
286
277
  raise Unsatisfiable("Failed to generate test cases from examples for this API operation") from None
287
- non_serializable = get_non_serializable_mark(pyfuncitem.obj)
278
+ non_serializable = NonSerializableMark.get(pyfuncitem.obj)
288
279
  if non_serializable is not None:
289
280
  media_types = ", ".join(non_serializable.media_types)
290
281
  raise SerializationNotPossible(
@@ -292,20 +283,16 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
292
283
  f" unsupported payload media types: {media_types}\n{SERIALIZERS_SUGGESTION_MESSAGE}",
293
284
  media_types=non_serializable.media_types,
294
285
  ) from None
295
- invalid_regex = get_invalid_regex_mark(pyfuncitem.obj)
286
+ invalid_regex = InvalidRegexMark.get(pyfuncitem.obj)
296
287
  if invalid_regex is not None:
297
- raise InvalidRegularExpression.from_schema_error(invalid_regex, from_examples=True) from None
298
- invalid_headers = get_invalid_example_headers_mark(pyfuncitem.obj)
288
+ raise InvalidRegexPattern.from_schema_error(invalid_regex, from_examples=True) from None
289
+ invalid_headers = InvalidHeadersExampleMark.get(pyfuncitem.obj)
299
290
  if invalid_headers is not None:
300
291
  raise InvalidHeadersExample.from_headers(invalid_headers) from None
301
292
  pytest.skip(exc.args[0])
302
293
  except SchemaError as exc:
303
- raise InvalidRegularExpression.from_schema_error(exc, from_examples=False) from exc
304
- except Exception as exc:
305
- if hasattr(exc, "__notes__"):
306
- exc.__notes__ = [note for note in exc.__notes__ if not _should_ignore_entry(note)] # type: ignore
307
- raise
308
- invalid_headers = get_invalid_example_headers_mark(pyfuncitem.obj)
294
+ raise InvalidRegexPattern.from_schema_error(exc, from_examples=False) from exc
295
+ invalid_headers = InvalidHeadersExampleMark.get(pyfuncitem.obj)
309
296
  if invalid_headers is not None:
310
297
  raise InvalidHeadersExample.from_headers(invalid_headers) from None
311
298
  else:
File without changes
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ if TYPE_CHECKING:
6
+ from starlette_testclient import TestClient as ASGIClient
7
+
8
+
9
+ def get_client(app: Any) -> ASGIClient:
10
+ from starlette_testclient import TestClient as ASGIClient
11
+
12
+ return ASGIClient(app)
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ if TYPE_CHECKING:
6
+ from werkzeug import Client
7
+
8
+
9
+ def get_client(app: Any) -> Client:
10
+ from werkzeug import Client
11
+
12
+ return Client(app)