schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +102 -82
  3. schemathesis/checks.py +126 -46
  4. schemathesis/cli/__init__.py +11 -1760
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +37 -0
  7. schemathesis/cli/commands/run/__init__.py +662 -0
  8. schemathesis/cli/commands/run/checks.py +80 -0
  9. schemathesis/cli/commands/run/context.py +117 -0
  10. schemathesis/cli/commands/run/events.py +35 -0
  11. schemathesis/cli/commands/run/executor.py +138 -0
  12. schemathesis/cli/commands/run/filters.py +194 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +18 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  17. schemathesis/cli/commands/run/handlers/output.py +746 -0
  18. schemathesis/cli/commands/run/hypothesis.py +105 -0
  19. schemathesis/cli/commands/run/loaders.py +129 -0
  20. schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
  21. schemathesis/cli/constants.py +5 -52
  22. schemathesis/cli/core.py +17 -0
  23. schemathesis/cli/ext/fs.py +14 -0
  24. schemathesis/cli/ext/groups.py +55 -0
  25. schemathesis/cli/{options.py → ext/options.py} +39 -10
  26. schemathesis/cli/hooks.py +36 -0
  27. schemathesis/contrib/__init__.py +1 -3
  28. schemathesis/contrib/openapi/__init__.py +1 -3
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
  30. schemathesis/core/__init__.py +58 -0
  31. schemathesis/core/compat.py +25 -0
  32. schemathesis/core/control.py +2 -0
  33. schemathesis/core/curl.py +58 -0
  34. schemathesis/core/deserialization.py +65 -0
  35. schemathesis/core/errors.py +370 -0
  36. schemathesis/core/failures.py +285 -0
  37. schemathesis/core/fs.py +19 -0
  38. schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
  39. schemathesis/core/loaders.py +104 -0
  40. schemathesis/core/marks.py +66 -0
  41. schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
  42. schemathesis/core/output/__init__.py +69 -0
  43. schemathesis/core/output/sanitization.py +197 -0
  44. schemathesis/core/rate_limit.py +60 -0
  45. schemathesis/core/registries.py +31 -0
  46. schemathesis/{internal → core}/result.py +1 -1
  47. schemathesis/core/transforms.py +113 -0
  48. schemathesis/core/transport.py +108 -0
  49. schemathesis/core/validation.py +38 -0
  50. schemathesis/core/version.py +7 -0
  51. schemathesis/engine/__init__.py +30 -0
  52. schemathesis/engine/config.py +59 -0
  53. schemathesis/engine/context.py +119 -0
  54. schemathesis/engine/control.py +36 -0
  55. schemathesis/engine/core.py +157 -0
  56. schemathesis/engine/errors.py +394 -0
  57. schemathesis/engine/events.py +337 -0
  58. schemathesis/engine/phases/__init__.py +66 -0
  59. schemathesis/{runner → engine/phases}/probes.py +50 -67
  60. schemathesis/engine/phases/stateful/__init__.py +65 -0
  61. schemathesis/engine/phases/stateful/_executor.py +326 -0
  62. schemathesis/engine/phases/stateful/context.py +85 -0
  63. schemathesis/engine/phases/unit/__init__.py +174 -0
  64. schemathesis/engine/phases/unit/_executor.py +321 -0
  65. schemathesis/engine/phases/unit/_pool.py +74 -0
  66. schemathesis/engine/recorder.py +241 -0
  67. schemathesis/errors.py +31 -0
  68. schemathesis/experimental/__init__.py +18 -14
  69. schemathesis/filters.py +103 -14
  70. schemathesis/generation/__init__.py +21 -37
  71. schemathesis/generation/case.py +190 -0
  72. schemathesis/generation/coverage.py +931 -0
  73. schemathesis/generation/hypothesis/__init__.py +30 -0
  74. schemathesis/generation/hypothesis/builder.py +585 -0
  75. schemathesis/generation/hypothesis/examples.py +50 -0
  76. schemathesis/generation/hypothesis/given.py +66 -0
  77. schemathesis/generation/hypothesis/reporting.py +14 -0
  78. schemathesis/generation/hypothesis/strategies.py +16 -0
  79. schemathesis/generation/meta.py +115 -0
  80. schemathesis/generation/modes.py +28 -0
  81. schemathesis/generation/overrides.py +96 -0
  82. schemathesis/generation/stateful/__init__.py +20 -0
  83. schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
  84. schemathesis/generation/targets.py +69 -0
  85. schemathesis/graphql/__init__.py +15 -0
  86. schemathesis/graphql/checks.py +115 -0
  87. schemathesis/graphql/loaders.py +131 -0
  88. schemathesis/hooks.py +99 -67
  89. schemathesis/openapi/__init__.py +13 -0
  90. schemathesis/openapi/checks.py +412 -0
  91. schemathesis/openapi/generation/__init__.py +0 -0
  92. schemathesis/openapi/generation/filters.py +63 -0
  93. schemathesis/openapi/loaders.py +178 -0
  94. schemathesis/pytest/__init__.py +5 -0
  95. schemathesis/pytest/control_flow.py +7 -0
  96. schemathesis/pytest/lazy.py +273 -0
  97. schemathesis/pytest/loaders.py +12 -0
  98. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
  99. schemathesis/python/__init__.py +0 -0
  100. schemathesis/python/asgi.py +12 -0
  101. schemathesis/python/wsgi.py +12 -0
  102. schemathesis/schemas.py +537 -261
  103. schemathesis/specs/graphql/__init__.py +0 -1
  104. schemathesis/specs/graphql/_cache.py +25 -0
  105. schemathesis/specs/graphql/nodes.py +1 -0
  106. schemathesis/specs/graphql/scalars.py +7 -5
  107. schemathesis/specs/graphql/schemas.py +215 -187
  108. schemathesis/specs/graphql/validation.py +11 -18
  109. schemathesis/specs/openapi/__init__.py +7 -1
  110. schemathesis/specs/openapi/_cache.py +122 -0
  111. schemathesis/specs/openapi/_hypothesis.py +146 -165
  112. schemathesis/specs/openapi/checks.py +565 -67
  113. schemathesis/specs/openapi/converter.py +33 -6
  114. schemathesis/specs/openapi/definitions.py +11 -18
  115. schemathesis/specs/openapi/examples.py +139 -23
  116. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  117. schemathesis/specs/openapi/expressions/context.py +4 -6
  118. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  119. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  120. schemathesis/specs/openapi/expressions/nodes.py +38 -14
  121. schemathesis/specs/openapi/expressions/parser.py +26 -5
  122. schemathesis/specs/openapi/formats.py +45 -0
  123. schemathesis/specs/openapi/links.py +65 -165
  124. schemathesis/specs/openapi/media_types.py +32 -0
  125. schemathesis/specs/openapi/negative/__init__.py +7 -3
  126. schemathesis/specs/openapi/negative/mutations.py +24 -8
  127. schemathesis/specs/openapi/parameters.py +46 -30
  128. schemathesis/specs/openapi/patterns.py +137 -0
  129. schemathesis/specs/openapi/references.py +47 -57
  130. schemathesis/specs/openapi/schemas.py +478 -369
  131. schemathesis/specs/openapi/security.py +25 -7
  132. schemathesis/specs/openapi/serialization.py +11 -6
  133. schemathesis/specs/openapi/stateful/__init__.py +185 -73
  134. schemathesis/specs/openapi/utils.py +6 -1
  135. schemathesis/transport/__init__.py +104 -0
  136. schemathesis/transport/asgi.py +26 -0
  137. schemathesis/transport/prepare.py +99 -0
  138. schemathesis/transport/requests.py +221 -0
  139. schemathesis/{_xml.py → transport/serialization.py} +143 -28
  140. schemathesis/transport/wsgi.py +165 -0
  141. schemathesis-4.0.0a1.dist-info/METADATA +297 -0
  142. schemathesis-4.0.0a1.dist-info/RECORD +152 -0
  143. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
  145. schemathesis/_compat.py +0 -74
  146. schemathesis/_dependency_versions.py +0 -17
  147. schemathesis/_hypothesis.py +0 -246
  148. schemathesis/_override.py +0 -49
  149. schemathesis/cli/cassettes.py +0 -375
  150. schemathesis/cli/context.py +0 -58
  151. schemathesis/cli/debug.py +0 -26
  152. schemathesis/cli/handlers.py +0 -16
  153. schemathesis/cli/junitxml.py +0 -43
  154. schemathesis/cli/output/__init__.py +0 -1
  155. schemathesis/cli/output/default.py +0 -790
  156. schemathesis/cli/output/short.py +0 -44
  157. schemathesis/cli/sanitization.py +0 -20
  158. schemathesis/code_samples.py +0 -149
  159. schemathesis/constants.py +0 -55
  160. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  161. schemathesis/contrib/openapi/formats/uuid.py +0 -15
  162. schemathesis/contrib/unique_data.py +0 -41
  163. schemathesis/exceptions.py +0 -560
  164. schemathesis/extra/_aiohttp.py +0 -27
  165. schemathesis/extra/_flask.py +0 -10
  166. schemathesis/extra/_server.py +0 -17
  167. schemathesis/failures.py +0 -209
  168. schemathesis/fixups/__init__.py +0 -36
  169. schemathesis/fixups/fast_api.py +0 -41
  170. schemathesis/fixups/utf8_bom.py +0 -29
  171. schemathesis/graphql.py +0 -4
  172. schemathesis/internal/__init__.py +0 -7
  173. schemathesis/internal/copy.py +0 -13
  174. schemathesis/internal/datetime.py +0 -5
  175. schemathesis/internal/deprecation.py +0 -34
  176. schemathesis/internal/jsonschema.py +0 -35
  177. schemathesis/internal/transformation.py +0 -15
  178. schemathesis/internal/validation.py +0 -34
  179. schemathesis/lazy.py +0 -361
  180. schemathesis/loaders.py +0 -120
  181. schemathesis/models.py +0 -1234
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -570
  184. schemathesis/runner/events.py +0 -329
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -1035
  187. schemathesis/runner/impl/solo.py +0 -90
  188. schemathesis/runner/impl/threadpool.py +0 -400
  189. schemathesis/runner/serialization.py +0 -411
  190. schemathesis/sanitization.py +0 -248
  191. schemathesis/serializers.py +0 -323
  192. schemathesis/service/__init__.py +0 -18
  193. schemathesis/service/auth.py +0 -11
  194. schemathesis/service/ci.py +0 -201
  195. schemathesis/service/client.py +0 -100
  196. schemathesis/service/constants.py +0 -38
  197. schemathesis/service/events.py +0 -57
  198. schemathesis/service/hosts.py +0 -107
  199. schemathesis/service/metadata.py +0 -46
  200. schemathesis/service/models.py +0 -49
  201. schemathesis/service/report.py +0 -255
  202. schemathesis/service/serialization.py +0 -199
  203. schemathesis/service/usage.py +0 -65
  204. schemathesis/specs/graphql/loaders.py +0 -344
  205. schemathesis/specs/openapi/filters.py +0 -49
  206. schemathesis/specs/openapi/loaders.py +0 -667
  207. schemathesis/specs/openapi/stateful/links.py +0 -92
  208. schemathesis/specs/openapi/validation.py +0 -25
  209. schemathesis/stateful/__init__.py +0 -133
  210. schemathesis/targets.py +0 -45
  211. schemathesis/throttling.py +0 -41
  212. schemathesis/transports/__init__.py +0 -5
  213. schemathesis/transports/auth.py +0 -15
  214. schemathesis/transports/headers.py +0 -35
  215. schemathesis/transports/responses.py +0 -52
  216. schemathesis/types.py +0 -35
  217. schemathesis/utils.py +0 -169
  218. schemathesis-3.25.6.dist-info/METADATA +0 -356
  219. schemathesis-3.25.6.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,122 +1,65 @@
