schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +26 -68
  3. schemathesis/checks.py +130 -60
  4. schemathesis/cli/__init__.py +5 -2105
  5. schemathesis/cli/commands/__init__.py +37 -0
  6. schemathesis/cli/commands/run/__init__.py +662 -0
  7. schemathesis/cli/commands/run/checks.py +80 -0
  8. schemathesis/cli/commands/run/context.py +117 -0
  9. schemathesis/cli/commands/run/events.py +30 -0
  10. schemathesis/cli/commands/run/executor.py +141 -0
  11. schemathesis/cli/commands/run/filters.py +202 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
  15. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1368 -0
  17. schemathesis/cli/commands/run/hypothesis.py +105 -0
  18. schemathesis/cli/commands/run/loaders.py +129 -0
  19. schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
  20. schemathesis/cli/constants.py +5 -58
  21. schemathesis/cli/core.py +17 -0
  22. schemathesis/cli/ext/fs.py +14 -0
  23. schemathesis/cli/ext/groups.py +55 -0
  24. schemathesis/cli/{options.py → ext/options.py} +37 -16
  25. schemathesis/cli/hooks.py +36 -0
  26. schemathesis/contrib/__init__.py +1 -3
  27. schemathesis/contrib/openapi/__init__.py +1 -3
  28. schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
  29. schemathesis/core/__init__.py +58 -0
  30. schemathesis/core/compat.py +25 -0
  31. schemathesis/core/control.py +2 -0
  32. schemathesis/core/curl.py +58 -0
  33. schemathesis/core/deserialization.py +65 -0
  34. schemathesis/core/errors.py +370 -0
  35. schemathesis/core/failures.py +315 -0
  36. schemathesis/core/fs.py +19 -0
  37. schemathesis/core/loaders.py +104 -0
  38. schemathesis/core/marks.py +66 -0
  39. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  40. schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
  41. schemathesis/core/output/sanitization.py +197 -0
  42. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  43. schemathesis/core/registries.py +31 -0
  44. schemathesis/core/transforms.py +113 -0
  45. schemathesis/core/transport.py +108 -0
  46. schemathesis/core/validation.py +38 -0
  47. schemathesis/core/version.py +7 -0
  48. schemathesis/engine/__init__.py +30 -0
  49. schemathesis/engine/config.py +59 -0
  50. schemathesis/engine/context.py +119 -0
  51. schemathesis/engine/control.py +36 -0
  52. schemathesis/engine/core.py +157 -0
  53. schemathesis/engine/errors.py +394 -0
  54. schemathesis/engine/events.py +243 -0
  55. schemathesis/engine/phases/__init__.py +66 -0
  56. schemathesis/{runner → engine/phases}/probes.py +49 -68
  57. schemathesis/engine/phases/stateful/__init__.py +66 -0
  58. schemathesis/engine/phases/stateful/_executor.py +301 -0
  59. schemathesis/engine/phases/stateful/context.py +85 -0
  60. schemathesis/engine/phases/unit/__init__.py +175 -0
  61. schemathesis/engine/phases/unit/_executor.py +322 -0
  62. schemathesis/engine/phases/unit/_pool.py +74 -0
  63. schemathesis/engine/recorder.py +246 -0
  64. schemathesis/errors.py +31 -0
  65. schemathesis/experimental/__init__.py +9 -40
  66. schemathesis/filters.py +7 -95
  67. schemathesis/generation/__init__.py +3 -3
  68. schemathesis/generation/case.py +190 -0
  69. schemathesis/generation/coverage.py +22 -22
  70. schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
  71. schemathesis/generation/hypothesis/builder.py +585 -0
  72. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  73. schemathesis/generation/hypothesis/given.py +66 -0
  74. schemathesis/generation/hypothesis/reporting.py +14 -0
  75. schemathesis/generation/hypothesis/strategies.py +16 -0
  76. schemathesis/generation/meta.py +115 -0
  77. schemathesis/generation/modes.py +28 -0
  78. schemathesis/generation/overrides.py +96 -0
  79. schemathesis/generation/stateful/__init__.py +20 -0
  80. schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
  81. schemathesis/generation/targets.py +69 -0
  82. schemathesis/graphql/__init__.py +15 -0
  83. schemathesis/graphql/checks.py +109 -0
  84. schemathesis/graphql/loaders.py +131 -0
  85. schemathesis/hooks.py +17 -62
  86. schemathesis/openapi/__init__.py +13 -0
  87. schemathesis/openapi/checks.py +387 -0
  88. schemathesis/openapi/generation/__init__.py +0 -0
  89. schemathesis/openapi/generation/filters.py +63 -0
  90. schemathesis/openapi/loaders.py +178 -0
  91. schemathesis/pytest/__init__.py +5 -0
  92. schemathesis/pytest/control_flow.py +7 -0
  93. schemathesis/pytest/lazy.py +273 -0
  94. schemathesis/pytest/loaders.py +12 -0
  95. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
  96. schemathesis/python/__init__.py +0 -0
  97. schemathesis/python/asgi.py +12 -0
  98. schemathesis/python/wsgi.py +12 -0
  99. schemathesis/schemas.py +456 -228
  100. schemathesis/specs/graphql/__init__.py +0 -1
  101. schemathesis/specs/graphql/_cache.py +1 -2
  102. schemathesis/specs/graphql/scalars.py +5 -3
  103. schemathesis/specs/graphql/schemas.py +122 -123
  104. schemathesis/specs/graphql/validation.py +11 -17
  105. schemathesis/specs/openapi/__init__.py +6 -1
  106. schemathesis/specs/openapi/_cache.py +1 -2
  107. schemathesis/specs/openapi/_hypothesis.py +97 -134
  108. schemathesis/specs/openapi/checks.py +238 -219
  109. schemathesis/specs/openapi/converter.py +4 -4
  110. schemathesis/specs/openapi/definitions.py +1 -1
  111. schemathesis/specs/openapi/examples.py +22 -20
  112. schemathesis/specs/openapi/expressions/__init__.py +11 -15
  113. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  114. schemathesis/specs/openapi/expressions/nodes.py +33 -32
  115. schemathesis/specs/openapi/formats.py +3 -2
  116. schemathesis/specs/openapi/links.py +123 -299
  117. schemathesis/specs/openapi/media_types.py +10 -12
  118. schemathesis/specs/openapi/negative/__init__.py +2 -1
  119. schemathesis/specs/openapi/negative/mutations.py +3 -2
  120. schemathesis/specs/openapi/parameters.py +8 -6
  121. schemathesis/specs/openapi/patterns.py +1 -1
  122. schemathesis/specs/openapi/references.py +11 -51
  123. schemathesis/specs/openapi/schemas.py +177 -191
  124. schemathesis/specs/openapi/security.py +1 -1
  125. schemathesis/specs/openapi/serialization.py +10 -6
  126. schemathesis/specs/openapi/stateful/__init__.py +97 -91
  127. schemathesis/transport/__init__.py +104 -0
  128. schemathesis/transport/asgi.py +26 -0
  129. schemathesis/transport/prepare.py +99 -0
  130. schemathesis/transport/requests.py +221 -0
  131. schemathesis/{_xml.py → transport/serialization.py} +69 -7
  132. schemathesis/transport/wsgi.py +165 -0
  133. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
  134. schemathesis-4.0.0a2.dist-info/RECORD +151 -0
  135. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
  136. schemathesis/_compat.py +0 -74
  137. schemathesis/_dependency_versions.py +0 -19
  138. schemathesis/_hypothesis.py +0 -559
  139. schemathesis/_override.py +0 -50
  140. schemathesis/_rate_limiter.py +0 -7
  141. schemathesis/cli/context.py +0 -75
  142. schemathesis/cli/debug.py +0 -27
  143. schemathesis/cli/handlers.py +0 -19
  144. schemathesis/cli/junitxml.py +0 -124
  145. schemathesis/cli/output/__init__.py +0 -1
  146. schemathesis/cli/output/default.py +0 -936
  147. schemathesis/cli/output/short.py +0 -59
  148. schemathesis/cli/reporting.py +0 -79
  149. schemathesis/cli/sanitization.py +0 -26
  150. schemathesis/code_samples.py +0 -151
  151. schemathesis/constants.py +0 -56
  152. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  153. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  154. schemathesis/contrib/unique_data.py +0 -41
  155. schemathesis/exceptions.py +0 -571
  156. schemathesis/extra/_aiohttp.py +0 -28
  157. schemathesis/extra/_flask.py +0 -13
  158. schemathesis/extra/_server.py +0 -18
  159. schemathesis/failures.py +0 -277
  160. schemathesis/fixups/__init__.py +0 -37
  161. schemathesis/fixups/fast_api.py +0 -41
  162. schemathesis/fixups/utf8_bom.py +0 -28
  163. schemathesis/generation/_methods.py +0 -44
  164. schemathesis/graphql.py +0 -3
  165. schemathesis/internal/__init__.py +0 -7
  166. schemathesis/internal/checks.py +0 -84
  167. schemathesis/internal/copy.py +0 -32
  168. schemathesis/internal/datetime.py +0 -5
  169. schemathesis/internal/deprecation.py +0 -38
  170. schemathesis/internal/diff.py +0 -15
  171. schemathesis/internal/extensions.py +0 -27
  172. schemathesis/internal/jsonschema.py +0 -36
  173. schemathesis/internal/transformation.py +0 -26
  174. schemathesis/internal/validation.py +0 -34
  175. schemathesis/lazy.py +0 -474
  176. schemathesis/loaders.py +0 -122
  177. schemathesis/models.py +0 -1341
  178. schemathesis/parameters.py +0 -90
  179. schemathesis/runner/__init__.py +0 -605
  180. schemathesis/runner/events.py +0 -389
  181. schemathesis/runner/impl/__init__.py +0 -3
  182. schemathesis/runner/impl/context.py +0 -104
  183. schemathesis/runner/impl/core.py +0 -1246
  184. schemathesis/runner/impl/solo.py +0 -80
  185. schemathesis/runner/impl/threadpool.py +0 -391
  186. schemathesis/runner/serialization.py +0 -544
  187. schemathesis/sanitization.py +0 -252
  188. schemathesis/serializers.py +0 -328
  189. schemathesis/service/__init__.py +0 -18
  190. schemathesis/service/auth.py +0 -11
  191. schemathesis/service/ci.py +0 -202
  192. schemathesis/service/client.py +0 -133
  193. schemathesis/service/constants.py +0 -38
  194. schemathesis/service/events.py +0 -61
  195. schemathesis/service/extensions.py +0 -224
  196. schemathesis/service/hosts.py +0 -111
  197. schemathesis/service/metadata.py +0 -71
  198. schemathesis/service/models.py +0 -258
  199. schemathesis/service/report.py +0 -255
  200. schemathesis/service/serialization.py +0 -173
  201. schemathesis/service/usage.py +0 -66
  202. schemathesis/specs/graphql/loaders.py +0 -364
  203. schemathesis/specs/openapi/expressions/context.py +0 -16
  204. schemathesis/specs/openapi/loaders.py +0 -708
  205. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  206. schemathesis/specs/openapi/stateful/types.py +0 -14
  207. schemathesis/specs/openapi/validation.py +0 -26
  208. schemathesis/stateful/__init__.py +0 -147
  209. schemathesis/stateful/config.py +0 -97
  210. schemathesis/stateful/context.py +0 -135
  211. schemathesis/stateful/events.py +0 -274
  212. schemathesis/stateful/runner.py +0 -309
  213. schemathesis/stateful/sink.py +0 -68
  214. schemathesis/stateful/statistic.py +0 -22
  215. schemathesis/stateful/validation.py +0 -100
  216. schemathesis/targets.py +0 -77
  217. schemathesis/transports/__init__.py +0 -359
  218. schemathesis/transports/asgi.py +0 -7
  219. schemathesis/transports/auth.py +0 -38
  220. schemathesis/transports/headers.py +0 -36
  221. schemathesis/transports/responses.py +0 -57
  222. schemathesis/types.py +0 -44
  223. schemathesis/utils.py +0 -164
  224. schemathesis-3.39.7.dist-info/RECORD +0 -160
  225. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  226. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  227. /schemathesis/{internal → core}/result.py +0 -0
  228. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
  229. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -1,559 +0,0 @@
