schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__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 (221) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +102 -82
  3. schemathesis/checks.py +126 -46
  4. schemathesis/cli/__init__.py +11 -1760
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +37 -0
  7. schemathesis/cli/commands/run/__init__.py +662 -0
  8. schemathesis/cli/commands/run/checks.py +80 -0
  9. schemathesis/cli/commands/run/context.py +117 -0
  10. schemathesis/cli/commands/run/events.py +35 -0
  11. schemathesis/cli/commands/run/executor.py +138 -0
  12. schemathesis/cli/commands/run/filters.py +194 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +18 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  17. schemathesis/cli/commands/run/handlers/output.py +746 -0
  18. schemathesis/cli/commands/run/hypothesis.py +105 -0
  19. schemathesis/cli/commands/run/loaders.py +129 -0
  20. schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
  21. schemathesis/cli/constants.py +5 -52
  22. schemathesis/cli/core.py +17 -0
  23. schemathesis/cli/ext/fs.py +14 -0
  24. schemathesis/cli/ext/groups.py +55 -0
  25. schemathesis/cli/{options.py → ext/options.py} +39 -10
  26. schemathesis/cli/hooks.py +36 -0
  27. schemathesis/contrib/__init__.py +1 -3
  28. schemathesis/contrib/openapi/__init__.py +1 -3
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
  30. schemathesis/core/__init__.py +58 -0
  31. schemathesis/core/compat.py +25 -0
  32. schemathesis/core/control.py +2 -0
  33. schemathesis/core/curl.py +58 -0
  34. schemathesis/core/deserialization.py +65 -0
  35. schemathesis/core/errors.py +370 -0
  36. schemathesis/core/failures.py +285 -0
  37. schemathesis/core/fs.py +19 -0
  38. schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
  39. schemathesis/core/loaders.py +104 -0
  40. schemathesis/core/marks.py +66 -0
  41. schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
  42. schemathesis/core/output/__init__.py +69 -0
  43. schemathesis/core/output/sanitization.py +197 -0
  44. schemathesis/core/rate_limit.py +60 -0
  45. schemathesis/core/registries.py +31 -0
  46. schemathesis/{internal → core}/result.py +1 -1
  47. schemathesis/core/transforms.py +113 -0
  48. schemathesis/core/transport.py +108 -0
  49. schemathesis/core/validation.py +38 -0
  50. schemathesis/core/version.py +7 -0
  51. schemathesis/engine/__init__.py +30 -0
  52. schemathesis/engine/config.py +59 -0
  53. schemathesis/engine/context.py +119 -0
  54. schemathesis/engine/control.py +36 -0
  55. schemathesis/engine/core.py +157 -0
  56. schemathesis/engine/errors.py +394 -0
  57. schemathesis/engine/events.py +337 -0
  58. schemathesis/engine/phases/__init__.py +66 -0
  59. schemathesis/{runner → engine/phases}/probes.py +50 -67
  60. schemathesis/engine/phases/stateful/__init__.py +65 -0
  61. schemathesis/engine/phases/stateful/_executor.py +326 -0
  62. schemathesis/engine/phases/stateful/context.py +85 -0
  63. schemathesis/engine/phases/unit/__init__.py +174 -0
  64. schemathesis/engine/phases/unit/_executor.py +321 -0
  65. schemathesis/engine/phases/unit/_pool.py +74 -0
  66. schemathesis/engine/recorder.py +241 -0
  67. schemathesis/errors.py +31 -0
  68. schemathesis/experimental/__init__.py +18 -14
  69. schemathesis/filters.py +103 -14
  70. schemathesis/generation/__init__.py +21 -37
  71. schemathesis/generation/case.py +190 -0
  72. schemathesis/generation/coverage.py +931 -0
  73. schemathesis/generation/hypothesis/__init__.py +30 -0
  74. schemathesis/generation/hypothesis/builder.py +585 -0
  75. schemathesis/generation/hypothesis/examples.py +50 -0
  76. schemathesis/generation/hypothesis/given.py +66 -0
  77. schemathesis/generation/hypothesis/reporting.py +14 -0
  78. schemathesis/generation/hypothesis/strategies.py +16 -0
  79. schemathesis/generation/meta.py +115 -0
  80. schemathesis/generation/modes.py +28 -0
  81. schemathesis/generation/overrides.py +96 -0
  82. schemathesis/generation/stateful/__init__.py +20 -0
  83. schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
  84. schemathesis/generation/targets.py +69 -0
  85. schemathesis/graphql/__init__.py +15 -0
  86. schemathesis/graphql/checks.py +115 -0
  87. schemathesis/graphql/loaders.py +131 -0
  88. schemathesis/hooks.py +99 -67
  89. schemathesis/openapi/__init__.py +13 -0
  90. schemathesis/openapi/checks.py +412 -0
  91. schemathesis/openapi/generation/__init__.py +0 -0
  92. schemathesis/openapi/generation/filters.py +63 -0
  93. schemathesis/openapi/loaders.py +178 -0
  94. schemathesis/pytest/__init__.py +5 -0
  95. schemathesis/pytest/control_flow.py +7 -0
  96. schemathesis/pytest/lazy.py +273 -0
  97. schemathesis/pytest/loaders.py +12 -0
  98. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
  99. schemathesis/python/__init__.py +0 -0
  100. schemathesis/python/asgi.py +12 -0
  101. schemathesis/python/wsgi.py +12 -0
  102. schemathesis/schemas.py +537 -261
  103. schemathesis/specs/graphql/__init__.py +0 -1
  104. schemathesis/specs/graphql/_cache.py +25 -0
  105. schemathesis/specs/graphql/nodes.py +1 -0
  106. schemathesis/specs/graphql/scalars.py +7 -5
  107. schemathesis/specs/graphql/schemas.py +215 -187
  108. schemathesis/specs/graphql/validation.py +11 -18
  109. schemathesis/specs/openapi/__init__.py +7 -1
  110. schemathesis/specs/openapi/_cache.py +122 -0
  111. schemathesis/specs/openapi/_hypothesis.py +146 -165
  112. schemathesis/specs/openapi/checks.py +565 -67
  113. schemathesis/specs/openapi/converter.py +33 -6
  114. schemathesis/specs/openapi/definitions.py +11 -18
  115. schemathesis/specs/openapi/examples.py +139 -23
  116. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  117. schemathesis/specs/openapi/expressions/context.py +4 -6
  118. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  119. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  120. schemathesis/specs/openapi/expressions/nodes.py +38 -14
  121. schemathesis/specs/openapi/expressions/parser.py +26 -5
  122. schemathesis/specs/openapi/formats.py +45 -0
  123. schemathesis/specs/openapi/links.py +65 -165
  124. schemathesis/specs/openapi/media_types.py +32 -0
  125. schemathesis/specs/openapi/negative/__init__.py +7 -3
  126. schemathesis/specs/openapi/negative/mutations.py +24 -8
  127. schemathesis/specs/openapi/parameters.py +46 -30
  128. schemathesis/specs/openapi/patterns.py +137 -0
  129. schemathesis/specs/openapi/references.py +47 -57
  130. schemathesis/specs/openapi/schemas.py +478 -369
  131. schemathesis/specs/openapi/security.py +25 -7
  132. schemathesis/specs/openapi/serialization.py +11 -6
  133. schemathesis/specs/openapi/stateful/__init__.py +185 -73
  134. schemathesis/specs/openapi/utils.py +6 -1
  135. schemathesis/transport/__init__.py +104 -0
  136. schemathesis/transport/asgi.py +26 -0
  137. schemathesis/transport/prepare.py +99 -0
  138. schemathesis/transport/requests.py +221 -0
  139. schemathesis/{_xml.py → transport/serialization.py} +143 -28
  140. schemathesis/transport/wsgi.py +165 -0
  141. schemathesis-4.0.0a1.dist-info/METADATA +297 -0
  142. schemathesis-4.0.0a1.dist-info/RECORD +152 -0
  143. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
  145. schemathesis/_compat.py +0 -74
  146. schemathesis/_dependency_versions.py +0 -17
  147. schemathesis/_hypothesis.py +0 -246
  148. schemathesis/_override.py +0 -49
  149. schemathesis/cli/cassettes.py +0 -375
  150. schemathesis/cli/context.py +0 -58
  151. schemathesis/cli/debug.py +0 -26
  152. schemathesis/cli/handlers.py +0 -16
  153. schemathesis/cli/junitxml.py +0 -43
  154. schemathesis/cli/output/__init__.py +0 -1
  155. schemathesis/cli/output/default.py +0 -790
  156. schemathesis/cli/output/short.py +0 -44
  157. schemathesis/cli/sanitization.py +0 -20
  158. schemathesis/code_samples.py +0 -149
  159. schemathesis/constants.py +0 -55
  160. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  161. schemathesis/contrib/openapi/formats/uuid.py +0 -15
  162. schemathesis/contrib/unique_data.py +0 -41
  163. schemathesis/exceptions.py +0 -560
  164. schemathesis/extra/_aiohttp.py +0 -27
  165. schemathesis/extra/_flask.py +0 -10
  166. schemathesis/extra/_server.py +0 -17
  167. schemathesis/failures.py +0 -209
  168. schemathesis/fixups/__init__.py +0 -36
  169. schemathesis/fixups/fast_api.py +0 -41
  170. schemathesis/fixups/utf8_bom.py +0 -29
  171. schemathesis/graphql.py +0 -4
  172. schemathesis/internal/__init__.py +0 -7
  173. schemathesis/internal/copy.py +0 -13
  174. schemathesis/internal/datetime.py +0 -5
  175. schemathesis/internal/deprecation.py +0 -34
  176. schemathesis/internal/jsonschema.py +0 -35
  177. schemathesis/internal/transformation.py +0 -15
  178. schemathesis/internal/validation.py +0 -34
  179. schemathesis/lazy.py +0 -361
  180. schemathesis/loaders.py +0 -120
  181. schemathesis/models.py +0 -1234
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -570
  184. schemathesis/runner/events.py +0 -329
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -1035
  187. schemathesis/runner/impl/solo.py +0 -90
  188. schemathesis/runner/impl/threadpool.py +0 -400
  189. schemathesis/runner/serialization.py +0 -411
  190. schemathesis/sanitization.py +0 -248
  191. schemathesis/serializers.py +0 -323
  192. schemathesis/service/__init__.py +0 -18
  193. schemathesis/service/auth.py +0 -11
  194. schemathesis/service/ci.py +0 -201
  195. schemathesis/service/client.py +0 -100
  196. schemathesis/service/constants.py +0 -38
  197. schemathesis/service/events.py +0 -57
  198. schemathesis/service/hosts.py +0 -107
  199. schemathesis/service/metadata.py +0 -46
  200. schemathesis/service/models.py +0 -49
  201. schemathesis/service/report.py +0 -255
  202. schemathesis/service/serialization.py +0 -199
  203. schemathesis/service/usage.py +0 -65
  204. schemathesis/specs/graphql/loaders.py +0 -344
  205. schemathesis/specs/openapi/filters.py +0 -49
  206. schemathesis/specs/openapi/loaders.py +0 -667
  207. schemathesis/specs/openapi/stateful/links.py +0 -92
  208. schemathesis/specs/openapi/validation.py +0 -25
  209. schemathesis/stateful/__init__.py +0 -133
  210. schemathesis/targets.py +0 -45
  211. schemathesis/throttling.py +0 -41
  212. schemathesis/transports/__init__.py +0 -5
  213. schemathesis/transports/auth.py +0 -15
  214. schemathesis/transports/headers.py +0 -35
  215. schemathesis/transports/responses.py +0 -52
  216. schemathesis/types.py +0 -35
  217. schemathesis/utils.py +0 -169
  218. schemathesis-3.25.6.dist-info/METADATA +0 -356
  219. schemathesis-3.25.6.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.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,55 +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