1
1
  from __future__ import annotations
2
- import string
3
- from base64 import b64encode
2
+
3
+ import time
4
4
  from contextlib import suppress
5
5
  from dataclasses import dataclass
6
- from functools import lru_cache
7
- from typing import Any, Callable, Dict, Iterable, Optional
6
+ from typing import Any, Callable, Dict, Iterable, Optional, Union, cast
8
7
  from urllib.parse import quote_plus
9
8
  from weakref import WeakKeyDictionary
10
9
 
11
- from hypothesis import strategies as st, reject
10
+ from hypothesis import event, note, reject
11
+ from hypothesis import strategies as st
12
12
  from hypothesis_jsonschema import from_schema
13
- from requests.auth import _basic_auth_str
14
- from requests.structures import CaseInsensitiveDict
15
- from requests.utils import to_key_val_list
16
-
17
- from ..._hypothesis import prepare_urlencoded
18
- from ...constants import NOT_SET
19
- from .formats import STRING_FORMATS
20
- from ... import auths, serializers
21
- from ...generation import DataGenerationMethod, GenerationConfig
22
- from ...internal.copy import fast_deepcopy
23
- from ...exceptions import SerializationNotPossible, BodyInGetRequestError
13
+
14
+ from schemathesis.core import NOT_SET, NotSet, media_types
15
+ from schemathesis.core.control import SkipTest
16
+ from schemathesis.core.errors import SERIALIZERS_SUGGESTION_MESSAGE, SerializationNotPossible
17
+ from schemathesis.core.transforms import deepclone
18
+ from schemathesis.core.transport import prepare_urlencoded
19
+ from schemathesis.generation.meta import (
20
+ CaseMetadata,
21
+ ComponentInfo,
22
+ ComponentKind,
23
+ ExplicitPhaseData,
24
+ GeneratePhaseData,
25
+ GenerationInfo,
26
+ PhaseInfo,
27
+ TestPhase,
28
+ )
29
+ from schemathesis.openapi.generation.filters import is_valid_header, is_valid_path, is_valid_query, is_valid_urlencoded
30
+ from schemathesis.schemas import APIOperation
31
+
32
+ from ... import auths
33
+ from ...generation import GenerationConfig, GenerationMode
24
34
  from ...hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