1
- """High-level API for creating Hypothesis tests."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- import json
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 . import _patches
19
- from .auths import get_auth_storage_from_test
20
- from .constants import DEFAULT_DEADLINE, NOT_SET
21
- from .exceptions import OperationSchemaError, SerializationNotPossible
22
- from .experimental import COVERAGE_PHASE
23
- from .generation import DataGenerationMethod, GenerationConfig, combine_strategies, coverage, get_single_example
24
- from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher
25
- from .models import APIOperation, Case, GenerationMetadata, TestPhase
26
- from .parameters import ParameterSet
27
- from .transports.content_types import parse_content_type
28
- from .transports.headers import has_invalid_characters, is_latin_1_encodable
29
- from .types import NotSet
30
-
31
- if TYPE_CHECKING:
32
- from .utils import GivenInput
33
-
34
- # Forcefully initializes Hypothesis' global PRNG to avoid races that initialize it
35
- # if e.g. Schemathesis CLI is used with multiple workers
36
- with deterministic_PRNG():
37
- pass
38
-
39
- _patches.install()
40
-
41
-
42
- def create_test(
43
- *,
44
- operation: APIOperation,
45
- test: Callable,
46
- settings: hypothesis.settings | None = None,
47
- seed: int | None = None,
48
- data_generation_methods: list[DataGenerationMethod],
49
- generation_config: GenerationConfig | None = None,
50
- as_strategy_kwargs: dict[str, Any] | None = None,
51
- keep_async_fn: bool = False,
52
- _given_args: tuple[GivenInput, ...] = (),
53
- _given_kwargs: dict[str, GivenInput] | None = None,
54
- ) -> Callable:
55
- """Create a Hypothesis test."""
56
- hook_dispatcher = getattr(test, "_schemathesis_hooks", None)
57
- auth_storage = get_auth_storage_from_test(test)
58
- strategies = []
59
- skip_on_not_negated = len(data_generation_methods) == 1 and DataGenerationMethod.negative in data_generation_methods
60
- as_strategy_kwargs = as_strategy_kwargs or {}
61
- as_strategy_kwargs.update(
62
- {
63
- "hooks": hook_dispatcher,
64
- "auth_storage": auth_storage,
65
- "generation_config": generation_config,
66
- "skip_on_not_negated": skip_on_not_negated,
67
- }
68
- )
69
- for data_generation_method in data_generation_methods:
70
- strategies.append(operation.as_strategy(data_generation_method=data_generation_method, **as_strategy_kwargs))
71
- strategy = combine_strategies(strategies)
72
- _given_kwargs = (_given_kwargs or {}).copy()
73
- _given_kwargs.setdefault("case", strategy)
74
-
75
- # Each generated test should be a unique function. It is especially important for the case when Schemathesis runs
76
- # tests in multiple threads because Hypothesis stores some internal attributes on function objects and re-writing
77
- # them from different threads may lead to unpredictable side-effects.
78
-
79
- @wraps(test)
80
- def test_function(*args: Any, **kwargs: Any) -> Any:
81
- __tracebackhide__ = True
82
- return test(*args, **kwargs)
83
-
84
- wrapped_test = hypothesis.given(*_given_args, **_given_kwargs)(test_function)
85
- if seed is not None:
86
- wrapped_test = hypothesis.seed(seed)(wrapped_test)
87
- if asyncio.iscoroutinefunction(test):
88
- # `pytest-trio` expects a coroutine function
89
- if keep_async_fn:
90
- wrapped_test.hypothesis.inner_test = test # type: ignore
91
- else:
92
- wrapped_test.hypothesis.inner_test = make_async_test(test) # type: ignore
93
- setup_default_deadline(wrapped_test)
94
- if settings is not None:
95
- existing_settings = _get_hypothesis_settings(wrapped_test)
96
- if existing_settings is not None:
97
- # Merge the user-provided settings with the current ones
98
- default = hypothesis.settings.default
99
- wrapped_test._hypothesis_internal_use_settings = hypothesis.settings(
100
- wrapped_test._hypothesis_internal_use_settings,
101
- **{item: value for item, value in settings.__dict__.items() if value != getattr(default, item)},
102
- )
103
- else:
104
- wrapped_test = settings(wrapped_test)
105
- existing_settings = _get_hypothesis_settings(wrapped_test)
106
- if existing_settings is not None:
107
- existing_settings = remove_explain_phase(existing_settings)
108
- wrapped_test._hypothesis_internal_use_settings = existing_settings # type: ignore
109
- if Phase.explicit in existing_settings.phases:
110
- wrapped_test = add_examples(
111
- wrapped_test, operation, hook_dispatcher=hook_dispatcher, as_strategy_kwargs=as_strategy_kwargs
112
- )
113
- if COVERAGE_PHASE.is_enabled:
114
- wrapped_test = add_coverage(wrapped_test, operation, data_generation_methods)
115
- return wrapped_test
116
-
117
-
118
- def setup_default_deadline(wrapped_test: Callable) -> None:
119
- # Quite hacky, but it is the simplest way to set up the default deadline value without affecting non-Schemathesis
120
- # tests globally
121
- existing_settings = _get_hypothesis_settings(wrapped_test)
122
- if existing_settings is not None and existing_settings.deadline == hypothesis.settings.default.deadline:
123
- with warnings.catch_warnings():
124
- warnings.simplefilter("ignore", HypothesisWarning)
125
- new_settings = hypothesis.settings(existing_settings, deadline=DEFAULT_DEADLINE)
126
- wrapped_test._hypothesis_internal_use_settings = new_settings # type: ignore
127
-
128
-
129
- def remove_explain_phase(settings: hypothesis.settings) -> hypothesis.settings:
130
- # The "explain" phase is not supported
131
- if Phase.explain in settings.phases:
132
- phases = tuple(phase for phase in settings.phases if phase != Phase.explain)
133
- return hypothesis.settings(settings, phases=phases)
134
- return settings
135
-
136
-
137
- def _get_hypothesis_settings(test: Callable) -> hypothesis.settings | None:
138
- return getattr(test, "_hypothesis_internal_use_settings", None)
139
-
140
-
141
- def make_async_test(test: Callable) -> Callable:
142
- def async_run(*args: Any, **kwargs: Any) -> None:
143
- try:
144
- loop = asyncio.get_event_loop()
145
- except RuntimeError:
146
- loop = asyncio.new_event_loop()
147
- coro = test(*args, **kwargs)
148
- future = asyncio.ensure_future(coro, loop=loop)
149
- loop.run_until_complete(future)
150
-
151
- return async_run
152
-
153
-
154
- def add_examples(
155
- test: Callable,
156
- operation: APIOperation,
157
- hook_dispatcher: HookDispatcher | None = None,
158
- as_strategy_kwargs: dict[str, Any] | None = None,
159
- ) -> Callable:
160
- """Add examples to the Hypothesis test, if they are specified in the schema."""
161
- from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
162
-
163
- try:
164
- examples: list[Case] = [
165
- get_single_example(strategy)
166
- for strategy in operation.get_strategies_from_examples(as_strategy_kwargs=as_strategy_kwargs)
167
- ]
168
- except (
169
- OperationSchemaError,
170
- HypothesisRefResolutionError,
171
- Unsatisfiable,
172
- SerializationNotPossible,
173
- SchemaError,
174
- ) as exc:
175
- # Invalid schema:
176
- # In this case, the user didn't pass `--validate-schema=false` and see an error in the output anyway,
177
- # and no tests will be executed. For this reason, examples can be skipped
178
- # Recursive references: This test will be skipped anyway
179
- # Unsatisfiable:
180
- # The underlying schema is not satisfiable and test will raise an error for the same reason.
181
- # Skipping this exception here allows us to continue the testing process for other operations.
182
- # Still, we allow running user-defined hooks
183
- examples = []
184
- if isinstance(exc, Unsatisfiable):
185
- add_unsatisfied_example_mark(test, exc)
186
- if isinstance(exc, SerializationNotPossible):
187
- add_non_serializable_mark(test, exc)
188
- if isinstance(exc, SchemaError):
189
- add_invalid_regex_mark(test, exc)
190
- context = HookContext(operation) # context should be passed here instead
191
- GLOBAL_HOOK_DISPATCHER.dispatch("before_add_examples", context, examples)
192
- operation.schema.hooks.dispatch("before_add_examples", context, examples)
193
- if hook_dispatcher:
194
- hook_dispatcher.dispatch("before_add_examples", context, examples)
195
- original_test = test
196
- for example in examples:
197
- if example.headers is not None:
198
- invalid_headers = dict(find_invalid_headers(example.headers))
199
- if invalid_headers:
200
- add_invalid_example_header_mark(original_test, invalid_headers)
201
- continue
202
- adjust_urlencoded_payload(example)
203
- test = hypothesis.example(case=example)(test)
204
- return test
205
-
206
-
207
- def adjust_urlencoded_payload(case: Case) -> None:
208
- if case.media_type is not None:
209
- try:
210
- media_type = parse_content_type(case.media_type)
211
- if media_type == ("application", "x-www-form-urlencoded"):
212
- case.body = prepare_urlencoded(case.body)
213
- except ValueError:
214
- pass
215
-
216
-
217
- def add_coverage(
218
- test: Callable, operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
219
- ) -> Callable:
220
- for example in _iter_coverage_cases(operation, data_generation_methods):
221
- adjust_urlencoded_payload(example)
222
- test = hypothesis.example(case=example)(test)
223
- return test
224
-
225
-
226
- def _iter_coverage_cases(
227
- operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
228
- ) -> Generator[Case, None, None]:
229
- from .specs.openapi.constants import LOCATION_TO_CONTAINER
230
- from .specs.openapi.examples import find_in_responses, find_matching_in_responses
231
-
232
- def _stringify_value(val: Any, location: str) -> str | list[str]:
233
- if isinstance(val, list):
234
- if location == "query":
235
- # Having a list here ensures there will be multiple query parameters wit the same name
236
- return [json.dumps(item) for item in val]
237
- # use comma-separated values style for arrays
238
- return ",".join(json.dumps(sub) for sub in val)
239
- return json.dumps(val)
240
-
241
- generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
242
- template: dict[str, Any] = {}
243
- responses = find_in_responses(operation)
244
- for parameter in operation.iter_parameters():
245
- location = parameter.location
246
- name = parameter.name
247
- schema = parameter.as_json_schema(operation, update_quantifiers=False)
248
- for value in find_matching_in_responses(responses, parameter.name):
249
- schema.setdefault("examples", []).append(value)
250
- gen = coverage.cover_schema_iter(
251
- coverage.CoverageContext(location=location, data_generation_methods=data_generation_methods), schema
252
- )
253
- value = next(gen, NOT_SET)
254
- if isinstance(value, NotSet):
255
- continue
256
- container = template.setdefault(LOCATION_TO_CONTAINER[location], {})
257
- if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
258
- container[name] = _stringify_value(value.value, location)
259
- else:
260
- container[name] = value.value
261
- generators[(location, name)] = gen
262
- if operation.body:
263
- for body in operation.body:
264
- schema = body.as_json_schema(operation, update_quantifiers=False)
265
- # Definition could be a list for Open API 2.0
266
- definition = body.definition if isinstance(body.definition, dict) else {}
267
- examples = [example["value"] for example in definition.get("examples", {}).values() if "value" in example]
268
- if examples:
269
- schema.setdefault("examples", []).extend(examples)
270
- gen = coverage.cover_schema_iter(
271
- coverage.CoverageContext(location="body", data_generation_methods=data_generation_methods), schema
272
- )
273
- value = next(gen, NOT_SET)
274
- if isinstance(value, NotSet):
275
- continue
276
- if "body" not in template:
277
- template["body"] = value.value
278
- template["media_type"] = body.media_type
279
- case = operation.make_case(**{**template, "body": value.value, "media_type": body.media_type})
280
- case.data_generation_method = value.data_generation_method
281
- case.meta = _make_meta(
282
- description=value.description,
283
- location=value.location,
284
- parameter=body.media_type,
285
- parameter_location="body",
286
- )
287
- yield case
288
- for next_value in gen:
289
- case = operation.make_case(**{**template, "body": next_value.value, "media_type": body.media_type})
290
- case.data_generation_method = next_value.data_generation_method
291
- case.meta = _make_meta(
292
- description=next_value.description,
293
- location=next_value.location,
294
- parameter=body.media_type,
295
- parameter_location="body",
296
- )
297
- yield case
298
- elif DataGenerationMethod.positive in data_generation_methods:
299
- case = operation.make_case(**template)
300
- case.data_generation_method = DataGenerationMethod.positive
301
- case.meta = _make_meta(description="Default positive test case")
302
- yield case
303
-
304
- for (location, name), gen in generators.items():
305
- container_name = LOCATION_TO_CONTAINER[location]
306
- container = template[container_name]
307
- for value in gen:
308
- if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
309
- generated = _stringify_value(value.value, location)
310
- else:
311
- generated = value.value
312
- case = operation.make_case(**{**template, container_name: {**container, name: generated}})
313
- case.data_generation_method = value.data_generation_method
314
- case.meta = _make_meta(
315
- description=value.description,
316
- location=value.location,
317
- parameter=name,
318
- parameter_location=location,
319
- )
320
- yield case
321
- if DataGenerationMethod.negative in data_generation_methods:
322
- # Generate HTTP methods that are not specified in the spec
323
- # NOTE: The HEAD method is excluded
324
- methods = {"get", "put", "post", "delete", "options", "patch", "trace"} - set(operation.schema[operation.path])
325
- for method in sorted(methods):
326
- case = operation.make_case(**template)
327
- case._explicit_method = method
328
- case.data_generation_method = DataGenerationMethod.negative
329
- case.meta = _make_meta(description=f"Unspecified HTTP method: {method.upper()}")
330
- yield case
331
- # Generate duplicate query parameters
332
- if operation.query:
333
- container = template["query"]
334
- for parameter in operation.query:
335
- value = container[parameter.name]
336
- case = operation.make_case(**{**template, "query": {**container, parameter.name: [value, value]}})
337
- case.data_generation_method = DataGenerationMethod.negative
338
- case.meta = _make_meta(
339
- description=f"Duplicate `{parameter.name}` query parameter",
340
- location=None,
341
- parameter=parameter.name,
342
- parameter_location="query",
343
- )
344
- yield case
345
- # Generate missing required parameters
346
- for parameter in operation.iter_parameters():
347
- if parameter.is_required and parameter.location != "path":
348
- name = parameter.name
349
- location = parameter.location
350
- container_name = LOCATION_TO_CONTAINER[location]
351
- container = template[container_name]
352
- case = operation.make_case(
353
- **{**template, container_name: {k: v for k, v in container.items() if k != name}}
354
- )
355
- case.data_generation_method = DataGenerationMethod.negative
356
- case.meta = _make_meta(
357
- description=f"Missing `{name}` at {location}",
358
- location=None,
359
- parameter=name,
360
- parameter_location=location,
361
- )
362
- yield case
363
- # Generate combinations for each location
364
- for location, parameter_set in [
365
- ("query", operation.query),
366
- ("header", operation.headers),
367
- ("cookie", operation.cookies),
368
- ]:
369
- if not parameter_set:
370
- continue
371
-
372
- container_name = LOCATION_TO_CONTAINER[location]
373
- base_container = template.get(container_name, {})
374
-
375
- # Get required and optional parameters
376
- required = {p.name for p in parameter_set if p.is_required}
377
- all_params = {p.name for p in parameter_set}
378
- optional = sorted(all_params - required)
379
-
380
- # Helper function to create and yield a case
381
- def make_case(
382
- container_values: dict,
383
- description: str,
384
- _location: str,
385
- _container_name: str,
386
- _parameter: str | None,
387
- _data_generation_method: DataGenerationMethod,
388
- ) -> Case:
389
- if _location in ("header", "cookie", "path", "query"):
390
- container = {
391
- name: _stringify_value(val, _location) if not isinstance(val, str) else val
392
- for name, val in container_values.items()
393
- }
394
- else:
395
- container = container_values
396
-
397
- case = operation.make_case(**{**template, _container_name: container})
398
- case.data_generation_method = _data_generation_method
399
- case.meta = _make_meta(
400
- description=description,
401
- location=None,
402
- parameter=_parameter,
403
- parameter_location=_location,
404
- )
405
- return case
406
-
407
- def _combination_schema(
408
- combination: dict[str, Any], _required: set[str], _parameter_set: ParameterSet
409
- ) -> dict[str, Any]:
410
- return {
411
- "properties": {
412
- parameter.name: parameter.as_json_schema(operation)
413
- for parameter in _parameter_set
414
- if parameter.name in combination
415
- },
416
- "required": list(_required),
417
- "additionalProperties": False,
418
- }
419
-
420
- def _yield_negative(
421
- subschema: dict[str, Any], _location: str, _container_name: str
422
- ) -> Generator[Case, None, None]:
423
- for more in coverage.cover_schema_iter(
424
- coverage.CoverageContext(location=_location, data_generation_methods=[DataGenerationMethod.negative]),
425
- subschema,
426
- ):
427
- yield make_case(
428
- more.value,
429
- more.description,
430
- _location,
431
- _container_name,
432
- more.parameter,
433
- DataGenerationMethod.negative,
434
- )
435
-
436
- # 1. Generate only required properties
437
- if required and all_params != required:
438
- only_required = {k: v for k, v in base_container.items() if k in required}
439
- if DataGenerationMethod.positive in data_generation_methods:
440
- yield make_case(
441
- only_required,
442
- "Only required properties",
443
- location,
444
- container_name,
445
- None,
446
- DataGenerationMethod.positive,
447
- )
448
- if DataGenerationMethod.negative in data_generation_methods:
449
- subschema = _combination_schema(only_required, required, parameter_set)
450
- for case in _yield_negative(subschema, location, container_name):
451
- # Already generated in one of the blocks above
452
- if location != "path" and not case.meta.description.startswith("Missing required property"):
453
- yield case
454
-
455
- # 2. Generate combinations with required properties and one optional property
456
- for opt_param in optional:
457
- combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
458
- if combo != base_container and DataGenerationMethod.positive in data_generation_methods:
459
- yield make_case(
460
- combo,
461
- f"All required properties and optional '{opt_param}'",
462
- location,
463
- container_name,
464
- None,
465
- DataGenerationMethod.positive,
466
- )
467
- if DataGenerationMethod.negative in data_generation_methods:
468
- subschema = _combination_schema(combo, required, parameter_set)
469
- for case in _yield_negative(subschema, location, container_name):
470
- # Already generated in one of the blocks above
471
- if location != "path" and not case.meta.description.startswith("Missing required property"):
472
- yield case
473
-
474
- # 3. Generate one combination for each size from 2 to N-1 of optional parameters
475
- if len(optional) > 1 and DataGenerationMethod.positive in data_generation_methods:
476
- for size in range(2, len(optional)):
477
- for combination in combinations(optional, size):
478
- combo = {k: v for k, v in base_container.items() if k in required or k in combination}
479
- if combo != base_container:
480
- yield make_case(
481
- combo,
482
- f"All required and {size} optional properties",
483
- location,
484
- container_name,
485
- None,
486
- DataGenerationMethod.positive,
487
- )
488
-
489
-
490
- def _make_meta(
491
- *,
492
- description: str,
493
- location: str | None = None,
494
- parameter: str | None = None,
495
- parameter_location: str | None = None,
496
- ) -> GenerationMetadata:
497
- return GenerationMetadata(
498
- query=None,
499
- path_parameters=None,
500
- headers=None,
501
- cookies=None,
502
- body=None,
503
- phase=TestPhase.COVERAGE,
504
- description=description,
505
- location=location,
506
- parameter=parameter,
507
- parameter_location=parameter_location,
508
- )
509
-
510
-
511
- def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
512
- for name, value in headers.items():
513
- if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
514
- yield name, value
515
-
516
-
517
- def prepare_urlencoded(data: Any) -> Any:
518
- if isinstance(data, list):
519
- output = []
520
- for item in data:
521
- if isinstance(item, dict):
522
- for key, value in item.items():
523
- output.append((key, value))
524
- else:
525
- output.append((item, "arbitrary-value"))
526
- return output
527
- return data
528
-
529
-
530
- def add_unsatisfied_example_mark(test: Callable, exc: Unsatisfiable) -> None:
531
- test._schemathesis_unsatisfied_example = exc # type: ignore
532
-
533
-
534
- def has_unsatisfied_example_mark(test: Callable) -> bool:
535
- return hasattr(test, "_schemathesis_unsatisfied_example")
536
-
537
-
538
- def add_non_serializable_mark(test: Callable, exc: SerializationNotPossible) -> None:
539
- test._schemathesis_non_serializable = exc # type: ignore
540
-
541
-
542
- def get_non_serializable_mark(test: Callable) -> SerializationNotPossible | None:
543
- return getattr(test, "_schemathesis_non_serializable", None)
544
-
545
-
546
- def get_invalid_regex_mark(test: Callable) -> SchemaError | None:
547
- return getattr(test, "_schemathesis_invalid_regex", None)
548
-
549
-
550
- def add_invalid_regex_mark(test: Callable, exc: SchemaError) -> None:
551
- test._schemathesis_invalid_regex = exc # type: ignore
552
-
553
-
554
- def get_invalid_example_headers_mark(test: Callable) -> dict[str, str] | None:
555
- return getattr(test, "_schemathesis_invalid_example_headers", None)
556
-
557
-
558
- def add_invalid_example_header_mark(test: Callable, headers: dict[str, str]) -> None:
559
- test._schemathesis_invalid_example_headers = headers # type: ignore
schemathesis/_override.py DELETED
@@ -1,50 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import TYPE_CHECKING
5
-
6
- from .exceptions import UsageError
7
-
8
- if TYPE_CHECKING:
9
- from .models import APIOperation
10
- from .parameters import ParameterSet
11
- from .types import GenericTest
12
-
13
-
14
- @dataclass
15
- class CaseOverride:
16
- """Overrides for various parts of a test case."""
17
-
18
- query: dict[str, str]
19
- headers: dict[str, str]
20
- cookies: dict[str, str]
21
- path_parameters: dict[str, str]
22
-
23
- def for_operation(self, operation: APIOperation) -> dict[str, dict[str, str]]:
24
- return {
25
- "query": (_for_parameters(self.query, operation.query)),
26
- "headers": (_for_parameters(self.headers, operation.headers)),
27
- "cookies": (_for_parameters(self.cookies, operation.cookies)),
28
- "path_parameters": (_for_parameters(self.path_parameters, operation.path_parameters)),
29
- }
30
-
31
-
32
- def _for_parameters(overridden: dict[str, str], defined: ParameterSet) -> dict[str, str]:
33
- output = {}
34
- for param in defined:
35
- if param.name in overridden:
36
- output[param.name] = overridden[param.name]
37
- return output
38
-
39
-
40
- def get_override_from_mark(test: GenericTest) -> CaseOverride | None:
41
- return getattr(test, "_schemathesis_override", None)
42
-
43
-
44
- def set_override_mark(test: GenericTest, override: CaseOverride) -> None:
45
- test._schemathesis_override = override # type: ignore[attr-defined]
46
-
47
-
48
- def check_no_override_mark(test: GenericTest) -> None:
49
- if hasattr(test, "_schemathesis_override"):
50
- raise UsageError(f"`{test.__name__}` has already been decorated with `override`.")
@@ -1,7 +0,0 @@
1
- from ._dependency_versions import IS_PYRATE_LIMITER_ABOVE_3
2
-
3
- if IS_PYRATE_LIMITER_ABOVE_3:
4
- from pyrate_limiter import Limiter, Rate, RateItem
5
- else:
6
- from pyrate_limiter import Limiter
7
- from pyrate_limiter import RequestRate as Rate
@@ -1,75 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import shutil
4
- from dataclasses import dataclass, field
5
- from typing import TYPE_CHECKING, Generator
6
-
7
- from ..code_samples import CodeSampleStyle
8
- from ..internal.deprecation import deprecated_property
9
- from ..internal.output import OutputConfig
10
-
11
- if TYPE_CHECKING:
12
- import os
13
- from queue import Queue
14
-
15
- import hypothesis
16
-
17
- from ..internal.result import Result
18
- from ..runner.probes import ProbeRun
19
- from ..runner.serialization import SerializedTestResult
20
- from ..service.models import AnalysisResult
21
- from ..stateful.sink import StateMachineSink
22
-
23
-
24
- @dataclass
25
- class ServiceReportContext:
26
- queue: Queue
27
- service_base_url: str
28
-
29
-
30
- @dataclass
31
- class FileReportContext:
32
- queue: Queue
33
- filename: str | None = None
34
-
35
-
36
- @dataclass
37
- class ExecutionContext:
38
- """Storage for the current context of the execution."""
39
-
40
- hypothesis_settings: hypothesis.settings
41
- hypothesis_output: list[str] = field(default_factory=list)
42
- workers_num: int = 1
43
- rate_limit: str | None = None
44
- show_trace: bool = False
45
- wait_for_schema: float | None = None
46
- validate_schema: bool = True
47
- operations_processed: int = 0
48
- # It is set in runtime, from the `Initialized` event
49
- operations_count: int | None = None
50
- seed: int | None = None
51
- current_line_length: int = 0
52
- terminal_size: os.terminal_size = field(default_factory=shutil.get_terminal_size)
53
- results: list[SerializedTestResult] = field(default_factory=list)
54
- cassette_path: str | None = None
55
- junit_xml_file: str | None = None
56
- is_interrupted: bool = False
57
- verbosity: int = 0
58
- code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
59
- report: ServiceReportContext | FileReportContext | None = None
60
- probes: list[ProbeRun] | None = None
61
- analysis: Result[AnalysisResult, Exception] | None = None
62
- output_config: OutputConfig = field(default_factory=OutputConfig)
63
- state_machine_sink: StateMachineSink | None = None
64
- initialization_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
65
- summary_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
66
-
67
- @deprecated_property(removed_in="4.0", replacement="show_trace")
68
- def show_errors_tracebacks(self) -> bool:
69
- return self.show_trace
70
-
71
- def add_initialization_line(self, line: str | Generator[str, None, None]) -> None:
72
- self.initialization_lines.append(line)
73
-
74
- def add_summary_line(self, line: str | Generator[str, None, None]) -> None:
75
- self.summary_lines.append(line)