- from typing import Any, Callable, Generator, Type, TypeVar, cast
6
+ from typing import TYPE_CHECKING, Any, Callable, Generator, Type, cast
7
7
 
8
8
  import pytest
9
- from _pytest import fixtures, nodes
9
+ from _pytest import nodes
10
10
  from _pytest.config import hookimpl
11
- from _pytest.fixtures import FuncFixtureInfo
12
- from _pytest.nodes import Node
13
11
  from _pytest.python import Class, Function, FunctionDefinition, Metafunc, Module, PyCollector
14
- from hypothesis import reporting
15
12
  from hypothesis.errors import InvalidArgument, Unsatisfiable
16
13
  from jsonschema.exceptions import SchemaError
17
14
 
18
- from .._dependency_versions import IS_PYTEST_ABOVE_7, IS_PYTEST_ABOVE_8, IS_PYTEST_ABOVE_54
19
- from .._override import get_override_from_mark
20
- from ..constants import (
21
- GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE,
15
+ from schemathesis.core.control import SkipTest
16
+ from schemathesis.core.errors import (
22
17
  RECURSIVE_REFERENCE_ERROR_MESSAGE,
23
18
  SERIALIZERS_SUGGESTION_MESSAGE,
24
- )
25
- from ..exceptions import (
19
+ IncorrectUsage,
26
20
  InvalidHeadersExample,
27
- InvalidRegularExpression,
28
- OperationSchemaError,
21
+ InvalidRegexPattern,
22
+ InvalidSchema,
29
23
  SerializationNotPossible,
30
- SkipTest,
31
- UsageError,
32
24
  )
33
- from ..internal.result import Ok, Result
34
- from ..models import APIOperation
35
- from ..utils import (
36
- PARAMETRIZE_MARKER,
37
- fail_on_no_matches,
38
- get_given_args,
39
- 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,
40
30
  is_given_applied,
41
- is_schemathesis_test,
42
31
  merge_given_args,
43
32
  validate_given_args,
44
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
38
+
39
+ if TYPE_CHECKING:
40
+ from _pytest.fixtures import FuncFixtureInfo
41
+
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
+
45
49
 
46
- T = TypeVar("T", bound=Node)
50
+ def _is_schema(value: object) -> bool:
51
+ from schemathesis.schemas import BaseSchema
47
52
 
53
+ return isinstance(value, BaseSchema)
48
54
 
49
- def create(cls: type[T], *args: Any, **kwargs: Any) -> T:
50
- if IS_PYTEST_ABOVE_54:
51
- return cls.from_parent(*args, **kwargs) # type: ignore
52
- return cls(*args, **kwargs)
55
+
56
+ SchemaHandleMark = Mark["BaseSchema"](attr_name="schema", check=_is_schema)
53
57
 
54
58
 
55
59
  class SchemathesisFunction(Function):
@@ -64,21 +68,15 @@ class SchemathesisFunction(Function):
64
68
  self.test_function = test_func
65
69
  self.test_name = test_name
66
70
 
67
- if not IS_PYTEST_ABOVE_7:
68
- # On pytest 7, `self.obj` is already `partial`
69
- def _getobj(self) -> partial:
70
- """Tests defined as methods require `self` as the first argument.
71
-
72
- This method is called only for this case.
73
- """
74
- return partial(self.obj, self.parent.obj) # type: ignore
75
-
76
71
 
77
72
  class SchemathesisCase(PyCollector):
78
- def __init__(self, test_function: Callable, *args: Any, **kwargs: Any) -> None:
79
- self.given_kwargs: dict[str, Any] | None
80
- given_args = get_given_args(test_function)
81
- 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
82
80
 
83
81
  def _init_with_valid_test(_test_function: Callable, _args: tuple, _kwargs: dict[str, Any]) -> None:
84
82
  self.test_function = _test_function
@@ -90,20 +88,18 @@ class SchemathesisCase(PyCollector):
90
88
  if failing_test is not None:
91
89
  self.test_function = failing_test
92
90
  self.is_invalid_test = True
93
- self.given_kwargs = None
91
+ self.given_kwargs = {}
94
92
  else:
95
93
  _init_with_valid_test(test_function, given_args, given_kwargs)
96
94
  else:
97
95
  _init_with_valid_test(test_function, given_args, given_kwargs)
98
- self.schemathesis_case = getattr(test_function, PARAMETRIZE_MARKER)
96
+ self.schema = schema
99
97
  super().__init__(*args, **kwargs)
100
98
 
101
99
  def _get_test_name(self, operation: APIOperation) -> str:
102
- return f"{self.name}[{operation.verbose_name}]"
100
+ return f"{self.name}[{operation.label}]"
103
101
 
104
- def _gen_items(
105
- self, result: Result[APIOperation, OperationSchemaError]
106
- ) -> Generator[SchemathesisFunction, None, None]:
102
+ def _gen_items(self, result: Result[APIOperation, InvalidSchema]) -> Generator[SchemathesisFunction, None, None]:
107
103
  """Generate all tests for the given API operation.
108
104
 
109
105
  Could produce more than one test item if
@@ -112,7 +108,7 @@ class SchemathesisCase(PyCollector):
112
108
  This implementation is based on the original one in pytest, but with slight adjustments
113
109
  to produce tests out of hypothesis ones.
114
110
  """
115
- from .._hypothesis import create_test
111
+ from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, create_test
116
112
 
117
113
  is_trio_test = False
118
114
  for mark in getattr(self.test_function, "pytestmark", []):
@@ -125,24 +121,29 @@ class SchemathesisCase(PyCollector):
125
121
  if self.is_invalid_test:
126
122
  funcobj = self.test_function
127
123
  else:
128
- override = get_override_from_mark(self.test_function)
129
- as_strategy_kwargs: dict | None
124
+ override = OverrideMark.get(self.test_function)
130
125
  if override is not None:
131
126
  as_strategy_kwargs = {}
132
127
  for location, entry in override.for_operation(operation).items():
133
128
  if entry:
134
129
  as_strategy_kwargs[location] = entry
135
130
  else:
136
- as_strategy_kwargs = None
131
+ as_strategy_kwargs = {}
137
132
  funcobj = create_test(
138
133
  operation=operation,
139
- test=self.test_function,
140
- _given_kwargs=self.given_kwargs,
141
- data_generation_methods=self.schemathesis_case.data_generation_methods,
142
- generation_config=self.schemathesis_case.generation_config,
143
- as_strategy_kwargs=as_strategy_kwargs,
144
- 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
+ ),
145
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
146
147
  name = self._get_test_name(operation)
147
148
  else:
148
149
  error = result.err()
@@ -155,7 +156,9 @@ class SchemathesisCase(PyCollector):
155
156
  name += f"[{error.full_path}]"
156
157
 
157
158
  cls = self._get_class_parent()
158
- definition: FunctionDefinition = create(FunctionDefinition, name=self.name, parent=self.parent, callobj=funcobj)
159
+ definition: FunctionDefinition = FunctionDefinition.from_parent(
160
+ name=self.name, parent=self.parent, callobj=funcobj
161
+ )
159
162
  fixturemanager = self.session._fixturemanager
160
163
  fixtureinfo = fixturemanager.getfixtureinfo(definition, funcobj, cls)
161
164
 
@@ -166,8 +169,7 @@ class SchemathesisCase(PyCollector):
166
169
  funcobj = partial(funcobj, self.parent.obj)
167
170
 
168
171
  if not metafunc._calls:
169
- yield create(
170
- SchemathesisFunction,
172
+ yield SchemathesisFunction.from_parent(
171
173
  name=name,
172
174
  parent=self.parent,
173
175
  callobj=funcobj,
@@ -176,15 +178,12 @@ class SchemathesisCase(PyCollector):
176
178
  originalname=self.name,
177
179
  )
178
180
  else:
179
- if not IS_PYTEST_ABOVE_8:
180
- fixtures.add_funcarg_pseudo_fixture_def(self.parent, metafunc, fixturemanager) # type: ignore[arg-type]
181
181
  fixtureinfo.prune_dependency_tree()
182
182
  for callspec in metafunc._calls:
183
183
  subname = f"{name}[{callspec.id}]"
184
- yield create(
185
- SchemathesisFunction,
184
+ yield SchemathesisFunction.from_parent(
185
+ self.parent,
186
186
  name=subname,
187
- parent=self.parent,
188
187
  callspec=callspec,
189
188
  callobj=funcobj,
190
189
  fixtureinfo=fixtureinfo,
@@ -200,13 +199,10 @@ class SchemathesisCase(PyCollector):
200
199
  def _parametrize(self, cls: type | None, definition: FunctionDefinition, fixtureinfo: FuncFixtureInfo) -> Metafunc:
201
200
  parent = self.getparent(Module)
202
201
  module = parent.obj if parent is not None else parent
203
- kwargs = {"cls": cls, "module": module}
204
- if IS_PYTEST_ABOVE_7:
205
- # Avoiding `Metafunc` is quite problematic for now, as there are quite a lot of internals we rely on
206
- kwargs["_ispytest"] = True
207
- 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)
208
204
  methods = []
209
- if hasattr(module, "pytest_generate_tests"):
205
+ if module is not None and hasattr(module, "pytest_generate_tests"):
210
206
  methods.append(module.pytest_generate_tests)
211
207
  if hasattr(cls, "pytest_generate_tests"):
212
208
  cls = cast(Type, cls)
@@ -217,13 +213,7 @@ class SchemathesisCase(PyCollector):
217
213
  def collect(self) -> list[Function]: # type: ignore
218
214
  """Generate different test items for all API operations available in the given schema."""
219
215
  try:
220
- items = [
221
- item
222
- for operation in self.schemathesis_case.get_all_operations(
223
- hooks=getattr(self.test_function, "_schemathesis_hooks", None)
224
- )
225
- for item in self._gen_items(operation)
226
- ]
216
+ items = [item for operation in self.schema.get_all_operations() for item in self._gen_items(operation)]
227
217
  if not items:
228
218
  fail_on_no_matches(self.nodeid)
229
219
  return items
@@ -231,37 +221,32 @@ class SchemathesisCase(PyCollector):
231
221
  pytest.fail("Error during collection")
232
222
 
233
223
 
234
- @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
235
238
  def pytest_pycollect_makeitem(collector: nodes.Collector, name: str, obj: Any) -> Generator[None, Any, None]:
236
239
  """Switch to a different collector if the test is parametrized marked by schemathesis."""
237
240
  outcome = yield
238
- if is_schemathesis_test(obj):
239
- outcome.force_result(create(SchemathesisCase, parent=collector, test_function=obj, name=name))
240
- 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:
241
246
  outcome.get_result()
242
247
 
243
248
 
244
- IGNORED_HYPOTHESIS_OUTPUT = ("Falsifying example",)
245
-
246
-
247
- def _should_ignore_entry(value: str) -> bool:
248
- return value.startswith(IGNORED_HYPOTHESIS_OUTPUT)
249
-
250
-
251
- def hypothesis_reporter(value: str) -> None:
252
- if _should_ignore_entry(value):
253
- return
254
- reporting.default(value)
255
-
256
-
257
- @contextmanager
258
- def skip_unnecessary_hypothesis_output() -> Generator:
259
- """Avoid printing Hypothesis output that is not necessary in Schemathesis' pytest plugin."""
260
- with reporting.with_reporter(hypothesis_reporter): # type: ignore
261
- yield
262
-
263
-
264
- @hookimpl(hookwrapper=True)
249
+ @hookimpl(wrapper=True)
265
250
  def pytest_pyfunc_call(pyfuncitem): # type:ignore
266
251
  """It is possible to have a Hypothesis exception in runtime.
267
252
 
@@ -269,29 +254,28 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
269
254
  """
270
255
  from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
271
256
 
272
- from .._hypothesis import (
273
- get_invalid_example_headers_mark,
274
- get_invalid_regex_mark,
275
- get_non_serializable_mark,
276
- has_unsatisfied_example_mark,
257
+ from schemathesis.generation.hypothesis.builder import (
258
+ InvalidHeadersExampleMark,
259
+ InvalidRegexMark,
260
+ NonSerializableMark,
261
+ UnsatisfiableExampleMark,
277
262
  )
278
263
 
279
264
  __tracebackhide__ = True
280
265
  if isinstance(pyfuncitem, SchemathesisFunction):
281
- with skip_unnecessary_hypothesis_output():
282
- outcome = yield
283
266
  try:
284
- outcome.get_result()
267
+ with ignore_hypothesis_output():
268
+ yield
285
269
  except InvalidArgument as exc:
286
270
  if "Inconsistent args" in str(exc) and "@example()" in str(exc):
287
- raise UsageError(GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE) from None
288
- 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
289
273
  except HypothesisRefResolutionError:
290
274
  pytest.skip(RECURSIVE_REFERENCE_ERROR_MESSAGE)
291
275
  except (SkipTest, unittest.SkipTest) as exc:
292
- if has_unsatisfied_example_mark(pyfuncitem.obj):
276
+ if UnsatisfiableExampleMark.is_set(pyfuncitem.obj):
293
277
  raise Unsatisfiable("Failed to generate test cases from examples for this API operation") from None
294
- non_serializable = get_non_serializable_mark(pyfuncitem.obj)
278
+ non_serializable = NonSerializableMark.get(pyfuncitem.obj)
295
279
  if non_serializable is not None:
296
280
  media_types = ", ".join(non_serializable.media_types)
297
281
  raise SerializationNotPossible(
@@ -299,22 +283,17 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
299
283
  f" unsupported payload media types: {media_types}\n{SERIALIZERS_SUGGESTION_MESSAGE}",
300
284
  media_types=non_serializable.media_types,
301
285
  ) from None
302
- invalid_regex = get_invalid_regex_mark(pyfuncitem.obj)
286
+ invalid_regex = InvalidRegexMark.get(pyfuncitem.obj)
303
287
  if invalid_regex is not None:
304
- raise InvalidRegularExpression.from_schema_error(invalid_regex, from_examples=True) from None
305
- 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)
306
290
  if invalid_headers is not None:
307
291
  raise InvalidHeadersExample.from_headers(invalid_headers) from None
308
292
  pytest.skip(exc.args[0])
309
293
  except SchemaError as exc:
310
- raise InvalidRegularExpression.from_schema_error(exc, from_examples=False) from exc
311
- except Exception as exc:
312
- if hasattr(exc, "__notes__"):
313
- exc.__notes__ = [note for note in exc.__notes__ if not _should_ignore_entry(note)] # type: ignore
314
- raise
315
- 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)
316
296
  if invalid_headers is not None:
317
297
  raise InvalidHeadersExample.from_headers(invalid_headers) from None
318
298
  else:
319
- outcome = yield
320
- outcome.get_result()
299
+ yield
File without changes