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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. schemathesis/__init__.py +41 -79
  2. schemathesis/auths.py +111 -122
  3. schemathesis/checks.py +169 -60
  4. schemathesis/cli/__init__.py +15 -2117
  5. schemathesis/cli/commands/__init__.py +85 -0
  6. schemathesis/cli/commands/data.py +10 -0
  7. schemathesis/cli/commands/run/__init__.py +590 -0
  8. schemathesis/cli/commands/run/context.py +204 -0
  9. schemathesis/cli/commands/run/events.py +60 -0
  10. schemathesis/cli/commands/run/executor.py +157 -0
  11. schemathesis/cli/commands/run/filters.py +53 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
  15. schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1628 -0
  17. schemathesis/cli/commands/run/loaders.py +114 -0
  18. schemathesis/cli/commands/run/validation.py +246 -0
  19. schemathesis/cli/constants.py +5 -58
  20. schemathesis/cli/core.py +19 -0
  21. schemathesis/cli/ext/fs.py +16 -0
  22. schemathesis/cli/ext/groups.py +84 -0
  23. schemathesis/cli/{options.py → ext/options.py} +36 -34
  24. schemathesis/config/__init__.py +189 -0
  25. schemathesis/config/_auth.py +51 -0
  26. schemathesis/config/_checks.py +268 -0
  27. schemathesis/config/_diff_base.py +99 -0
  28. schemathesis/config/_env.py +21 -0
  29. schemathesis/config/_error.py +156 -0
  30. schemathesis/config/_generation.py +149 -0
  31. schemathesis/config/_health_check.py +24 -0
  32. schemathesis/config/_operations.py +327 -0
  33. schemathesis/config/_output.py +171 -0
  34. schemathesis/config/_parameters.py +19 -0
  35. schemathesis/config/_phases.py +187 -0
  36. schemathesis/config/_projects.py +527 -0
  37. schemathesis/config/_rate_limit.py +17 -0
  38. schemathesis/config/_report.py +120 -0
  39. schemathesis/config/_validator.py +9 -0
  40. schemathesis/config/_warnings.py +25 -0
  41. schemathesis/config/schema.json +885 -0
  42. schemathesis/core/__init__.py +67 -0
  43. schemathesis/core/compat.py +32 -0
  44. schemathesis/core/control.py +2 -0
  45. schemathesis/core/curl.py +58 -0
  46. schemathesis/core/deserialization.py +65 -0
  47. schemathesis/core/errors.py +459 -0
  48. schemathesis/core/failures.py +315 -0
  49. schemathesis/core/fs.py +19 -0
  50. schemathesis/core/hooks.py +20 -0
  51. schemathesis/core/loaders.py +104 -0
  52. schemathesis/core/marks.py +66 -0
  53. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  54. schemathesis/core/output/__init__.py +46 -0
  55. schemathesis/core/output/sanitization.py +54 -0
  56. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  57. schemathesis/core/registries.py +31 -0
  58. schemathesis/core/transforms.py +113 -0
  59. schemathesis/core/transport.py +223 -0
  60. schemathesis/core/validation.py +54 -0
  61. schemathesis/core/version.py +7 -0
  62. schemathesis/engine/__init__.py +28 -0
  63. schemathesis/engine/context.py +118 -0
  64. schemathesis/engine/control.py +36 -0
  65. schemathesis/engine/core.py +169 -0
  66. schemathesis/engine/errors.py +464 -0
  67. schemathesis/engine/events.py +258 -0
  68. schemathesis/engine/phases/__init__.py +88 -0
  69. schemathesis/{runner → engine/phases}/probes.py +52 -68
  70. schemathesis/engine/phases/stateful/__init__.py +68 -0
  71. schemathesis/engine/phases/stateful/_executor.py +356 -0
  72. schemathesis/engine/phases/stateful/context.py +85 -0
  73. schemathesis/engine/phases/unit/__init__.py +212 -0
  74. schemathesis/engine/phases/unit/_executor.py +416 -0
  75. schemathesis/engine/phases/unit/_pool.py +82 -0
  76. schemathesis/engine/recorder.py +247 -0
  77. schemathesis/errors.py +43 -0
  78. schemathesis/filters.py +17 -98
  79. schemathesis/generation/__init__.py +5 -33
  80. schemathesis/generation/case.py +317 -0
  81. schemathesis/generation/coverage.py +282 -175
  82. schemathesis/generation/hypothesis/__init__.py +36 -0
  83. schemathesis/generation/hypothesis/builder.py +800 -0
  84. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  85. schemathesis/generation/hypothesis/given.py +66 -0
  86. schemathesis/generation/hypothesis/reporting.py +14 -0
  87. schemathesis/generation/hypothesis/strategies.py +16 -0
  88. schemathesis/generation/meta.py +115 -0
  89. schemathesis/generation/metrics.py +93 -0
  90. schemathesis/generation/modes.py +20 -0
  91. schemathesis/generation/overrides.py +116 -0
  92. schemathesis/generation/stateful/__init__.py +37 -0
  93. schemathesis/generation/stateful/state_machine.py +278 -0
  94. schemathesis/graphql/__init__.py +15 -0
  95. schemathesis/graphql/checks.py +109 -0
  96. schemathesis/graphql/loaders.py +284 -0
  97. schemathesis/hooks.py +80 -101
  98. schemathesis/openapi/__init__.py +13 -0
  99. schemathesis/openapi/checks.py +455 -0
  100. schemathesis/openapi/generation/__init__.py +0 -0
  101. schemathesis/openapi/generation/filters.py +72 -0
  102. schemathesis/openapi/loaders.py +313 -0
  103. schemathesis/pytest/__init__.py +5 -0
  104. schemathesis/pytest/control_flow.py +7 -0
  105. schemathesis/pytest/lazy.py +281 -0
  106. schemathesis/pytest/loaders.py +36 -0
  107. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
  108. schemathesis/python/__init__.py +0 -0
  109. schemathesis/python/asgi.py +12 -0
  110. schemathesis/python/wsgi.py +12 -0
  111. schemathesis/schemas.py +537 -273
  112. schemathesis/specs/graphql/__init__.py +0 -1
  113. schemathesis/specs/graphql/_cache.py +1 -2
  114. schemathesis/specs/graphql/scalars.py +42 -6
  115. schemathesis/specs/graphql/schemas.py +141 -137
  116. schemathesis/specs/graphql/validation.py +11 -17
  117. schemathesis/specs/openapi/__init__.py +6 -1
  118. schemathesis/specs/openapi/_cache.py +1 -2
  119. schemathesis/specs/openapi/_hypothesis.py +142 -156
  120. schemathesis/specs/openapi/checks.py +368 -257
  121. schemathesis/specs/openapi/converter.py +4 -4
  122. schemathesis/specs/openapi/definitions.py +1 -1
  123. schemathesis/specs/openapi/examples.py +23 -21
  124. schemathesis/specs/openapi/expressions/__init__.py +31 -19
  125. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  126. schemathesis/specs/openapi/expressions/lexer.py +1 -1
  127. schemathesis/specs/openapi/expressions/nodes.py +36 -41
  128. schemathesis/specs/openapi/expressions/parser.py +1 -1
  129. schemathesis/specs/openapi/formats.py +35 -7
  130. schemathesis/specs/openapi/media_types.py +53 -12
  131. schemathesis/specs/openapi/negative/__init__.py +7 -4
  132. schemathesis/specs/openapi/negative/mutations.py +6 -5
  133. schemathesis/specs/openapi/parameters.py +7 -10
  134. schemathesis/specs/openapi/patterns.py +94 -31
  135. schemathesis/specs/openapi/references.py +12 -53
  136. schemathesis/specs/openapi/schemas.py +238 -308
  137. schemathesis/specs/openapi/security.py +1 -1
  138. schemathesis/specs/openapi/serialization.py +12 -6
  139. schemathesis/specs/openapi/stateful/__init__.py +268 -133
  140. schemathesis/specs/openapi/stateful/control.py +87 -0
  141. schemathesis/specs/openapi/stateful/links.py +209 -0
  142. schemathesis/transport/__init__.py +142 -0
  143. schemathesis/transport/asgi.py +26 -0
  144. schemathesis/transport/prepare.py +124 -0
  145. schemathesis/transport/requests.py +244 -0
  146. schemathesis/{_xml.py → transport/serialization.py} +69 -11
  147. schemathesis/transport/wsgi.py +171 -0
  148. schemathesis-4.0.0.dist-info/METADATA +204 -0
  149. schemathesis-4.0.0.dist-info/RECORD +164 -0
  150. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
  152. schemathesis/_compat.py +0 -74
  153. schemathesis/_dependency_versions.py +0 -19
  154. schemathesis/_hypothesis.py +0 -712
  155. schemathesis/_override.py +0 -50
  156. schemathesis/_patches.py +0 -21
  157. schemathesis/_rate_limiter.py +0 -7
  158. schemathesis/cli/callbacks.py +0 -466
  159. schemathesis/cli/cassettes.py +0 -561
  160. schemathesis/cli/context.py +0 -75
  161. schemathesis/cli/debug.py +0 -27
  162. schemathesis/cli/handlers.py +0 -19
  163. schemathesis/cli/junitxml.py +0 -124
  164. schemathesis/cli/output/__init__.py +0 -1
  165. schemathesis/cli/output/default.py +0 -920
  166. schemathesis/cli/output/short.py +0 -59
  167. schemathesis/cli/reporting.py +0 -79
  168. schemathesis/cli/sanitization.py +0 -26
  169. schemathesis/code_samples.py +0 -151
  170. schemathesis/constants.py +0 -54
  171. schemathesis/contrib/__init__.py +0 -11
  172. schemathesis/contrib/openapi/__init__.py +0 -11
  173. schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
  174. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  175. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  176. schemathesis/contrib/unique_data.py +0 -41
  177. schemathesis/exceptions.py +0 -571
  178. schemathesis/experimental/__init__.py +0 -109
  179. schemathesis/extra/_aiohttp.py +0 -28
  180. schemathesis/extra/_flask.py +0 -13
  181. schemathesis/extra/_server.py +0 -18
  182. schemathesis/failures.py +0 -284
  183. schemathesis/fixups/__init__.py +0 -37
  184. schemathesis/fixups/fast_api.py +0 -41
  185. schemathesis/fixups/utf8_bom.py +0 -28
  186. schemathesis/generation/_methods.py +0 -44
  187. schemathesis/graphql.py +0 -3
  188. schemathesis/internal/__init__.py +0 -7
  189. schemathesis/internal/checks.py +0 -86
  190. schemathesis/internal/copy.py +0 -32
  191. schemathesis/internal/datetime.py +0 -5
  192. schemathesis/internal/deprecation.py +0 -37
  193. schemathesis/internal/diff.py +0 -15
  194. schemathesis/internal/extensions.py +0 -27
  195. schemathesis/internal/jsonschema.py +0 -36
  196. schemathesis/internal/output.py +0 -68
  197. schemathesis/internal/transformation.py +0 -26
  198. schemathesis/internal/validation.py +0 -34
  199. schemathesis/lazy.py +0 -474
  200. schemathesis/loaders.py +0 -122
  201. schemathesis/models.py +0 -1341
  202. schemathesis/parameters.py +0 -90
  203. schemathesis/runner/__init__.py +0 -605
  204. schemathesis/runner/events.py +0 -389
  205. schemathesis/runner/impl/__init__.py +0 -3
  206. schemathesis/runner/impl/context.py +0 -88
  207. schemathesis/runner/impl/core.py +0 -1280
  208. schemathesis/runner/impl/solo.py +0 -80
  209. schemathesis/runner/impl/threadpool.py +0 -391
  210. schemathesis/runner/serialization.py +0 -544
  211. schemathesis/sanitization.py +0 -252
  212. schemathesis/serializers.py +0 -328
  213. schemathesis/service/__init__.py +0 -18
  214. schemathesis/service/auth.py +0 -11
  215. schemathesis/service/ci.py +0 -202
  216. schemathesis/service/client.py +0 -133
  217. schemathesis/service/constants.py +0 -38
  218. schemathesis/service/events.py +0 -61
  219. schemathesis/service/extensions.py +0 -224
  220. schemathesis/service/hosts.py +0 -111
  221. schemathesis/service/metadata.py +0 -71
  222. schemathesis/service/models.py +0 -258
  223. schemathesis/service/report.py +0 -255
  224. schemathesis/service/serialization.py +0 -173
  225. schemathesis/service/usage.py +0 -66
  226. schemathesis/specs/graphql/loaders.py +0 -364
  227. schemathesis/specs/openapi/expressions/context.py +0 -16
  228. schemathesis/specs/openapi/links.py +0 -389
  229. schemathesis/specs/openapi/loaders.py +0 -707
  230. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  231. schemathesis/specs/openapi/stateful/types.py +0 -14
  232. schemathesis/specs/openapi/validation.py +0 -26
  233. schemathesis/stateful/__init__.py +0 -147
  234. schemathesis/stateful/config.py +0 -97
  235. schemathesis/stateful/context.py +0 -135
  236. schemathesis/stateful/events.py +0 -274
  237. schemathesis/stateful/runner.py +0 -309
  238. schemathesis/stateful/sink.py +0 -68
  239. schemathesis/stateful/state_machine.py +0 -328
  240. schemathesis/stateful/statistic.py +0 -22
  241. schemathesis/stateful/validation.py +0 -100
  242. schemathesis/targets.py +0 -77
  243. schemathesis/transports/__init__.py +0 -369
  244. schemathesis/transports/asgi.py +0 -7
  245. schemathesis/transports/auth.py +0 -38
  246. schemathesis/transports/headers.py +0 -36
  247. schemathesis/transports/responses.py +0 -57
  248. schemathesis/types.py +0 -44
  249. schemathesis/utils.py +0 -164
  250. schemathesis-3.39.15.dist-info/METADATA +0 -293
  251. schemathesis-3.39.15.dist-info/RECORD +0 -160
  252. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  253. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  254. /schemathesis/{internal → core}/result.py +0 -0
  255. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -1,712 +0,0 @@