25
- from ...internal.validation import is_illegal_surrogate
26
- from ...models import APIOperation, Case, cant_serialize
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
- from ...serializers import Binary
31
- from ...utils import compose, skip
32
35
  from .constants import LOCATION_TO_CONTAINER
36
+ from .formats import HEADER_FORMAT, STRING_FORMATS, get_default_format_strategies, header_values
37
+ from .media_types import MEDIA_TYPES
33
38
  from .negative import negative_schema
34
39
  from .negative.utils import can_negate
35
- from .parameters import OpenAPIBody, parameters_to_json_schema
40
+ from .parameters import OpenAPIBody, OpenAPIParameter, parameters_to_json_schema
36
41
  from .utils import is_header_location
37
42
 
38
- HEADER_FORMAT = "_header_value"
39
43
  SLASH = "/"
40
44
  StrategyFactory = Callable[[Dict[str, Any], str, str, Optional[str], GenerationConfig], st.SearchStrategy]
41
45
 
42
46
 
43
- def header_values(blacklist_characters: str = "\n\r") -> st.SearchStrategy[str]:
44
- return st.text(alphabet=st.characters(min_codepoint=0, max_codepoint=255, blacklist_characters="\n\r"))
45
-
46
-
47
- @lru_cache
48
- def get_default_format_strategies() -> dict[str, st.SearchStrategy]:
49
- """Get all default "format" strategies."""
50
-
51
- def make_basic_auth_str(item: tuple[str, str]) -> str:
52
- return _basic_auth_str(*item)
53
-
54
- latin1_text = st.text(alphabet=st.characters(min_codepoint=0, max_codepoint=255))
55
-
56
- # Define valid characters here to avoid filtering them out in `is_valid_header` later
57
- header_value = header_values()
58
-
59
- return {
60
- "binary": st.binary().map(Binary),
61
- "byte": st.binary().map(lambda x: b64encode(x).decode()),
62
- # RFC 7230, Section 3.2.6
63
- "_header_name": st.text(
64
- min_size=1, alphabet=st.sampled_from("!#$%&'*+-.^_`|~" + string.digits + string.ascii_letters)
65
- ),
66
- # Header values with leading non-visible chars can't be sent with `requests`
67
- HEADER_FORMAT: header_value.map(str.lstrip),
68
- "_basic_auth": st.tuples(latin1_text, latin1_text).map(make_basic_auth_str),
69
- "_bearer_auth": header_value.map("Bearer {}".format),
70
- }
71
-
72
-
73
- def is_valid_header(headers: dict[str, Any]) -> bool:
74
- """Verify if the generated headers are valid."""
75
- for name, value in headers.items():
76
- if not is_latin_1_encodable(value):
77
- return False
78
- if has_invalid_characters(name, value):
79
- return False
80
- return True
81
-
82
-
83
- def is_valid_query(query: dict[str, Any]) -> bool:
84
- """Surrogates are not allowed in a query string.
85
-
86
- `requests` and `werkzeug` will fail to send it to the application.
87
- """
88
- for name, value in query.items():
89
- if is_illegal_surrogate(name) or is_illegal_surrogate(value):
90
- return False
91
- return True
92
-
93
-
94
- def is_valid_urlencoded(data: Any) -> bool:
95
- if data is NOT_SET:
96
- return True
97
- try:
98
- for _, __ in to_key_val_list(data): # type: ignore[no-untyped-call]
99
- pass
100
- return True
101
- except (TypeError, ValueError):
102
- return False
103
-
104
-
105
47
  @st.composite # type: ignore
