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
@@ -3,29 +3,38 @@ from __future__ import annotations
3
3
  import time
4
4
  from contextlib import suppress
5
5
  from dataclasses import dataclass
6
- from typing import Any, Callable, Dict, Iterable, Optional
6
+ from typing import Any, Callable, Dict, Iterable, Optional, Union, cast
7
7
  from urllib.parse import quote_plus
8
8
  from weakref import WeakKeyDictionary
9
9
 
10
- from hypothesis import reject
10
+ import jsonschema.protocols
11
+ from hypothesis import event, note, reject
11
12
  from hypothesis import strategies as st
12
13
  from hypothesis_jsonschema import from_schema
13
14
  from requests.structures import CaseInsensitiveDict
14
- from requests.utils import to_key_val_list
15
15
 
16
- from ... import auths, serializers
17
- from ..._hypothesis import prepare_urlencoded
18
- from ...constants import NOT_SET
19
- from ...exceptions import BodyInGetRequestError, SerializationNotPossible
20
- from ...generation import DataGenerationMethod, GenerationConfig
16
+ from schemathesis.config import GenerationConfig
17
+ from schemathesis.core import NOT_SET, NotSet, media_types
18
+ from schemathesis.core.control import SkipTest
19
+ from schemathesis.core.errors import SERIALIZERS_SUGGESTION_MESSAGE, SerializationNotPossible
20
+ from schemathesis.core.transforms import deepclone
21
+ from schemathesis.core.transport import prepare_urlencoded
22
+ from schemathesis.generation.meta import (
23
+ CaseMetadata,
24
+ ComponentInfo,
25
+ ComponentKind,
26
+ ExplicitPhaseData,
27
+ GeneratePhaseData,
28
+ GenerationInfo,
29
+ PhaseInfo,
30
+ TestPhase,
31
+ )
32
+ from schemathesis.openapi.generation.filters import is_valid_header, is_valid_path, is_valid_query, is_valid_urlencoded
33
+ from schemathesis.schemas import APIOperation
34
+
35
+ from ... import auths
36
+ from ...generation import GenerationMode
21
37
  from ...hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
22
- from ...internal.copy import fast_deepcopy
23
- from ...internal.validation import is_illegal_surrogate
24
- from ...models import APIOperation, Case, GenerationMetadata, TestPhase, cant_serialize
25
- from ...transports.content_types import parse_content_type
26
- from ...transports.headers import has_invalid_characters, is_latin_1_encodable
27
- from ...types import NotSet
28
- from ...utils import skip
29
38
  from .constants import LOCATION_TO_CONTAINER
30
39
  from .formats import HEADER_FORMAT, STRING_FORMATS, get_default_format_strategies, header_values
31
40
  from .media_types import MEDIA_TYPES
@@ -35,57 +44,27 @@ from .parameters import OpenAPIBody, OpenAPIParameter, parameters_to_json_schema
35
44
  from .utils import is_header_location
36
45
 
37
46
  SLASH = "/"
38
- StrategyFactory = Callable[[Dict[str, Any], str, str, Optional[str], GenerationConfig], st.SearchStrategy]
39
-
40
-
41
- def is_valid_header(headers: dict[str, Any]) -> bool:
42
- """Verify if the generated headers are valid."""
43
- for name, value in headers.items():
44
- if not is_latin_1_encodable(value):
45
- return False
46
- if has_invalid_characters(name, value):
47
- return False
48
- return True
49
-
50
-
51
- def is_valid_query(query: dict[str, Any]) -> bool:
52
- """Surrogates are not allowed in a query string.
53
-
54
- `requests` and `werkzeug` will fail to send it to the application.
55
- """
56
- for name, value in query.items():
57
- if is_illegal_surrogate(name) or is_illegal_surrogate(value):
58
- return False
59
- return True
60
-
61
-
62
- def is_valid_urlencoded(data: Any) -> bool:
63
- if data is NOT_SET:
64
- return True
65
- try:
66
- for _, __ in to_key_val_list(data): # type: ignore[no-untyped-call]
67
- pass
68
- return True
69
- except (TypeError, ValueError):
70
- return False
47
+ StrategyFactory = Callable[
48
+ [Dict[str, Any], str, str, Optional[str], GenerationConfig, type[jsonschema.protocols.Validator]], st.SearchStrategy
49
+ ]
71
50
 
