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