106
- def get_case_strategy(
48
+ def openapi_cases(
107
49
  draw: Callable,
50
+ *,
108
51
  operation: APIOperation,
109
52
  hooks: HookDispatcher | None = None,
110
53
  auth_storage: auths.AuthStorage | None = None,
111
- generator: DataGenerationMethod = DataGenerationMethod.default(),
112
- generation_config: GenerationConfig | None = None,
54
+ generation_mode: GenerationMode = GenerationMode.default(),
55
+ generation_config: GenerationConfig,
113
56
  path_parameters: NotSet | dict[str, Any] = NOT_SET,
114
57
  headers: NotSet | dict[str, Any] = NOT_SET,
115
58
  cookies: NotSet | dict[str, Any] = NOT_SET,
116
59
  query: NotSet | dict[str, Any] = NOT_SET,
117
60
  body: Any = NOT_SET,
118
61
  media_type: str | None = None,
119
- skip_on_not_negated: bool = True,
62
+ phase: TestPhase = TestPhase.GENERATE,
120
63
  ) -> Any:
121
64
  """A strategy that creates `Case` instances.
122
65
 
@@ -130,70 +73,110 @@ def get_case_strategy(
130
73
  The primary purpose of this behavior is to prevent sending incomplete explicit examples by generating missing parts
131
74
  as it works with `body`.
132
75
  """
133
- strategy_factory = DATA_GENERATION_METHOD_TO_STRATEGY_FACTORY[generator]
76
+ start = time.monotonic()
77
+ strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generation_mode]
134
78
 
