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,8 +3,8 @@ from __future__ import annotations
3
3
  from itertools import chain
4
4
  from typing import Any, Callable
5
5
 
6
- from ...internal.copy import fast_deepcopy
7
- from ...internal.jsonschema import traverse_schema
6
+ from schemathesis.core.transforms import deepclone, transform
7
+
8
8
  from .patterns import update_quantifier
9
9
 
10
10
 
@@ -22,7 +22,7 @@ def to_json_schema(
22
22
  See a recursive version below.
23
23
  """
24
24
  if copy:
25
- schema = fast_deepcopy(schema)
25
+ schema = deepclone(schema)
26
26
  if schema.get(nullable_name) is True:
27
27
  del schema[nullable_name]
28
28
  schema = {"anyOf": [schema, {"type": "null"}]}
@@ -94,7 +94,7 @@ def is_read_only(schema: dict[str, Any] | bool) -> bool:
94
94
  def to_json_schema_recursive(
95
95
  schema: dict[str, Any], nullable_name: str, is_response_schema: bool = False, update_quantifiers: bool = True
96
96
  ) -> dict[str, Any]:
97
- return traverse_schema(
97
+ return transform(
98
98
  schema,
99
99
  to_json_schema,
100
100
  nullable_name=nullable_name,
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  from typing import TYPE_CHECKING, Any
5
5
 
6
- from ..._lazy_import import lazy_import
6
+ from schemathesis.core.lazy_import import lazy_import
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from jsonschema import Validator
@@ -9,13 +9,16 @@ from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast
9
9
  import requests
10
10
  from hypothesis_jsonschema import from_schema
11
11
 
12
+ from schemathesis.config import GenerationConfig
13
+ from schemathesis.core.transforms import deepclone
14
+ from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
15
+ from schemathesis.generation.case import Case
16
+ from schemathesis.generation.hypothesis import examples
17
+ from schemathesis.generation.meta import TestPhase
18
+ from schemathesis.schemas import APIOperation
12
19
  from schemathesis.specs.openapi.serialization import get_serializers_for_operation
13
20
 
14
- from ...constants import DEFAULT_RESPONSE_TIMEOUT
15
- from ...generation import get_single_example
16
- from ...internal.copy import fast_deepcopy
17
- from ...models import APIOperation, Case, TestPhase
18
- from ._hypothesis import get_case_strategy, get_default_format_strategies
21
+ from ._hypothesis import get_default_format_strategies, openapi_cases
19
22
  from .constants import LOCATION_TO_CONTAINER
20
23
  from .formats import STRING_FORMATS
21
24
  from .parameters import OpenAPIBody, OpenAPIParameter
@@ -23,8 +26,6 @@ from .parameters import OpenAPIBody, OpenAPIParameter
23
26
  if TYPE_CHECKING:
24
27
  from hypothesis.strategies import SearchStrategy
25
28
 
26
- from ...generation import GenerationConfig
27
-
28
29
 
29
30
  @dataclass
30
31
  class ParameterExample:
@@ -47,7 +48,7 @@ Example = Union[ParameterExample, BodyExample]
47
48
 
48
49
 
49
50
  def get_strategies_from_examples(
50
- operation: APIOperation[OpenAPIParameter, Case], as_strategy_kwargs: dict[str, Any] | None = None
51
+ operation: APIOperation[OpenAPIParameter], **kwargs: Any
51
52
  ) -> list[SearchStrategy[Case]]:
52
53
  """Build a set of strategies that generate test cases based on explicit examples in the schema."""
53
54
  maps = get_serializers_for_operation(operation)
@@ -66,15 +67,15 @@ def get_strategies_from_examples(
66
67
  examples = list(extract_top_level(operation))
67
68
  # Add examples from parameter's schemas
68
69
  examples.extend(extract_from_schemas(operation))
69
- as_strategy_kwargs = as_strategy_kwargs or {}
70
- as_strategy_kwargs["phase"] = TestPhase.EXPLICIT
71
70
  return [
72
- get_case_strategy(operation=operation, **{**parameters, **(as_strategy_kwargs or {})}).map(serialize_components)
71
+ openapi_cases(operation=operation, **{**parameters, **kwargs, "phase": TestPhase.EXAMPLES}).map(
72
+ serialize_components
73
+ )
73
74
  for parameters in produce_combinations(examples)
74
75
  ]
75
76
 
76
77
 
77
- def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Generator[Example, None, None]:
78
+ def extract_top_level(operation: APIOperation[OpenAPIParameter]) -> Generator[Example, None, None]:
78
79
  """Extract top-level parameter examples from `examples` & `example` fields."""
79
80
  responses = find_in_responses(operation)
80
81
  for parameter in operation.iter_parameters():
@@ -142,7 +143,7 @@ def _expand_subschemas(schema: dict[str, Any] | bool) -> Generator[dict[str, Any
142
143
  for subschema in schema[key]:
143
144
  yield subschema
144
145
  if "allOf" in schema:
145
- subschema = fast_deepcopy(schema["allOf"][0])
146
+ subschema = deepclone(schema["allOf"][0])
146
147
  for sub in schema["allOf"][1:]:
147
148
  if isinstance(sub, dict):
148
149
  for key, value in sub.items():
@@ -160,7 +161,7 @@ def _expand_subschemas(schema: dict[str, Any] | bool) -> Generator[dict[str, Any
160
161
 
161
162
 
162
163
  def _find_parameter_examples_definition(
163
- operation: APIOperation[OpenAPIParameter, Case], parameter_name: str, field_name: str
164
+ operation: APIOperation[OpenAPIParameter], parameter_name: str, field_name: str
164
165
  ) -> dict[str, Any]:
165
166
  """Find the original, unresolved `examples` definition of a parameter."""
166
167
  from .schemas import BaseOpenAPISchema
@@ -178,13 +179,13 @@ def _find_parameter_examples_definition(
178
179
 
179
180
 
180
181
  def _find_request_body_examples_definition(
181
- operation: APIOperation[OpenAPIParameter, Case], alternative: OpenAPIBody
182
+ operation: APIOperation[OpenAPIParameter], alternative: OpenAPIBody
182
183
  ) -> dict[str, Any]:
183
184
  """Find the original, unresolved `examples` definition of a request body variant."""
184
185
  from .schemas import BaseOpenAPISchema
185
186
 
186
187
  schema = cast(BaseOpenAPISchema, operation.schema)
187
- if schema.spec_version == "2.0":
188
+ if schema.specification.version == "2.0":
188
189
  raw_schema = schema.raw_schema
189
190
  path_data = raw_schema["paths"][operation.path]
190
191
  parameters = chain(path_data[operation.method].get("parameters", []), path_data.get("parameters", []))
@@ -220,12 +221,12 @@ def extract_inner_examples(
220
221
  @lru_cache
221
222
  def load_external_example(url: str) -> bytes:
222
223
  """Load examples the `externalValue` keyword."""
223
- response = requests.get(url, timeout=DEFAULT_RESPONSE_TIMEOUT / 1000)
224
+ response = requests.get(url, timeout=DEFAULT_RESPONSE_TIMEOUT)
224
225
  response.raise_for_status()
225
226
  return response.content
226
227
 
227
228
 
228
- def extract_from_schemas(operation: APIOperation[OpenAPIParameter, Case]) -> Generator[Example, None, None]:
229
+ def extract_from_schemas(operation: APIOperation[OpenAPIParameter]) -> Generator[Example, None, None]:
229
230
  """Extract examples from parameters' schema definitions."""
230
231
  for parameter in operation.iter_parameters():
231
232
  schema = parameter.as_json_schema(operation)
@@ -242,7 +243,7 @@ def extract_from_schemas(operation: APIOperation[OpenAPIParameter, Case]) -> Gen
242
243
 
243
244
 
244
245
  def extract_from_schema(
245
- operation: APIOperation[OpenAPIParameter, Case],
246
+ operation: APIOperation[OpenAPIParameter],
246
247
  schema: dict[str, Any],
247
248
  example_field_name: str,
248
249
  examples_field_name: str,
@@ -273,12 +274,13 @@ def extract_from_schema(
273
274
  continue
274
275
  variants[name] = values
275
276
  if variants:
277
+ config = operation.schema.config.generation_for(operation=operation, phase="examples")
276
278
  for name, subschema in to_generate.items():
277
279
  if name in variants:
278
280
  # Generated by one of `anyOf` or similar sub-schemas
279
281
  continue
280
282
  subschema = operation.schema.prepare_schema(subschema)
281
- generated = _generate_single_example(subschema, operation.schema.generation_config)
283
+ generated = _generate_single_example(subschema, config)
282
284
  variants[name] = [generated]
283
285
  # Calculate the maximum number of examples any property has
284
286
  total_combos = max(len(examples) for examples in variants.values())
@@ -304,7 +306,7 @@ def _generate_single_example(
304
306
  allow_x00=generation_config.allow_x00,
305
307
  codec=generation_config.codec,
306
308
  )
307
- return get_single_example(strategy)
309
+ return examples.generate_one(strategy)
308
310
 
309
311
 
310
312
  def produce_combinations(examples: list[Example]) -> Generator[dict[str, Any], None, None]:
@@ -8,42 +8,54 @@ from __future__ import annotations
8
8
  import json
9
9
  from typing import Any
10
10
 
11
+ from schemathesis.core.transforms import UNRESOLVABLE, Unresolvable
12
+ from schemathesis.generation.stateful.state_machine import StepOutput
13
+
11
14
  from . import lexer, nodes, parser
12
- from .context import ExpressionContext
13
15
 
14
- __all__ = [
15
- "lexer",
16
- "nodes",
17
- "parser",
18
- "ExpressionContext",
19
- ]
16
+ __all__ = ["lexer", "nodes", "parser"]
20
17
 
21
18
 
22
- def evaluate(expr: Any, context: ExpressionContext, evaluate_nested: bool = False) -> Any:
19
+ def evaluate(expr: Any, output: StepOutput, evaluate_nested: bool = False) -> Any:
23
20
  """Evaluate runtime expression in context."""
24
21
  if isinstance(expr, (dict, list)) and evaluate_nested:
25
- return _evaluate_nested(expr, context)
22
+ return _evaluate_nested(expr, output)
26
23
  if not isinstance(expr, str):
27
24
  # Can be a non-string constant
28
25
  return expr
29
- parts = [node.evaluate(context) for node in parser.parse(expr)]
26
+ parts = [node.evaluate(output) for node in parser.parse(expr)]
30
27
  if len(parts) == 1:
31
28
  return parts[0] # keep the return type the same as the internal value type
32
- # otherwise, concatenate into a string
29
+ if any(isinstance(part, Unresolvable) for part in parts):
30
+ return UNRESOLVABLE
33
31
  return "".join(str(part) for part in parts if part is not None)
34
32
 
35
33
 
36
- def _evaluate_nested(expr: dict[str, Any] | list, context: ExpressionContext) -> Any:
34
+ def _evaluate_nested(expr: dict[str, Any] | list, output: StepOutput) -> Any:
37
35
  if isinstance(expr, dict):
38
- return {
39
- _evaluate_object_key(key, context): evaluate(value, context, evaluate_nested=True)
40
- for key, value in expr.items()
41
- }
42
- return [evaluate(item, context, evaluate_nested=True) for item in expr]
36
+ result_dict = {}
37
+ for key, value in expr.items():
38
+ new_key = _evaluate_object_key(key, output)
39
+ if new_key is UNRESOLVABLE:
40
+ return new_key
41
+ new_value = evaluate(value, output, evaluate_nested=True)
42
+ if new_value is UNRESOLVABLE:
43
+ return new_value
44
+ result_dict[new_key] = new_value
45
+ return result_dict
46
+ result_list = []
47
+ for item in expr:
48
+ new_value = evaluate(item, output, evaluate_nested=True)
49
+ if new_value is UNRESOLVABLE:
50
+ return new_value
51
+ result_list.append(new_value)
52
+ return result_list
43
53
 
44
54
 
45
- def _evaluate_object_key(key: str, context: ExpressionContext) -> Any:
46
- evaluated = evaluate(key, context)
55
+ def _evaluate_object_key(key: str, output: StepOutput) -> Any:
56
+ evaluated = evaluate(key, output)
57
+ if evaluated is UNRESOLVABLE:
58
+ return evaluated
47
59
  if isinstance(evaluated, str):
48
60
  return evaluated
49
61
  if isinstance(evaluated, bool):
@@ -1,10 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import re
3
4
  from dataclasses import dataclass
4
- from typing import TYPE_CHECKING
5
-
6
- if TYPE_CHECKING:
7
- import re
8
5
 
9
6
 
10
7
  @dataclass
@@ -6,7 +6,7 @@ from typing import Callable, Generator
6
6
 
7
7
 
8
8
  @unique
9
- class TokenType(Enum):
9
+ class TokenType(int, Enum):
10
10
  VARIABLE = 1
11
11
  STRING = 2
12
12
  POINTER = 3
@@ -4,14 +4,15 @@ from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass
6
6
  from enum import Enum, unique
7
- from typing import TYPE_CHECKING, Any
7
+ from typing import TYPE_CHECKING, Any, cast
8
8
 
9
9
  from requests.structures import CaseInsensitiveDict
10
10
 
11
- from .. import references
11
+ from schemathesis.core.transforms import UNRESOLVABLE, Unresolvable, resolve_pointer
12
+ from schemathesis.generation.stateful.state_machine import StepOutput
13
+ from schemathesis.transport.requests import REQUESTS_TRANSPORT
12
14
 
13
15
  if TYPE_CHECKING:
14
- from .context import ExpressionContext
15
16
  from .extractors import Extractor
16
17
 
17
18
 
@@ -19,12 +20,12 @@ if TYPE_CHECKING:
19
20
  class Node:
20
21
  """Generic expression node."""
21
22
 
22
- def evaluate(self, context: ExpressionContext) -> str:
23
+ def evaluate(self, output: StepOutput) -> str | Unresolvable:
23
24
  raise NotImplementedError
24
25
 
25
26
 
26
27
  @unique
27
- class NodeType(Enum):
28
+ class NodeType(str, Enum):
28
29
  URL = "$url"
29
30
  METHOD = "$method"
30
31
  STATUS_CODE = "$statusCode"
@@ -38,7 +39,7 @@ class String(Node):
38
39
 
39
40
  value: str
40
41
 
41
- def evaluate(self, context: ExpressionContext) -> str:
42
+ def evaluate(self, output: StepOutput) -> str | Unresolvable:
42
43
  """String tokens are passed as they are.
43
44
 
44
45
  ``foo{$request.path.id}``
@@ -52,24 +53,29 @@ class String(Node):
52
53
  class URL(Node):
53
54
  """A node for `$url` expression."""
54
55
 
55
- def evaluate(self, context: ExpressionContext) -> str:
56
- return context.case.get_full_url()
56
+ def evaluate(self, output: StepOutput) -> str | Unresolvable:
57
+ import requests
58
+
59
+ base_url = output.case.operation.base_url or "http://127.0.0.1"
60
+ kwargs = REQUESTS_TRANSPORT.serialize_case(output.case, base_url=base_url)
61
+ prepared = requests.Request(**kwargs).prepare()
62
+ return cast(str, prepared.url)
57
63
 
58
64
 
59
65
  @dataclass
60
66
  class Method(Node):
61
67
  """A node for `$method` expression."""
62
68
 
63
- def evaluate(self, context: ExpressionContext) -> str:
64
- return context.case.operation.method.upper()
69
+ def evaluate(self, output: StepOutput) -> str | Unresolvable:
70
+ return output.case.operation.method.upper()
65
71
 
66
72
 
67
73
  @dataclass
68
74
  class StatusCode(Node):
69
75
  """A node for `$statusCode` expression."""
70
76
 
71
- def evaluate(self, context: ExpressionContext) -> str:
72
- return str(context.response.status_code)
77
+ def evaluate(self, output: StepOutput) -> str | Unresolvable:
78
+ return str(output.response.status_code)
73
79
 
74
80
 
75
81
  @dataclass
@@ -80,19 +86,19 @@ class NonBodyRequest(Node):
80
86
  parameter: str
81
87
  extractor: Extractor | None = None
82
88
 
83
- def evaluate(self, context: ExpressionContext) -> str:
84
- container: dict | CaseInsensitiveDict = {
85
- "query": context.case.query,
86
- "path": context.case.path_parameters,
87
- "header": context.case.headers,
89
+ def evaluate(self, output: StepOutput) -> str | Unresolvable:
90
+ container = {
91
+ "query": output.case.query,
92
+ "path": output.case.path_parameters,
93
+ "header": output.case.headers,
88
94
  }[self.location] or {}
89
95
  if self.location == "header":
90
96
  container = CaseInsensitiveDict(container)
91
97
  value = container.get(self.parameter)
92
98
  if value is None:
93
- return ""
99
+ return UNRESOLVABLE
94
100
  if self.extractor is not None:
95
- return self.extractor.extract(value) or ""
101
+ return self.extractor.extract(value) or UNRESOLVABLE
96
102
  return value
97
103
 
98
104
 
@@ -102,14 +108,11 @@ class BodyRequest(Node):
102
108
 
103
109
  pointer: str | None = None
104
110
 
105
- def evaluate(self, context: ExpressionContext) -> Any:
106
- document = context.case.body
111
+ def evaluate(self, output: StepOutput) -> Any | Unresolvable:
112
+ document = output.case.body
107
113
  if self.pointer is None:
108
114
  return document
109
- resolved = references.resolve_pointer(document, self.pointer[1:])
110
- if resolved is references.UNRESOLVABLE:
111
- return None
112
- return resolved
115
+ return resolve_pointer(document, self.pointer[1:])
113
116
 
114
117
 
115
118
  @dataclass
@@ -119,13 +122,13 @@ class HeaderResponse(Node):
119
122
  parameter: str
120
123
  extractor: Extractor | None = None
121
124
 
122
- def evaluate(self, context: ExpressionContext) -> str:
123
- value = context.response.headers.get(self.parameter)
125
+ def evaluate(self, output: StepOutput) -> str | Unresolvable:
126
+ value = output.response.headers.get(self.parameter.lower())
124
127
  if value is None:
125
- return ""
128
+ return UNRESOLVABLE
126
129
  if self.extractor is not None:
127
- return self.extractor.extract(value) or ""
128
- return value
130
+ return self.extractor.extract(value[0]) or UNRESOLVABLE
131
+ return value[0]
129
132
 
130
133
 
131
134
  @dataclass
@@ -134,17 +137,9 @@ class BodyResponse(Node):
134
137
 
135
138
  pointer: str | None = None
136
139
 
137
- def evaluate(self, context: ExpressionContext) -> Any:
138
- from ....transports.responses import WSGIResponse
139
-
140
- if isinstance(context.response, WSGIResponse):
141
- document = context.response.json
142
- else:
143
- document = context.response.json()
140
+ def evaluate(self, output: StepOutput) -> Any:
141
+ document = output.response.json()
144
142
  if self.pointer is None:
145
143
  # We need the parsed document - data will be serialized before sending to the application
146
144
  return document
147
- resolved = references.resolve_pointer(document, self.pointer[1:])
148
- if resolved is references.UNRESOLVABLE:
149
- return None
150
- return resolved
145
+ return resolve_pointer(document, self.pointer[1:])
@@ -46,7 +46,7 @@ def _parse_variable(tokens: lexer.TokenGenerator, token: lexer.Token, expr: str)
46
46
  elif token.value == nodes.NodeType.RESPONSE.value:
47
47
  yield _parse_response(tokens, expr)
48
48
  else:
49
- raise UnknownToken(token.value)
49
+ raise UnknownToken(f"Invalid expression `{expr}`. Unknown token: `{token.value}`")
50
50
 
51
51
 
52
52
  def _parse_request(tokens: lexer.TokenGenerator, expr: str) -> nodes.BodyRequest | nodes.NonBodyRequest:
@@ -5,6 +5,8 @@ from base64 import b64encode
5
5
  from functools import lru_cache
6
6
  from typing import TYPE_CHECKING
7
7
 
8
+ from schemathesis.transport.serialization import Binary
9
+
8
10
  if TYPE_CHECKING:
9
11
  from hypothesis import strategies as st
10
12
 
@@ -13,10 +15,37 @@ STRING_FORMATS: dict[str, st.SearchStrategy] = {}
13
15
 
14
16
 
15
17
  def register_string_format(name: str, strategy: st.SearchStrategy) -> None:
16
- """Register a new strategy for generating data for specific string "format".
18
+ r"""Register a custom Hypothesis strategy for generating string format data.
19
+
20
+ Args:
21
+ name: String format name that matches the "format" keyword in your API schema
22
+ strategy: Hypothesis strategy to generate values for this format
23
+
24
+ Example:
25
+ ```python
26
+ import schemathesis
27
+ from hypothesis import strategies as st
28
+
29
+ # Register phone number format
30
+ phone_strategy = st.from_regex(r"\+1-\d{3}-\d{3}-\d{4}")
31
+ schemathesis.openapi.format("phone", phone_strategy)
32
+
33
+ # Register email with specific domain
34
+ email_strategy = st.from_regex(r"[a-z]+@company\.com")
35
+ schemathesis.openapi.format("company-email", email_strategy)
36
+ ```
37
+
38
+ Schema usage:
39
+ ```yaml
40
+ properties:
41
+ phone:
42
+ type: string
43
+ format: phone # Uses your phone_strategy
44
+ contact_email:
45
+ type: string
46
+ format: company-email # Uses your email_strategy
47
+ ```
17
48
 
18
- :param str name: Format name. It should correspond the one used in the API schema as the "format" keyword value.
19
- :param strategy: Hypothesis strategy you'd like to use to generate values for this format.
20
49
  """
21
50
  from hypothesis.strategies import SearchStrategy
22
51
 
@@ -36,11 +65,11 @@ def unregister_string_format(name: str) -> None:
36
65
  raise ValueError(f"Unknown Open API format: {name}") from exc
37
66
 
38
67
 
39
- def header_values(blacklist_characters: str = "\n\r") -> st.SearchStrategy[str]:
68
+ def header_values(exclude_characters: str = "\n\r") -> st.SearchStrategy[str]:
40
69
  from hypothesis import strategies as st
41
70
 
42
71
  return st.text(
43
- alphabet=st.characters(min_codepoint=0, max_codepoint=255, blacklist_characters=blacklist_characters)
72
+ alphabet=st.characters(min_codepoint=0, max_codepoint=255, exclude_characters=exclude_characters)
44
73
  # Header values with leading non-visible chars can't be sent with `requests`
45
74
  ).map(str.lstrip)
46
75
 
@@ -54,8 +83,6 @@ def get_default_format_strategies() -> dict[str, st.SearchStrategy]:
54
83
  from hypothesis import strategies as st
55
84
  from requests.auth import _basic_auth_str
56
85
 
57
- from ...serializers import Binary
58
-
59
86
  def make_basic_auth_str(item: tuple[str, str]) -> str:
60
87
  return _basic_auth_str(*item)
61
88
 
@@ -67,6 +94,7 @@ def get_default_format_strategies() -> dict[str, st.SearchStrategy]:
67
94
  return {
68
95
  "binary": st.binary().map(Binary),
69
96
  "byte": st.binary().map(lambda x: b64encode(x).decode()),
97
+ "uuid": st.uuids().map(str),
70
98
  # RFC 7230, Section 3.2.6
71
99
  "_header_name": st.text(
72
100
  min_size=1, alphabet=st.sampled_from("!#$%&'*+-.^_`|~" + string.digits + string.ascii_letters)
@@ -2,6 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any, Collection
4
4
 
5
+ from schemathesis.transport import SerializationContext
6
+ from schemathesis.transport.asgi import ASGI_TRANSPORT
7
+ from schemathesis.transport.requests import REQUESTS_TRANSPORT
8
+ from schemathesis.transport.wsgi import WSGI_TRANSPORT
9
+
5
10
  if TYPE_CHECKING:
6
11
  from hypothesis import strategies as st
7
12
 
@@ -10,16 +15,56 @@ MEDIA_TYPES: dict[str, st.SearchStrategy[bytes]] = {}
10
15
 
11
16
 
12
17
  def register_media_type(name: str, strategy: st.SearchStrategy[bytes], *, aliases: Collection[str] = ()) -> None:
13
- """Register a strategy for the given media type."""
14
- from ...serializers import SerializerContext, register
18
+ r"""Register a custom Hypothesis strategy for generating media type content.
19
+
20
+ Args:
21
+ name: Media type name that matches your OpenAPI requestBody content type
22
+ strategy: Hypothesis strategy that generates bytes for this media type
23
+ aliases: Additional media type names that use the same strategy
24
+
25
+ Example:
26
+ ```python
27
+ import schemathesis
28
+ from hypothesis import strategies as st
29
+
30
+ # Register PDF file strategy
31
+ pdf_strategy = st.sampled_from([
32
+ b"%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n>>\nendobj\n%%EOF",
33
+ b"%PDF-1.5\n%\xe2\xe3\xcf\xd3\n1 0 obj\n<<\n/Type /Catalog\n>>\nendobj\n%%EOF"
34
+ ])
35
+ schemathesis.openapi.media_type("application/pdf", pdf_strategy)
15
36
 
16
- @register(name, aliases=aliases)
17
- class MediaTypeSerializer:
18
- def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
19
- return {"data": value}
37
+ # Dynamic content generation
38
+ @st.composite
39
+ def xml_content(draw):
40
+ tag = draw(st.text(min_size=3, max_size=10))
41
+ content = draw(st.text(min_size=1, max_size=50))
42
+ return f"<?xml version='1.0'?><{tag}>{content}</{tag}>".encode()
20
43
 
21
- def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
22
- return {"data": value}
44
+ schemathesis.openapi.media_type("application/xml", xml_content())
45
+ ```
46
+
47
+ Schema usage:
48
+ ```yaml
49
+ requestBody:
50
+ content:
51
+ application/pdf: # Uses your PDF strategy
52
+ schema:
53
+ type: string
54
+ format: binary
55
+ application/xml: # Uses your XML strategy
56
+ schema:
57
+ type: string
58
+ format: binary
59
+ ```
60
+
61
+ """
62
+
63
+ @REQUESTS_TRANSPORT.serializer(name, *aliases)
64
+ @ASGI_TRANSPORT.serializer(name, *aliases)
65
+ @WSGI_TRANSPORT.serializer(name, *aliases)
66
+ def serialize(ctx: SerializationContext, value: Any) -> dict[str, Any]:
67
+ return {"data": value}
23
68
 
24
69
  MEDIA_TYPES[name] = strategy
25
70
  for alias in aliases:
@@ -27,8 +72,4 @@ def register_media_type(name: str, strategy: st.SearchStrategy[bytes], *, aliase
27
72
 
28
73
 
29
74
  def unregister_all() -> None:
30
- from ...serializers import unregister
31
-
32
- for media_type in MEDIA_TYPES:
33
- unregister(media_type)
34
75
  MEDIA_TYPES.clear()
@@ -9,11 +9,12 @@ import jsonschema
9
9
  from hypothesis import strategies as st
10
10
  from hypothesis_jsonschema import from_schema
11
11
 
12
+ from schemathesis.config import GenerationConfig
13
+
12
14
  from ..constants import ALL_KEYWORDS
13
15
  from .mutations import MutationContext
14
16
 
15
17
  if TYPE_CHECKING:
16
- from ....generation import GenerationConfig
17
18
  from .types import Draw, Schema
18
19
 
19
20
 
@@ -27,16 +28,17 @@ class CacheKey:
27
28
  operation_name: str
28
29
  location: str
29
30
  schema: Schema
31
+ validator_cls: type[jsonschema.Validator]
30
32
 
31
33
  def __hash__(self) -> int:
32
34
  return hash((self.operation_name, self.location))
33
35
 
34
36
 
35
37
  @lru_cache
36
- def get_validator(cache_key: CacheKey) -> jsonschema.Draft4Validator:
38
+ def get_validator(cache_key: CacheKey) -> jsonschema.Validator:
37
39
  """Get JSON Schema validator for the given schema."""
38
40
  # Each operation / location combo has only a single schema, therefore could be cached
39
- return jsonschema.Draft4Validator(cache_key.schema)
41
+ return cache_key.validator_cls(cache_key.schema)
40
42
 
41
43
 
42
44
  @lru_cache
@@ -62,6 +64,7 @@ def negative_schema(
62
64
  generation_config: GenerationConfig,
63
65
  *,
64
66
  custom_formats: dict[str, st.SearchStrategy[str]],
67
+ validator_cls: type[jsonschema.Validator],
65
68
  ) -> st.SearchStrategy:
66
69
  """A strategy for instances that DO NOT match the input schema.
67
70
 
@@ -69,7 +72,7 @@ def negative_schema(
69
72
  """
70
73
  # The mutated schema is passed to `from_schema` and guarded against producing instances valid against
71
74
  # the original schema.
72
- cache_key = CacheKey(operation_name, location, schema)
75
+ cache_key = CacheKey(operation_name, location, schema, validator_cls)
73
76
  validator = get_validator(cache_key)
74
77
  keywords, non_keywords = split_schema(cache_key)
75
78