1
- """High-level API for creating Hypothesis tests."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- from dataclasses import dataclass
7
- import warnings
8
- from functools import wraps
9
- from itertools import combinations
10
- from typing import TYPE_CHECKING, Any, Callable, Generator, Mapping
11
-
12
- import hypothesis
13
- from hypothesis import Phase
14
- from hypothesis.errors import HypothesisWarning, Unsatisfiable
15
- from hypothesis.internal.entropy import deterministic_PRNG
16
- from jsonschema.exceptions import SchemaError
17
-
18
- from schemathesis.serializers import get_first_matching_media_type
19
-
20
- from . import _patches
21
- from .auths import AuthStorage, get_auth_storage_from_test
22
- from .constants import DEFAULT_DEADLINE, NOT_SET
23
- from .exceptions import OperationSchemaError, SerializationNotPossible
24
- from .experimental import COVERAGE_PHASE
25
- from .generation import DataGenerationMethod, GenerationConfig, combine_strategies, coverage, get_single_example
26
- from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher
27
- from .models import APIOperation, Case, GenerationMetadata, TestPhase
28
- from .parameters import ParameterSet
29
- from .transports.content_types import parse_content_type
30
- from .transports.headers import has_invalid_characters, is_latin_1_encodable
31
- from .types import NotSet
32
- from schemathesis import auths
33
-
34
- if TYPE_CHECKING:
35
- from .utils import GivenInput
36
-
37
- # Forcefully initializes Hypothesis' global PRNG to avoid races that initialize it
38
- # if e.g. Schemathesis CLI is used with multiple workers
39
- with deterministic_PRNG():
40
- pass
41
-
42
- _patches.install()
43
-
44
-
45
- def create_test(
46
- *,
47
- operation: APIOperation,
48
- test: Callable,
49
- settings: hypothesis.settings | None = None,
50
- seed: int | None = None,
51
- data_generation_methods: list[DataGenerationMethod],
52
- generation_config: GenerationConfig | None = None,
53
- as_strategy_kwargs: dict[str, Any] | None = None,
54
- keep_async_fn: bool = False,
55
- _given_args: tuple[GivenInput, ...] = (),
56
- _given_kwargs: dict[str, GivenInput] | None = None,
57
- ) -> Callable:
58
- """Create a Hypothesis test."""
59
- hook_dispatcher = getattr(test, "_schemathesis_hooks", None)
60
- auth_storage = get_auth_storage_from_test(test)
61
- strategies = []
62
- skip_on_not_negated = len(data_generation_methods) == 1 and DataGenerationMethod.negative in data_generation_methods
63
- as_strategy_kwargs = as_strategy_kwargs or {}
64
- as_strategy_kwargs.update(
65
- {
66
- "hooks": hook_dispatcher,
67
- "auth_storage": auth_storage,
68
- "generation_config": generation_config,
69
- "skip_on_not_negated": skip_on_not_negated,
70
- }
71
- )
72
- for data_generation_method in data_generation_methods:
73
- strategies.append(operation.as_strategy(data_generation_method=data_generation_method, **as_strategy_kwargs))
74
- strategy = combine_strategies(strategies)
75
- _given_kwargs = (_given_kwargs or {}).copy()
76
- _given_kwargs.setdefault("case", strategy)
77
-
78
- # Each generated test should be a unique function. It is especially important for the case when Schemathesis runs
79
- # tests in multiple threads because Hypothesis stores some internal attributes on function objects and re-writing
80
- # them from different threads may lead to unpredictable side-effects.
81
-
82
- @wraps(test)
83
- def test_function(*args: Any, **kwargs: Any) -> Any:
84
- __tracebackhide__ = True
85
- return test(*args, **kwargs)
86
-
87
- wrapped_test = hypothesis.given(*_given_args, **_given_kwargs)(test_function)
88
- if seed is not None:
89
- wrapped_test = hypothesis.seed(seed)(wrapped_test)
90
- if asyncio.iscoroutinefunction(test):
91
- # `pytest-trio` expects a coroutine function
92
- if keep_async_fn:
93
- wrapped_test.hypothesis.inner_test = test # type: ignore
94
- else:
95
- wrapped_test.hypothesis.inner_test = make_async_test(test) # type: ignore
96
- setup_default_deadline(wrapped_test)
97
- if settings is not None:
98
- existing_settings = _get_hypothesis_settings(wrapped_test)
99
- if existing_settings is not None:
100
- # Merge the user-provided settings with the current ones
101
- default = hypothesis.settings.default
102
- wrapped_test._hypothesis_internal_use_settings = hypothesis.settings(
103
- wrapped_test._hypothesis_internal_use_settings,
104
- **{item: value for item, value in settings.__dict__.items() if value != getattr(default, item)},
105
- )
106
- else:
107
- wrapped_test = settings(wrapped_test)
108
- existing_settings = _get_hypothesis_settings(wrapped_test)
109
- if existing_settings is not None:
110
- existing_settings = remove_explain_phase(existing_settings)
111
- wrapped_test._hypothesis_internal_use_settings = existing_settings # type: ignore
112
- if Phase.explicit in existing_settings.phases:
113
- wrapped_test = add_examples(
114
- wrapped_test, operation, hook_dispatcher=hook_dispatcher, as_strategy_kwargs=as_strategy_kwargs
115
- )
116
- if COVERAGE_PHASE.is_enabled:
117
- unexpected_methods = generation_config.unexpected_methods if generation_config else None
118
- wrapped_test = add_coverage(
119
- wrapped_test,
120
- operation,
121
- data_generation_methods,
122
- auth_storage,
123
- as_strategy_kwargs,
124
- unexpected_methods,
125
- )
126
- return wrapped_test
127
-
128
-
129
- def setup_default_deadline(wrapped_test: Callable) -> None:
130
- # Quite hacky, but it is the simplest way to set up the default deadline value without affecting non-Schemathesis
131
- # tests globally
132
- existing_settings = _get_hypothesis_settings(wrapped_test)
133
- if existing_settings is not None and existing_settings.deadline == hypothesis.settings.default.deadline:
134
- with warnings.catch_warnings():
135
- warnings.simplefilter("ignore", HypothesisWarning)
136
- new_settings = hypothesis.settings(existing_settings, deadline=DEFAULT_DEADLINE)
137
- wrapped_test._hypothesis_internal_use_settings = new_settings # type: ignore
138
-
139
-
140
- def remove_explain_phase(settings: hypothesis.settings) -> hypothesis.settings:
141
- # The "explain" phase is not supported
142
- if Phase.explain in settings.phases:
143
- phases = tuple(phase for phase in settings.phases if phase != Phase.explain)
144
- return hypothesis.settings(settings, phases=phases)
145
- return settings
146
-
147
-
148
- def _get_hypothesis_settings(test: Callable) -> hypothesis.settings | None:
149
- return getattr(test, "_hypothesis_internal_use_settings", None)
150
-
151
-
152
- def make_async_test(test: Callable) -> Callable:
153
- def async_run(*args: Any, **kwargs: Any) -> None:
154
- try:
155
- loop = asyncio.get_event_loop()
156
- except RuntimeError:
157
- loop = asyncio.new_event_loop()
158
- coro = test(*args, **kwargs)
159
- future = asyncio.ensure_future(coro, loop=loop)
160
- loop.run_until_complete(future)
161
-
162
- return async_run
163
-
164
-
165
- def add_examples(
166
- test: Callable,
167
- operation: APIOperation,
168
- hook_dispatcher: HookDispatcher | None = None,
169
- as_strategy_kwargs: dict[str, Any] | None = None,
170
- ) -> Callable:
171
- """Add examples to the Hypothesis test, if they are specified in the schema."""
172
- from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
173
-
174
- try:
175
- examples: list[Case] = [
176
- get_single_example(strategy)
177
- for strategy in operation.get_strategies_from_examples(as_strategy_kwargs=as_strategy_kwargs)
178
- ]
179
- except (
180
- OperationSchemaError,
181
- HypothesisRefResolutionError,
182
- Unsatisfiable,
183
- SerializationNotPossible,
184
- SchemaError,
185
- ) as exc:
186
- # Invalid schema:
187
- # In this case, the user didn't pass `--validate-schema=false` and see an error in the output anyway,
188
- # and no tests will be executed. For this reason, examples can be skipped
189
- # Recursive references: This test will be skipped anyway
190
- # Unsatisfiable:
191
- # The underlying schema is not satisfiable and test will raise an error for the same reason.
192
- # Skipping this exception here allows us to continue the testing process for other operations.
193
- # Still, we allow running user-defined hooks
194
- examples = []
195
- if isinstance(exc, Unsatisfiable):
196
- add_unsatisfied_example_mark(test, exc)
197
- if isinstance(exc, SerializationNotPossible):
198
- add_non_serializable_mark(test, exc)
199
- if isinstance(exc, SchemaError):
200
- add_invalid_regex_mark(test, exc)
201
- context = HookContext(operation) # context should be passed here instead
202
- GLOBAL_HOOK_DISPATCHER.dispatch("before_add_examples", context, examples)
203
- operation.schema.hooks.dispatch("before_add_examples", context, examples)
204
- if hook_dispatcher:
205
- hook_dispatcher.dispatch("before_add_examples", context, examples)
206
- original_test = test
207
- for example in examples:
208
- if example.headers is not None:
209
- invalid_headers = dict(find_invalid_headers(example.headers))
210
- if invalid_headers:
211
- add_invalid_example_header_mark(original_test, invalid_headers)
212
- continue
213
- adjust_urlencoded_payload(example)
214
- test = hypothesis.example(case=example)(test)
215
- return test
216
-
217
-
218
- def adjust_urlencoded_payload(case: Case) -> None:
219
- if case.media_type is not None:
220
- try:
221
- media_type = parse_content_type(case.media_type)
222
- if media_type == ("application", "x-www-form-urlencoded"):
223
- case.body = prepare_urlencoded(case.body)
224
- except ValueError:
225
- pass
226
-
227
-
228
- def add_coverage(
229
- test: Callable,
230
- operation: APIOperation,
231
- data_generation_methods: list[DataGenerationMethod],
232
- auth_storage: AuthStorage | None,
233
- as_strategy_kwargs: dict[str, Any],
234
- unexpected_methods: set[str] | None = None,
235
- ) -> Callable:
236
- from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
237
-
238
- auth_context = auths.AuthContext(
239
- operation=operation,
240
- app=operation.app,
241
- )
242
- overrides = {
243
- container: as_strategy_kwargs[container]
244
- for container in LOCATION_TO_CONTAINER.values()
245
- if container in as_strategy_kwargs
246
- }
247
- for case in _iter_coverage_cases(operation, data_generation_methods, unexpected_methods):
248
- if case.media_type and get_first_matching_media_type(case.media_type) is None:
249
- continue
250
- adjust_urlencoded_payload(case)
251
- auths.set_on_case(case, auth_context, auth_storage)
252
- for container_name, value in overrides.items():
253
- container = getattr(case, container_name)
254
- if container is None:
255
- setattr(case, container_name, value)
256
- else:
257
- container.update(value)
258
- test = hypothesis.example(case=case)(test)
259
- return test
260
-
261
-
262
- class Template:
263
- __slots__ = ("_components", "_template", "_serializers")
264
-
265
- def __init__(self, serializers: dict[str, Callable]) -> None:
266
- self._components: dict[str, DataGenerationMethod] = {}
267
- self._template: dict[str, Any] = {}
268
- self._serializers = serializers
269
-
270
- def __contains__(self, key: str) -> bool:
271
- return key in self._template
272
-
273
- def __getitem__(self, key: str) -> dict:
274
- return self._template[key]
275
-
276
- def get(self, key: str, default: Any = None) -> dict:
277
- return self._template.get(key, default)
278
-
279
- def add_parameter(self, location: str, name: str, value: coverage.GeneratedValue) -> None:
280
- from .specs.openapi.constants import LOCATION_TO_CONTAINER
281
-
282
- component_name = LOCATION_TO_CONTAINER[location]
283
- method = self._components.get(component_name)
284
- if method is None:
285
- self._components[component_name] = value.data_generation_method
286
- elif value.data_generation_method == DataGenerationMethod.negative:
287
- self._components[component_name] = DataGenerationMethod.negative
288
-
289
- container = self._template.setdefault(component_name, {})
290
- container[name] = value.value
291
-
292
- def set_body(self, body: coverage.GeneratedValue, media_type: str) -> None:
293
- self._template["body"] = body.value
294
- self._template["media_type"] = media_type
295
- self._components["body"] = body.data_generation_method
296
-
297
- def _serialize(self, kwargs: dict[str, Any]) -> dict[str, Any]:
298
- from schemathesis.specs.openapi._hypothesis import quote_all
299
-
300
- output = {}
301
- for container_name, value in kwargs.items():
302
- serializer = self._serializers.get(container_name)
303
- if container_name in ("headers", "cookies") and isinstance(value, dict):
304
- value = _stringify_value(value, container_name)
305
- if serializer is not None:
306
- value = serializer(value)
307
- if container_name == "query" and isinstance(value, dict):
308
- value = _stringify_value(value, container_name)
309
- if container_name == "path_parameters" and isinstance(value, dict):
310
- value = _stringify_value(quote_all(value), container_name)
311
- output[container_name] = value
312
- return output
313
-
314
- def unmodified(self) -> TemplateValue:
315
- kwargs = self._template.copy()
316
- kwargs = self._serialize(kwargs)
317
- return TemplateValue(kwargs=kwargs, components=self._components.copy())
318
-
319
- def with_body(self, *, media_type: str, value: coverage.GeneratedValue) -> TemplateValue:
320
- kwargs = {**self._template, "media_type": media_type, "body": value.value}
321
- kwargs = self._serialize(kwargs)
322
- components = {**self._components, "body": value.data_generation_method}
323
- return TemplateValue(kwargs=kwargs, components=components)
324
-
325
- def with_parameter(self, *, location: str, name: str, value: coverage.GeneratedValue) -> TemplateValue:
326
- from .specs.openapi.constants import LOCATION_TO_CONTAINER
327
-
328
- container_name = LOCATION_TO_CONTAINER[location]
329
- container = self._template[container_name]
330
- return self.with_container(
331
- container_name=container_name,
332
- value={**container, name: value.value},
333
- data_generation_method=value.data_generation_method,
334
- )
335
-
336
- def with_container(
337
- self, *, container_name: str, value: Any, data_generation_method: DataGenerationMethod
338
- ) -> TemplateValue:
339
- kwargs = {**self._template, container_name: value}
340
- kwargs = self._serialize(kwargs)
341
- components = {**self._components, container_name: data_generation_method}
342
- return TemplateValue(kwargs=kwargs, components=components)
343
-
344
-
345
- @dataclass
346
- class TemplateValue:
347
- kwargs: dict[str, Any]
348
- components: dict[str, DataGenerationMethod]
349
- __slots__ = ("kwargs", "components")
350
-
351
-
352
- def _stringify_value(val: Any, container_name: str) -> Any:
353
- if val is None:
354
- return "null"
355
- if val is True:
356
- return "true"
357
- if val is False:
358
- return "false"
359
- if isinstance(val, (int, float)):
360
- return str(val)
361
- if isinstance(val, list):
362
- if container_name == "query":
363
- # Having a list here ensures there will be multiple query parameters wit the same name
364
- return [_stringify_value(item, container_name) for item in val]
365
- # use comma-separated values style for arrays
366
- return ",".join(_stringify_value(sub, container_name) for sub in val)
367
- if isinstance(val, dict):
368
- return {key: _stringify_value(sub, container_name) for key, sub in val.items()}
369
- return val
370
-
371
-
372
- def _iter_coverage_cases(
373
- operation: APIOperation,
374
- data_generation_methods: list[DataGenerationMethod],
375
- unexpected_methods: set[str] | None = None,
376
- ) -> Generator[Case, None, None]:
377
- from .specs.openapi.constants import LOCATION_TO_CONTAINER
378
- from .specs.openapi.examples import find_in_responses, find_matching_in_responses
379
- from schemathesis.specs.openapi.serialization import get_serializers_for_operation
380
-
381
- generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
382
- serializers = get_serializers_for_operation(operation)
383
- template = Template(serializers)
384
- responses = find_in_responses(operation)
385
- # NOTE: The HEAD method is excluded
386
- unexpected_methods = unexpected_methods or {"get", "put", "post", "delete", "options", "patch", "trace"}
387
- for parameter in operation.iter_parameters():
388
- location = parameter.location
389
- name = parameter.name
390
- schema = parameter.as_json_schema(operation, update_quantifiers=False)
391
- for value in find_matching_in_responses(responses, parameter.name):
392
- schema.setdefault("examples", []).append(value)
393
- gen = coverage.cover_schema_iter(
394
- coverage.CoverageContext(location=location, data_generation_methods=data_generation_methods), schema
395
- )
396
- value = next(gen, NOT_SET)
397
- if isinstance(value, NotSet):
398
- continue
399
- template.add_parameter(location, name, value)
400
- generators[(location, name)] = gen
401
- if operation.body:
402
- for body in operation.body:
403
- schema = body.as_json_schema(operation, update_quantifiers=False)
404
- # Definition could be a list for Open API 2.0
405
- definition = body.definition if isinstance(body.definition, dict) else {}
406
- examples = [example["value"] for example in definition.get("examples", {}).values() if "value" in example]
407
- if examples:
408
- schema.setdefault("examples", []).extend(examples)
409
- gen = coverage.cover_schema_iter(
410
- coverage.CoverageContext(location="body", data_generation_methods=data_generation_methods), schema
411
- )
412
- value = next(gen, NOT_SET)
413
- if isinstance(value, NotSet):
414
- continue
415
- if "body" not in template:
416
- template.set_body(value, body.media_type)
417
- data = template.with_body(value=value, media_type=body.media_type)
418
- case = operation.make_case(**data.kwargs)
419
- case.data_generation_method = value.data_generation_method
420
- case.meta = _make_meta(
421
- description=value.description,
422
- location=value.location,
423
- parameter=body.media_type,
424
- parameter_location="body",
425
- **data.components,
426
- )
427
- yield case
428
- for next_value in gen:
429
- data = template.with_body(value=next_value, media_type=body.media_type)
430
- case = operation.make_case(**data.kwargs)
431
- case.data_generation_method = next_value.data_generation_method
432
- case.meta = _make_meta(
433
- description=next_value.description,
434
- location=next_value.location,
435
- parameter=body.media_type,
436
- parameter_location="body",
437
- **data.components,
438
- )
439
- yield case
440
- elif DataGenerationMethod.positive in data_generation_methods:
441
- data = template.unmodified()
442
- case = operation.make_case(**data.kwargs)
443
- case.data_generation_method = DataGenerationMethod.positive
444
- case.meta = _make_meta(description="Default positive test case", **data.components)
445
- yield case
446
-
447
- for (location, name), gen in generators.items():
448
- for value in gen:
449
- data = template.with_parameter(location=location, name=name, value=value)
450
- case = operation.make_case(**data.kwargs)
451
- case.data_generation_method = value.data_generation_method
452
- case.meta = _make_meta(
453
- description=value.description,
454
- location=value.location,
455
- parameter=name,
456
- parameter_location=location,
457
- **data.components,
458
- )
459
- yield case
460
- if DataGenerationMethod.negative in data_generation_methods:
461
- # Generate HTTP methods that are not specified in the spec
462
- methods = unexpected_methods - set(operation.schema[operation.path])
463
- for method in sorted(methods):
464
- data = template.unmodified()
465
- case = operation.make_case(**data.kwargs)
466
- case._explicit_method = method
467
- case.data_generation_method = DataGenerationMethod.negative
468
- case.meta = _make_meta(description=f"Unspecified HTTP method: {method.upper()}", **data.components)
469
- yield case
470
- # Generate duplicate query parameters
471
- if operation.query:
472
- container = template["query"]
473
- for parameter in operation.query:
474
- # Could be absent if value schema can't be negated
475
- # I.e. contains just `default` value without any other keywords
476
- value = container.get(parameter.name, NOT_SET)
477
- if value is not NOT_SET:
478
- data = template.with_container(
479
- container_name="query",
480
- value={**container, parameter.name: [value, value]},
481
- data_generation_method=DataGenerationMethod.negative,
482
- )
483
- case = operation.make_case(**data.kwargs)
484
- case.data_generation_method = DataGenerationMethod.negative
485
- case.meta = _make_meta(
486
- description=f"Duplicate `{parameter.name}` query parameter",
487
- location=None,
488
- parameter=parameter.name,
489
- parameter_location="query",
490
- **data.components,
491
- )
492
- yield case
493
- # Generate missing required parameters
494
- for parameter in operation.iter_parameters():
495
- if parameter.is_required and parameter.location != "path":
496
- name = parameter.name
497
- location = parameter.location
498
- container_name = LOCATION_TO_CONTAINER[location]
499
- container = template[container_name]
500
- data = template.with_container(
501
- container_name=container_name,
502
- value={k: v for k, v in container.items() if k != name},
503
- data_generation_method=DataGenerationMethod.negative,
504
- )
505
- case = operation.make_case(**data.kwargs)
506
- case.data_generation_method = DataGenerationMethod.negative
507
- case.meta = _make_meta(
508
- description=f"Missing `{name}` at {location}",
509
- location=None,
510
- parameter=name,
511
- parameter_location=location,
512
- **data.components,
513
- )
514
- yield case
515
- # Generate combinations for each location
516
- for location, parameter_set in [
517
- ("query", operation.query),
518
- ("header", operation.headers),
519
- ("cookie", operation.cookies),
520
- ]:
521
- if not parameter_set:
522
- continue
523
-
524
- container_name = LOCATION_TO_CONTAINER[location]
525
- base_container = template.get(container_name, {})
526
-
527
- # Get required and optional parameters
528
- required = {p.name for p in parameter_set if p.is_required}
529
- all_params = {p.name for p in parameter_set}
530
- optional = sorted(all_params - required)
531
-
532
- # Helper function to create and yield a case
533
- def make_case(
534
- container_values: dict,
535
- description: str,
536
- _location: str,
537
- _container_name: str,
538
- _parameter: str | None,
539
- _data_generation_method: DataGenerationMethod,
540
- ) -> Case:
541
- data = template.with_container(
542
- container_name=_container_name, value=container_values, data_generation_method=_data_generation_method
543
- )
544
- case = operation.make_case(**data.kwargs)
545
- case.data_generation_method = _data_generation_method
546
- case.meta = _make_meta(
547
- description=description,
548
- location=None,
549
- parameter=_parameter,
550
- parameter_location=_location,
551
- **data.components,
552
- )
553
- return case
554
-
555
- def _combination_schema(
556
- combination: dict[str, Any], _required: set[str], _parameter_set: ParameterSet
557
- ) -> dict[str, Any]:
558
- return {
559
- "properties": {
560
- parameter.name: parameter.as_json_schema(operation)
561
- for parameter in _parameter_set
562
- if parameter.name in combination
563
- },
564
- "required": list(_required),
565
- "additionalProperties": False,
566
- }
567
-
568
- def _yield_negative(
569
- subschema: dict[str, Any], _location: str, _container_name: str
570
- ) -> Generator[Case, None, None]:
571
- for more in coverage.cover_schema_iter(
572
- coverage.CoverageContext(location=_location, data_generation_methods=[DataGenerationMethod.negative]),
573
- subschema,
574
- ):
575
- yield make_case(
576
- more.value,
577
- more.description,
578
- _location,
579
- _container_name,
580
- more.parameter,
581
- DataGenerationMethod.negative,
582
- )
583
-
584
- # 1. Generate only required properties
585
- if required and all_params != required:
586
- only_required = {k: v for k, v in base_container.items() if k in required}
587
- if DataGenerationMethod.positive in data_generation_methods:
588
- yield make_case(
589
- only_required,
590
- "Only required properties",
591
- location,
592
- container_name,
593
- None,
594
- DataGenerationMethod.positive,
595
- )
596
- if DataGenerationMethod.negative in data_generation_methods:
597
- subschema = _combination_schema(only_required, required, parameter_set)
598
- for case in _yield_negative(subschema, location, container_name):
599
- # Already generated in one of the blocks above
600
- if location != "path" and not case.meta.description.startswith("Missing required property"):
601
- yield case
602
-
603
- # 2. Generate combinations with required properties and one optional property
604
- for opt_param in optional:
605
- combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
606
- if combo != base_container and DataGenerationMethod.positive in data_generation_methods:
607
- yield make_case(
608
- combo,
609
- f"All required properties and optional '{opt_param}'",
610
- location,
611
- container_name,
612
- None,
613
- DataGenerationMethod.positive,
614
- )
615
- if DataGenerationMethod.negative in data_generation_methods:
616
- subschema = _combination_schema(combo, required, parameter_set)
617
- for case in _yield_negative(subschema, location, container_name):
618
- # Already generated in one of the blocks above
619
- if location != "path" and not case.meta.description.startswith("Missing required property"):
620
- yield case
621
-
622
- # 3. Generate one combination for each size from 2 to N-1 of optional parameters
623
- if len(optional) > 1 and DataGenerationMethod.positive in data_generation_methods:
624
- for size in range(2, len(optional)):
625
- for combination in combinations(optional, size):
626
- combo = {k: v for k, v in base_container.items() if k in required or k in combination}
627
- if combo != base_container:
628
- yield make_case(
629
- combo,
630
- f"All required and {size} optional properties",
631
- location,
632
- container_name,
633
- None,
634
- DataGenerationMethod.positive,
635
- )
636
-
637
-
638
- def _make_meta(
639
- *,
640
- description: str,
641
- location: str | None = None,
642
- parameter: str | None = None,
643
- parameter_location: str | None = None,
644
- query: DataGenerationMethod | None = None,
645
- path_parameters: DataGenerationMethod | None = None,
646
- headers: DataGenerationMethod | None = None,
647
- cookies: DataGenerationMethod | None = None,
648
- body: DataGenerationMethod | None = None,
649
- ) -> GenerationMetadata:
650
- return GenerationMetadata(
651
- query=query,
652
- path_parameters=path_parameters,
653
- headers=headers,
654
- cookies=cookies,
655
- body=body,
656
- phase=TestPhase.COVERAGE,
657
- description=description,
658
- location=location,
659
- parameter=parameter,
660
- parameter_location=parameter_location,
661
- )
662
-
663
-
664
- def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
665
- for name, value in headers.items():
666
- if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
667
- yield name, value
668
-
669
-
670
- def prepare_urlencoded(data: Any) -> Any:
671
- if isinstance(data, list):
672
- output = []
673
- for item in data:
674
- if isinstance(item, dict):
675
- for key, value in item.items():
676
- output.append((key, value))
677
- else:
678
- output.append((item, "arbitrary-value"))
679
- return output
680
- return data
681
-
682
-
683
- def add_unsatisfied_example_mark(test: Callable, exc: Unsatisfiable) -> None:
684
- test._schemathesis_unsatisfied_example = exc # type: ignore
685
-
686
-
687
- def has_unsatisfied_example_mark(test: Callable) -> bool:
688
- return hasattr(test, "_schemathesis_unsatisfied_example")
689
-
690
-
691
- def add_non_serializable_mark(test: Callable, exc: SerializationNotPossible) -> None:
692
- test._schemathesis_non_serializable = exc # type: ignore
693
-
694
-
695
- def get_non_serializable_mark(test: Callable) -> SerializationNotPossible | None:
696
- return getattr(test, "_schemathesis_non_serializable", None)
697
-
698
-
699
- def get_invalid_regex_mark(test: Callable) -> SchemaError | None:
700
- return getattr(test, "_schemathesis_invalid_regex", None)
701
-
702
-
703
- def add_invalid_regex_mark(test: Callable, exc: SchemaError) -> None:
704
- test._schemathesis_invalid_regex = exc # type: ignore
705
-
706
-
707
- def get_invalid_example_headers_mark(test: Callable) -> dict[str, str] | None:
708
- return getattr(test, "_schemathesis_invalid_example_headers", None)
709
-
710
-
711
- def add_invalid_example_header_mark(test: Callable, headers: dict[str, str]) -> None:
712
- test._schemathesis_invalid_example_headers = headers # type: ignore