72
51
 
73
52
  @st.composite # type: ignore
74
- def get_case_strategy(
53
+ def openapi_cases(
75
54
  draw: Callable,
55
+ *,
76
56
  operation: APIOperation,
77
57
  hooks: HookDispatcher | None = None,
78
58
  auth_storage: auths.AuthStorage | None = None,
79
- generator: DataGenerationMethod = DataGenerationMethod.default(),
80
- generation_config: GenerationConfig | None = None,
59
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
81
60
  path_parameters: NotSet | dict[str, Any] = NOT_SET,
82
61
  headers: NotSet | dict[str, Any] = NOT_SET,
83
62
  cookies: NotSet | dict[str, Any] = NOT_SET,
84
63
  query: NotSet | dict[str, Any] = NOT_SET,
85
64
  body: Any = NOT_SET,
86
65
  media_type: str | None = None,
87
- skip_on_not_negated: bool = True,
88
- phase: TestPhase = TestPhase.GENERATE,
66
+ phase: TestPhase = TestPhase.FUZZING,
67
+ __is_stateful_phase: bool = False,
89
68
  ) -> Any:
90
69
  """A strategy that creates `Case` instances.
91
70
 
@@ -100,46 +79,58 @@ def get_case_strategy(
100
79
  as it works with `body`.
101
80
  """
102
81
  start = time.monotonic()
103
- strategy_factory = DATA_GENERATION_METHOD_TO_STRATEGY_FACTORY[generator]
82
+ strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generation_mode]
104
83
 
105
- context = HookContext(operation)
84
+ phase_name = "stateful" if __is_stateful_phase else phase.value
85
+ generation_config = operation.schema.config.generation_for(operation=operation, phase=phase_name)
106
86
 
107
- generation_config = generation_config or operation.schema.generation_config
87
+ ctx = HookContext(operation=operation)
108
88
 
109
89
  path_parameters_ = generate_parameter(
110
- "path", path_parameters, operation, draw, context, hooks, generator, generation_config
90
+ "path", path_parameters, operation, draw, ctx, hooks, generation_mode, generation_config
111
91
  )
112
- headers_ = generate_parameter("header", headers, operation, draw, context, hooks, generator, generation_config)
113
- cookies_ = generate_parameter("cookie", cookies, operation, draw, context, hooks, generator, generation_config)
114
- query_ = generate_parameter("query", query, operation, draw, context, hooks, generator, generation_config)
92
+ headers_ = generate_parameter("header", headers, operation, draw, ctx, hooks, generation_mode, generation_config)
93
+ cookies_ = generate_parameter("cookie", cookies, operation, draw, ctx, hooks, generation_mode, generation_config)
94
+ query_ = generate_parameter("query", query, operation, draw, ctx, hooks, generation_mode, generation_config)
115
95
 
116
96
  if body is NOT_SET:
117
97
  if operation.body:
118
- body_generator = generator
119
- if generator.is_negative:
98
+ body_generator = generation_mode
99
+ if generation_mode.is_negative:
120
100
  # Consider only schemas that are possible to negate
121
101
  candidates = [item for item in operation.body.items if can_negate(item.as_json_schema(operation))]
122
102
  # Not possible to negate body, fallback to positive data generation
123
103
  if not candidates:
124
104
  candidates = operation.body.items
125
105
  strategy_factory = make_positive_strategy
126
- body_generator = DataGenerationMethod.positive
106
+ body_generator = GenerationMode.POSITIVE
127
107
  else:
128
108
  candidates = operation.body.items
129
109
  parameter = draw(st.sampled_from(candidates))
130
110
  strategy = _get_body_strategy(parameter, strategy_factory, operation, generation_config)
131
- strategy = apply_hooks(operation, context, hooks, strategy, "body")
111
+ strategy = apply_hooks(operation, ctx, hooks, strategy, "body")
132
112
  # Parameter may have a wildcard media type. In this case, choose any supported one
133
- possible_media_types = sorted(serializers.get_matching_media_types(parameter.media_type))
113
+ possible_media_types = sorted(
114
+ operation.schema.transport.get_matching_media_types(parameter.media_type), key=lambda x: x[0]
115
+ )
134
116
  if not possible_media_types:
135
117
  all_media_types = operation.get_request_payload_content_types()