135
79
  context = HookContext(operation)
136
80
 
137
- generation_config = generation_config or operation.schema.generation_config
138
-
139
81
  path_parameters_ = generate_parameter(
140
- "path", path_parameters, operation, draw, context, hooks, generator, generation_config
82
+ "path", path_parameters, operation, draw, context, hooks, generation_mode, generation_config
83
+ )
84
+ headers_ = generate_parameter(
85
+ "header", headers, operation, draw, context, hooks, generation_mode, generation_config
86
+ )
87
+ cookies_ = generate_parameter(
88
+ "cookie", cookies, operation, draw, context, hooks, generation_mode, generation_config
141
89
  )
142
- headers_ = generate_parameter("header", headers, operation, draw, context, hooks, generator, generation_config)
143
- cookies_ = generate_parameter("cookie", cookies, operation, draw, context, hooks, generator, generation_config)
144
- query_ = generate_parameter("query", query, operation, draw, context, hooks, generator, generation_config)
90
+ query_ = generate_parameter("query", query, operation, draw, context, hooks, generation_mode, generation_config)
145
91
 
146
92
  if body is NOT_SET:
147
93
  if operation.body:
148
- body_generator = generator
149
- if generator.is_negative:
94
+ body_generator = generation_mode
95
+ if generation_mode.is_negative:
150
96
  # Consider only schemas that are possible to negate
151
97
  candidates = [item for item in operation.body.items if can_negate(item.as_json_schema(operation))]
152
98
  # Not possible to negate body, fallback to positive data generation
153
99
  if not candidates:
154
100
  candidates = operation.body.items
155
101
  strategy_factory = make_positive_strategy
156
- body_generator = DataGenerationMethod.positive
102
+ body_generator = GenerationMode.POSITIVE
157
103
  else:
158
104
  candidates = operation.body.items
159
105
  parameter = draw(st.sampled_from(candidates))
160
106
  strategy = _get_body_strategy(parameter, strategy_factory, operation, generation_config)
161
107
  strategy = apply_hooks(operation, context, hooks, strategy, "body")
162
108
  # Parameter may have a wildcard media type. In this case, choose any supported one
163
- possible_media_types = sorted(serializers.get_matching_media_types(parameter.media_type))
109
+ possible_media_types = sorted(
110
+ operation.schema.transport.get_matching_media_types(parameter.media_type), key=lambda x: x[0]
111
+ )
164
112
  if not possible_media_types:
165
113
  all_media_types = operation.get_request_payload_content_types()
166
- if all(serializers.get_first_matching_media_type(media_type) is None for media_type in all_media_types):
114
+ if all(
115
+ operation.schema.transport.get_first_matching_media_type(media_type) is None
116
+ for media_type in all_media_types
117
+ ):
167
118
  # None of media types defined for this operation are not supported
168
119
  raise SerializationNotPossible.from_media_types(*all_media_types)
169
120
  # Other media types are possible - avoid choosing this media type in the future
170
- cant_serialize(parameter.media_type)
171
- media_type = draw(st.sampled_from(possible_media_types))
172
- if media_type is not None and parse_content_type(media_type) == ("application", "x-www-form-urlencoded"):
121
+ event_text = f"Can't serialize data to `{parameter.media_type}`."
122
+ note(f"{event_text} {SERIALIZERS_SUGGESTION_MESSAGE}")
123
+ event(event_text)
124
+ reject() # type: ignore
125
+ media_type, _ = draw(st.sampled_from(possible_media_types))
126
+ if media_type is not None and media_types.parse(media_type) == (
127
+ "application",
128
+ "x-www-form-urlencoded",
129
+ ):
173
130
  strategy = strategy.map(prepare_urlencoded).filter(is_valid_urlencoded)
174
131
  body_ = ValueContainer(value=draw(strategy), location="body", generator=body_generator)
175
132
  else:
176
133
  body_ = ValueContainer(value=body, location="body", generator=None)
177
134
  else:
135
+ # This explicit body payload comes for a media type that has a custom strategy registered
136
+ # Such strategies only support binary payloads, otherwise they can't be serialized
137
+ if not isinstance(body, bytes) and media_type in MEDIA_TYPES:
138
+ all_media_types = operation.get_request_payload_content_types()
139
+ raise SerializationNotPossible.from_media_types(*all_media_types)
178
140
  body_ = ValueContainer(value=body, location="body", generator=None)
179
141
 
180
- if operation.schema.validate_schema and operation.method.upper() == "GET" and operation.body:
181
- raise BodyInGetRequestError("GET requests should not contain body parameters.")
182
142
  # If we need to generate negative cases but no generated values were negated, then skip the whole test
183
- if generator.is_negative and not any_negated_values([query_, cookies_, headers_, path_parameters_, body_]):
184
- if skip_on_not_negated:
185
- skip(operation.verbose_name)
143
+ if generation_mode.is_negative and not any_negated_values([query_, cookies_, headers_, path_parameters_, body_]):
144
+ if generation_config.modes == [GenerationMode.NEGATIVE]:
145
+ raise SkipTest("Impossible to generate negative test cases")
186
146
  else:
187
147
  reject()
188
- instance = Case(
189
- operation=operation,
148
+
149
+ _phase_data = {
150
+ TestPhase.EXPLICIT: ExplicitPhaseData(),
151
+ TestPhase.GENERATE: GeneratePhaseData(),
152
+ }[phase]
153
+ phase_data = cast(Union[ExplicitPhaseData, GeneratePhaseData], _phase_data)
154
+
155
+ instance = operation.Case(
190
156
  media_type=media_type,
191
157
  path_parameters=path_parameters_.value,
192
- headers=CaseInsensitiveDict(headers_.value) if headers_.value is not None else headers_.value,
158
+ headers=headers_.value,
193
159
  cookies=cookies_.value,
194
160
  query=query_.value,
195
161
  body=body_.value,
196
- data_generation_method=generator,
162
+ meta=CaseMetadata(
163
+ generation=GenerationInfo(
164
+ time=time.monotonic() - start,
165
+ mode=generation_mode,
166
+ ),
167
+ phase=PhaseInfo(name=phase, data=phase_data),
168
+ components={
169
+ kind: ComponentInfo(mode=value.generator)
170
+ for kind, value in [
171
+ (ComponentKind.QUERY, query_),
172
+ (ComponentKind.PATH_PARAMETERS, path_parameters_),
173
+ (ComponentKind.HEADERS, headers_),
174
+ (ComponentKind.COOKIES, cookies_),
175
+ (ComponentKind.BODY, body_),
176
+ ]
177
+ if value.generator is not None
178
+ },
179
+ ),
197
180
  )
198
181
  auth_context = auths.AuthContext(
199
182
  operation=operation,
@@ -212,13 +195,15 @@ def _get_body_strategy(
212
195
  operation: APIOperation,
213
196
  generation_config: GenerationConfig,
214
197
  ) -> st.SearchStrategy:
198
+ if parameter.media_type in MEDIA_TYPES:
199
+ return MEDIA_TYPES[parameter.media_type]
215
200
  # The cache key relies on object ids, which means that the parameter should not be mutated
216
201
  # Note, the parent schema is not included as each parameter belong only to one schema
217
202
  if parameter in _BODY_STRATEGIES_CACHE and strategy_factory in _BODY_STRATEGIES_CACHE[parameter]:
218
203
  return _BODY_STRATEGIES_CACHE[parameter][strategy_factory]
219
204
  schema = parameter.as_json_schema(operation)
220
205
  schema = operation.schema.prepare_schema(schema)
221
- strategy = strategy_factory(schema, operation.verbose_name, "body", parameter.media_type, generation_config)
206
+ strategy = strategy_factory(schema, operation.label, "body", parameter.media_type, generation_config)
222
207
  if not parameter.is_required:
223
208
  strategy |= st.just(NOT_SET)
224
209
  _BODY_STRATEGIES_CACHE.setdefault(parameter, {})[strategy_factory] = strategy
@@ -248,7 +233,7 @@ def get_parameters_value(
248
233
  strategy = apply_hooks(operation, context, hooks, strategy, location)
249
234
  new = draw(strategy)
250
235
  if new is not None:
251
- copied = fast_deepcopy(value)
236
+ copied = deepclone(value)
252
237
  copied.update(new)
253
238
  return copied
254
239
  return value
@@ -263,7 +248,9 @@ class ValueContainer:
263
248
 
264
249
  value: Any
265
250
  location: str
266
- generator: DataGenerationMethod | None
251
+ generator: GenerationMode | None
252
+
253
+ __slots__ = ("value", "location", "generator")
267
254
 
268
255
  @property
269
256
  def is_generated(self) -> bool:
@@ -273,7 +260,7 @@ class ValueContainer:
273
260
 
274
261
  def any_negated_values(values: list[ValueContainer]) -> bool:
275
262
  """Check if any generated values are negated."""
276
- return any(value.generator == DataGenerationMethod.negative for value in values if value.is_generated)
263
+ return any(value.generator == GenerationMode.NEGATIVE for value in values if value.is_generated)
277
264
 
278
265
 
279
266
  def generate_parameter(
@@ -283,7 +270,7 @@ def generate_parameter(
283
270
  draw: Callable,
284
271
  context: HookContext,
285
272
  hooks: HookDispatcher | None,
286
- generator: DataGenerationMethod,
273
+ generator: GenerationMode,
287
274
  generation_config: GenerationConfig,
288
275
  ) -> ValueContainer:
289
276
  """Generate a value for a parameter.
@@ -297,13 +284,13 @@ def generate_parameter(
297
284
  # If we can't negate any parameter, generate positive ones
298
285
  # If nothing else will be negated, then skip the test completely
299
286
  strategy_factory = make_positive_strategy
300
- generator = DataGenerationMethod.positive
287
+ generator = GenerationMode.POSITIVE
301
288
  else:
302
- strategy_factory = DATA_GENERATION_METHOD_TO_STRATEGY_FACTORY[generator]
289
+ strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generator]
303
290
  value = get_parameters_value(
304
291
  explicit, location, draw, operation, context, hooks, strategy_factory, generation_config
305
292
  )
306
- used_generator: DataGenerationMethod | None = generator
293
+ used_generator: GenerationMode | None = generator
307
294
  if value == explicit:
308
295
  # When we pass `explicit`, then its parts are excluded from generation of the final value
309
296
  # If the final value is the same, then other parameters were generated at all
@@ -332,6 +319,18 @@ def can_negate_headers(operation: APIOperation, location: str) -> bool:
332
319
  return any(header != {"type": "string"} for header in headers.values())
333
320
 
334
321
 
322
+ def get_schema_for_location(
323
+ operation: APIOperation, location: str, parameters: Iterable[OpenAPIParameter]
324
+ ) -> dict[str, Any]:
325
+ schema = parameters_to_json_schema(operation, parameters)
326
+ if location == "path":
327
+ schema["required"] = list(schema["properties"])
328
+ for prop in schema.get("properties", {}).values():
329
+ if prop.get("type") == "string":
330
+ prop.setdefault("minLength", 1)
331
+ return operation.schema.prepare_schema(schema)
332
+
333
+
335
334
  def get_parameters_strategy(
336
335
  operation: APIOperation,
337
336
  strategy_factory: StrategyFactory,
@@ -346,13 +345,7 @@ def get_parameters_strategy(
346
345
  nested_cache_key = (strategy_factory, location, tuple(sorted(exclude)))
347
346
  if operation in _PARAMETER_STRATEGIES_CACHE and nested_cache_key in _PARAMETER_STRATEGIES_CACHE[operation]:
348
347
  return _PARAMETER_STRATEGIES_CACHE[operation][nested_cache_key]
349
- schema = parameters_to_json_schema(operation, parameters)
350
- if not operation.schema.validate_schema and location == "path":
351
- # If schema validation is disabled, we try to generate data even if the parameter definition
352
- # contains errors.
353
- # In this case, we know that the `required` keyword should always be `True`.
354
- schema["required"] = list(schema["properties"])
355
- schema = operation.schema.prepare_schema(schema)
348
+ schema = get_schema_for_location(operation, location, parameters)
356
349
  for name in exclude:
357
350
  # Values from `exclude` are not necessarily valid for the schema - they come from user-defined examples
358
351
  # that may be invalid
@@ -363,7 +356,7 @@ def get_parameters_strategy(
363
356
  # Nothing to negate - all properties were excluded
364
357
  strategy = st.none()
365
358
  else:
366
- strategy = strategy_factory(schema, operation.verbose_name, location, None, generation_config)
359
+ strategy = strategy_factory(schema, operation.label, location, None, generation_config)
367
360
  serialize = operation.get_parameter_serializer(location)
368
361
  if serialize is not None:
369
362
  strategy = strategy.map(serialize)
@@ -380,12 +373,10 @@ def get_parameters_strategy(
380
373
  # `True` / `False` / `None` improves chances of them passing validation in apps
381
374
  # that expect boolean / null types
382
375
  # and not aware of Python-specific representation of those types
383
- map_func = {
384
- "path": compose(quote_all, jsonify_python_specific_types),
385
- "query": jsonify_python_specific_types,
386
- }.get(location)
387
- if map_func:
388
- strategy = strategy.map(map_func) # type: ignore
376
+ if location == "path":
377
+ strategy = strategy.map(quote_all).map(jsonify_python_specific_types)
378
+ elif location == "query":
379
+ strategy = strategy.map(jsonify_python_specific_types)
389
380
  _PARAMETER_STRATEGIES_CACHE.setdefault(operation, {})[nested_cache_key] = strategy
390
381
  return strategy
391
382
  # No parameters defined for this location
@@ -412,6 +403,17 @@ def jsonify_python_specific_types(value: dict[str, Any]) -> dict[str, Any]:
412
403
  return value
413
404
 
414
405
 
406
+ def _build_custom_formats(
407
+ custom_formats: dict[str, st.SearchStrategy] | None, generation_config: GenerationConfig
408
+ ) -> dict[str, st.SearchStrategy]:
409
+ custom_formats = {**get_default_format_strategies(), **STRING_FORMATS, **(custom_formats or {})}
410
+ if generation_config.headers.strategy is not None:
411
+ custom_formats[HEADER_FORMAT] = generation_config.headers.strategy
412
+ elif not generation_config.allow_x00:
413
+ custom_formats[HEADER_FORMAT] = header_values(blacklist_characters="\n\r\x00")
414
+ return custom_formats
415
+
416
+
415
417
  def make_positive_strategy(
416
418
  schema: dict[str, Any],
417
419
  operation_name: str,
@@ -428,9 +430,10 @@ def make_positive_strategy(
428
430
  for sub_schema in schema.get("properties", {}).values():
429
431
  if list(sub_schema) == ["type"] and sub_schema["type"] == "string":
430
432
  sub_schema.setdefault("format", HEADER_FORMAT)
433
+ custom_formats = _build_custom_formats(custom_formats, generation_config)
431
434
  return from_schema(
432
435
  schema,
433
- custom_formats={**get_default_format_strategies(), **STRING_FORMATS, **(custom_formats or {})},
436
+ custom_formats=custom_formats,
434
437
  allow_x00=generation_config.allow_x00,
435
438
  codec=generation_config.codec,
436
439
  )
@@ -449,40 +452,23 @@ def make_negative_strategy(
449
452
  generation_config: GenerationConfig,
450
453
  custom_formats: dict[str, st.SearchStrategy] | None = None,
451
454
  ) -> st.SearchStrategy:
455
+ custom_formats = _build_custom_formats(custom_formats, generation_config)
452
456
  return negative_schema(
453
457
  schema,
454
458
  operation_name=operation_name,
455
459
  location=location,
456
460
  media_type=media_type,
457
- custom_formats={**get_default_format_strategies(), **STRING_FORMATS, **(custom_formats or {})},
461
+ custom_formats=custom_formats,
458
462
  generation_config=generation_config,
459
463
  )
460
464
 
461
465
 
462
- DATA_GENERATION_METHOD_TO_STRATEGY_FACTORY = {
463
- DataGenerationMethod.positive: make_positive_strategy,
464
- DataGenerationMethod.negative: make_negative_strategy,
466
+ GENERATOR_MODE_TO_STRATEGY_FACTORY = {
467
+ GenerationMode.POSITIVE: make_positive_strategy,
468
+ GenerationMode.NEGATIVE: make_negative_strategy,
465
469
  }
466
470
 
467
471
 
468
- def is_valid_path(parameters: dict[str, Any]) -> bool:
469
- """Empty strings ("") are excluded from path by urllib3.
470
-
471
- A path containing to "/" or "%2F" will lead to ambiguous path resolution in
472
- many frameworks and libraries, such behaviour have been observed in both
473
- WSGI and ASGI applications.
474
-
475
- In this case one variable in the path template will be empty, which will lead to 404 in most of the cases.
476
- Because of it this case doesn't bring much value and might lead to false positives results of Schemathesis runs.
477
- """
478
- disallowed_values = (SLASH, "")
479
-
480
- return not any(
481
- (value in disallowed_values or is_illegal_surrogate(value) or isinstance(value, str) and SLASH in value)
482
- for value in parameters.values()
483
- )
484
-
485
-
486
472
  def quote_all(parameters: dict[str, Any]) -> dict[str, Any]:
487
473
  """Apply URL quotation for all values in a dictionary."""
488
474
  # Even though, "." is an unreserved character, it has a special meaning in "." and ".." strings.
@@ -512,8 +498,3 @@ def apply_hooks(
512
498
  """Apply all hooks related to the given location."""
513
499
  container = LOCATION_TO_CONTAINER[location]
514
500
  return apply_to_all_dispatchers(operation, context, hooks, strategy, container)
515
-
516
-
517
- def clear_cache() -> None:
518
- _PARAMETER_STRATEGIES_CACHE.clear()
519
- _BODY_STRATEGIES_CACHE.clear()