136
- if all(serializers.get_first_matching_media_type(media_type) is None for media_type in all_media_types):
118
+ if all(
119
+ operation.schema.transport.get_first_matching_media_type(media_type) is None
120
+ for media_type in all_media_types
121
+ ):
137
122
  # None of media types defined for this operation are not supported
138
123
  raise SerializationNotPossible.from_media_types(*all_media_types)
139
124
  # Other media types are possible - avoid choosing this media type in the future
140
- cant_serialize(parameter.media_type)
141
- media_type = draw(st.sampled_from(possible_media_types))
142
- if media_type is not None and parse_content_type(media_type) == ("application", "x-www-form-urlencoded"):
125
+ event_text = f"Can't serialize data to `{parameter.media_type}`."
126
+ note(f"{event_text} {SERIALIZERS_SUGGESTION_MESSAGE}")
127
+ event(event_text)
128
+ reject() # type: ignore
129
+ media_type, _ = draw(st.sampled_from(possible_media_types))
130
+ if media_type is not None and media_types.parse(media_type) == (
131
+ "application",
132
+ "x-www-form-urlencoded",
133
+ ):
143
134
  strategy = strategy.map(prepare_urlencoded).filter(is_valid_urlencoded)
144
135
  body_ = ValueContainer(value=draw(strategy), location="body", generator=body_generator)
145
136
  else:
@@ -152,35 +143,43 @@ def get_case_strategy(
152
143
  raise SerializationNotPossible.from_media_types(*all_media_types)
153
144
  body_ = ValueContainer(value=body, location="body", generator=None)
154
145
 
155
- if operation.schema.validate_schema and operation.method.upper() == "GET" and operation.body:
156
- raise BodyInGetRequestError("GET requests should not contain body parameters.")
157
146
  # If we need to generate negative cases but no generated values were negated, then skip the whole test
158
- if generator.is_negative and not any_negated_values([query_, cookies_, headers_, path_parameters_, body_]):
159
- if skip_on_not_negated:
160
- skip(operation.verbose_name)
147
+ if generation_mode.is_negative and not any_negated_values([query_, cookies_, headers_, path_parameters_, body_]):
148
+ if generation_config.modes == [GenerationMode.NEGATIVE]:
149
+ raise SkipTest("Impossible to generate negative test cases")
161
150
  else:
162
151
  reject()
163
- instance = Case(
164
- operation=operation,
165
- generation_time=time.monotonic() - start,
152
+
153
+ _phase_data = {
154
+ TestPhase.EXAMPLES: ExplicitPhaseData(),
155
+ TestPhase.FUZZING: GeneratePhaseData(),
156
+ }[phase]
157
+ phase_data = cast(Union[ExplicitPhaseData, GeneratePhaseData], _phase_data)
158
+
159
+ instance = operation.Case(
166
160
  media_type=media_type,
167
- path_parameters=path_parameters_.value,
168
- headers=CaseInsensitiveDict(headers_.value) if headers_.value is not None else headers_.value,
169
- cookies=cookies_.value,
170
- query=query_.value,
161
+ path_parameters=path_parameters_.value or {},
162
+ headers=headers_.value or CaseInsensitiveDict(),
163
+ cookies=cookies_.value or {},
164
+ query=query_.value or {},
171
165
  body=body_.value,
172
- data_generation_method=generator,
173
- meta=GenerationMetadata(
174
- query=query_.generator,
175
- path_parameters=path_parameters_.generator,
176
- headers=headers_.generator,
177
- cookies=cookies_.generator,
178
- body=body_.generator,
179
- phase=phase,
180
- description=None,
181
- location=None,
182
- parameter=None,
183
- parameter_location=None,
166
+ _meta=CaseMetadata(
167
+ generation=GenerationInfo(
168
+ time=time.monotonic() - start,
169
+ mode=generation_mode,
170
+ ),
171
+ phase=PhaseInfo(name=phase, data=phase_data),
172
+ components={
173
+ kind: ComponentInfo(mode=value.generator)
174
+ for kind, value in [
175
+ (ComponentKind.QUERY, query_),
176
+ (ComponentKind.PATH_PARAMETERS, path_parameters_),
177
+ (ComponentKind.HEADERS, headers_),
178
+ (ComponentKind.COOKIES, cookies_),
179
+ (ComponentKind.BODY, body_),
180
+ ]
181
+ if value.generator is not None
182
+ },
184
183
  ),
185
184
  )
186
185
  auth_context = auths.AuthContext(
@@ -200,6 +199,8 @@ def _get_body_strategy(
200
199
  operation: APIOperation,
201
200
  generation_config: GenerationConfig,
202
201
  ) -> st.SearchStrategy:
202
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
203
+
203
204
  if parameter.media_type in MEDIA_TYPES:
204
205
  return MEDIA_TYPES[parameter.media_type]
205
206
  # The cache key relies on object ids, which means that the parameter should not be mutated
@@ -208,7 +209,10 @@ def _get_body_strategy(
208
209
  return _BODY_STRATEGIES_CACHE[parameter][strategy_factory]
209
210
  schema = parameter.as_json_schema(operation)
210
211
  schema = operation.schema.prepare_schema(schema)
211
- strategy = strategy_factory(schema, operation.verbose_name, "body", parameter.media_type, generation_config)
212
+ assert isinstance(operation.schema, BaseOpenAPISchema)
213
+ strategy = strategy_factory(
214
+ schema, operation.label, "body", parameter.media_type, generation_config, operation.schema.validator_cls
215
+ )
212
216
  if not parameter.is_required:
213
217
  strategy |= st.just(NOT_SET)
214
218
  _BODY_STRATEGIES_CACHE.setdefault(parameter, {})[strategy_factory] = strategy
@@ -220,7 +224,7 @@ def get_parameters_value(
220
224
  location: str,
221
225
  draw: Callable,
222
226
  operation: APIOperation,
223
- context: HookContext,
227
+ ctx: HookContext,
224
228
  hooks: HookDispatcher | None,
225
229
  strategy_factory: StrategyFactory,
226
230
  generation_config: GenerationConfig,
@@ -232,13 +236,13 @@ def get_parameters_value(
232
236
  """
233
237
  if isinstance(value, NotSet) or not value:
234
238
  strategy = get_parameters_strategy(operation, strategy_factory, location, generation_config)
235
- strategy = apply_hooks(operation, context, hooks, strategy, location)
239
+ strategy = apply_hooks(operation, ctx, hooks, strategy, location)
236
240
  return draw(strategy)
237
241
  strategy = get_parameters_strategy(operation, strategy_factory, location, generation_config, exclude=value.keys())
238
- strategy = apply_hooks(operation, context, hooks, strategy, location)
242
+ strategy = apply_hooks(operation, ctx, hooks, strategy, location)
239
243
  new = draw(strategy)
240
244
  if new is not None:
241
- copied = fast_deepcopy(value)
245
+ copied = deepclone(value)
242
246
  copied.update(new)
243
247
  return copied
244
248
  return value
@@ -253,7 +257,7 @@ class ValueContainer:
253
257
 
254
258
  value: Any
255
259
  location: str
256
- generator: DataGenerationMethod | None
260
+ generator: GenerationMode | None
257
261
 
258
262
  __slots__ = ("value", "location", "generator")
259
263
 
@@ -265,7 +269,7 @@ class ValueContainer:
265
269
 
266
270
  def any_negated_values(values: list[ValueContainer]) -> bool:
267
271
  """Check if any generated values are negated."""
268
- return any(value.generator == DataGenerationMethod.negative for value in values if value.is_generated)
272
+ return any(value.generator == GenerationMode.NEGATIVE for value in values if value.is_generated)
269
273
 
270
274
 
271
275
  def generate_parameter(
@@ -273,9 +277,9 @@ def generate_parameter(
273
277
  explicit: NotSet | dict[str, Any],
274
278
  operation: APIOperation,
275
279
  draw: Callable,
276
- context: HookContext,
280
+ ctx: HookContext,
277
281
  hooks: HookDispatcher | None,
278
- generator: DataGenerationMethod,
282
+ generator: GenerationMode,
279
283
  generation_config: GenerationConfig,
280
284
  ) -> ValueContainer:
281
285
  """Generate a value for a parameter.
@@ -289,13 +293,11 @@ def generate_parameter(
289
293
  # If we can't negate any parameter, generate positive ones
290
294
  # If nothing else will be negated, then skip the test completely
291
295
  strategy_factory = make_positive_strategy
292
- generator = DataGenerationMethod.positive
296
+ generator = GenerationMode.POSITIVE
293
297
  else:
294
- strategy_factory = DATA_GENERATION_METHOD_TO_STRATEGY_FACTORY[generator]
295
- value = get_parameters_value(
296
- explicit, location, draw, operation, context, hooks, strategy_factory, generation_config
297
- )
298
- used_generator: DataGenerationMethod | None = generator
298
+ strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generator]
299
+ value = get_parameters_value(explicit, location, draw, operation, ctx, hooks, strategy_factory, generation_config)
300
+ used_generator: GenerationMode | None = generator
299
301
  if value == explicit:
300
302
  # When we pass `explicit`, then its parts are excluded from generation of the final value
301
303
  # If the final value is the same, then other parameters were generated at all
@@ -329,11 +331,7 @@ def get_schema_for_location(
329
331
  ) -> dict[str, Any]:
330
332
  schema = parameters_to_json_schema(operation, parameters)
331
333
  if location == "path":
332
- if not operation.schema.validate_schema:
333
- # If schema validation is disabled, we try to generate data even if the parameter definition
334
- # contains errors.
335
- # In this case, we know that the `required` keyword should always be `True`.
336
- schema["required"] = list(schema["properties"])
334
+ schema["required"] = list(schema["properties"])
337
335
  for prop in schema.get("properties", {}).values():
338
336
  if prop.get("type") == "string":
339
337
  prop.setdefault("minLength", 1)
@@ -348,6 +346,8 @@ def get_parameters_strategy(
348
346
  exclude: Iterable[str] = (),
349
347
  ) -> st.SearchStrategy:
350
348
  """Create a new strategy for the case's component from the API operation parameters."""
349
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
350
+
351
351
  parameters = getattr(operation, LOCATION_TO_CONTAINER[location])
352
352
  if parameters:
353
353
  # The cache key relies on object ids, which means that the parameter should not be mutated
@@ -355,17 +355,28 @@ def get_parameters_strategy(
355
355
  if operation in _PARAMETER_STRATEGIES_CACHE and nested_cache_key in _PARAMETER_STRATEGIES_CACHE[operation]:
356
356
  return _PARAMETER_STRATEGIES_CACHE[operation][nested_cache_key]
357
357
  schema = get_schema_for_location(operation, location, parameters)
358
- for name in exclude:
359
- # Values from `exclude` are not necessarily valid for the schema - they come from user-defined examples
360
- # that may be invalid
361
- schema["properties"].pop(name, None)
362
- with suppress(ValueError):
363
- schema["required"].remove(name)
358
+ if location == "header" and exclude:
359
+ # Remove excluded headers case-insensitively
360
+ exclude_lower = {name.lower() for name in exclude}
361
+ schema["properties"] = {
362
+ key: value for key, value in schema["properties"].items() if key.lower() not in exclude_lower
363
+ }
364
+ if "required" in schema:
365
+ schema["required"] = [key for key in schema["required"] if key.lower() not in exclude_lower]
366
+ elif exclude:
367
+ # Non-header locations: remove by exact name
368
+ for name in exclude:
369
+ schema["properties"].pop(name, None)
370
+ with suppress(ValueError):
371
+ schema["required"].remove(name)
364
372
  if not schema["properties"] and strategy_factory is make_negative_strategy:
365
373
  # Nothing to negate - all properties were excluded
366
374
  strategy = st.none()
367
375
  else:
368
- strategy = strategy_factory(schema, operation.verbose_name, location, None, generation_config)
376
+ assert isinstance(operation.schema, BaseOpenAPISchema)
377
+ strategy = strategy_factory(
378
+ schema, operation.label, location, None, generation_config, operation.schema.validator_cls
379
+ )
369
380
  serialize = operation.get_parameter_serializer(location)
370
381
  if serialize is not None:
371
382
  strategy = strategy.map(serialize)
@@ -416,10 +427,10 @@ def _build_custom_formats(
416
427
  custom_formats: dict[str, st.SearchStrategy] | None, generation_config: GenerationConfig
417
428
  ) -> dict[str, st.SearchStrategy]:
418
429
  custom_formats = {**get_default_format_strategies(), **STRING_FORMATS, **(custom_formats or {})}
419
- if generation_config.headers.strategy is not None:
420
- custom_formats[HEADER_FORMAT] = generation_config.headers.strategy
430
+ if generation_config.exclude_header_characters is not None:
431
+ custom_formats[HEADER_FORMAT] = header_values(exclude_characters=generation_config.exclude_header_characters)
421
432
  elif not generation_config.allow_x00:
422
- custom_formats[HEADER_FORMAT] = header_values(blacklist_characters="\n\r\x00")
433
+ custom_formats[HEADER_FORMAT] = header_values(exclude_characters="\n\r\x00")
423
434
  return custom_formats
424
435
 
425
436
 
@@ -429,6 +440,7 @@ def make_positive_strategy(
429
440
  location: str,
430
441
  media_type: str | None,
431
442
  generation_config: GenerationConfig,
443
+ validator_cls: type[jsonschema.protocols.Validator],
432
444
  custom_formats: dict[str, st.SearchStrategy] | None = None,
433
445
  ) -> st.SearchStrategy:
434
446
  """Strategy for generating values that fit the schema."""
@@ -459,6 +471,7 @@ def make_negative_strategy(
459
471
  location: str,
460
472
  media_type: str | None,
461
473
  generation_config: GenerationConfig,
474
+ validator_cls: type[jsonschema.protocols.Validator],
462
475
  custom_formats: dict[str, st.SearchStrategy] | None = None,
463
476
  ) -> st.SearchStrategy:
464
477
  custom_formats = _build_custom_formats(custom_formats, generation_config)
@@ -469,38 +482,16 @@ def make_negative_strategy(
469
482
  media_type=media_type,
470
483
  custom_formats=custom_formats,
471
484
  generation_config=generation_config,
485
+ validator_cls=validator_cls,
472
486
  )
473
487
 
474
488
 
475
- DATA_GENERATION_METHOD_TO_STRATEGY_FACTORY = {
476
- DataGenerationMethod.positive: make_positive_strategy,
477
- DataGenerationMethod.negative: make_negative_strategy,
489
+ GENERATOR_MODE_TO_STRATEGY_FACTORY = {
490
+ GenerationMode.POSITIVE: make_positive_strategy,
491
+ GenerationMode.NEGATIVE: make_negative_strategy,
478
492
  }
479
493
 
480
494
 
481
- def is_valid_path(parameters: dict[str, Any]) -> bool:
482
- """Empty strings ("") are excluded from path by urllib3.
483
-
484
- A path containing to "/" or "%2F" will lead to ambiguous path resolution in
485
- many frameworks and libraries, such behaviour have been observed in both
486
- WSGI and ASGI applications.
487
-
488
- In this case one variable in the path template will be empty, which will lead to 404 in most of the cases.
489
- Because of it this case doesn't bring much value and might lead to false positives results of Schemathesis runs.
490
- """
491
- disallowed_values = (SLASH, "")
492
-
493
- return not any(
494
- (
495
- value in disallowed_values
496
- or is_illegal_surrogate(value)
497
- or isinstance(value, str)
498
- and (SLASH in value or "}" in value or "{" in value)
499
- )
500
- for value in parameters.values()
501
- )
502
-
503
-
504
495
  def quote_all(parameters: dict[str, Any]) -> dict[str, Any]:
505
496
  """Apply URL quotation for all values in a dictionary."""
506
497
  # Even though, "." is an unreserved character, it has a special meaning in "." and ".." strings.
@@ -522,16 +513,11 @@ def quote_all(parameters: dict[str, Any]) -> dict[str, Any]:
522
513
 
523
514
  def apply_hooks(
524
515
  operation: APIOperation,
525
- context: HookContext,
516
+ ctx: HookContext,
526
517
  hooks: HookDispatcher | None,
527
518
  strategy: st.SearchStrategy,
528
519
  location: str,
529
520
  ) -> st.SearchStrategy:
530
521
  """Apply all hooks related to the given location."""
531
522
  container = LOCATION_TO_CONTAINER[location]
532
- return apply_to_all_dispatchers(operation, context, hooks, strategy, container)
533
-
534
-
535
- def clear_cache() -> None:
536
- _PARAMETER_STRATEGIES_CACHE.clear()
537
- _BODY_STRATEGIES_CACHE.clear()
523
+ return apply_to_all_dispatchers(operation, ctx, hooks, strategy, container)