schemathesis 4.1.3__py3-none-any.whl → 4.2.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 (70) hide show
  1. schemathesis/cli/commands/run/executor.py +1 -1
  2. schemathesis/cli/commands/run/handlers/base.py +28 -1
  3. schemathesis/cli/commands/run/handlers/cassettes.py +10 -12
  4. schemathesis/cli/commands/run/handlers/junitxml.py +5 -6
  5. schemathesis/cli/commands/run/handlers/output.py +7 -1
  6. schemathesis/cli/ext/fs.py +1 -1
  7. schemathesis/config/_diff_base.py +3 -1
  8. schemathesis/config/_operations.py +2 -0
  9. schemathesis/config/_phases.py +21 -4
  10. schemathesis/config/_projects.py +10 -2
  11. schemathesis/core/adapter.py +34 -0
  12. schemathesis/core/errors.py +29 -5
  13. schemathesis/core/jsonschema/__init__.py +13 -0
  14. schemathesis/core/jsonschema/bundler.py +163 -0
  15. schemathesis/{specs/openapi/constants.py → core/jsonschema/keywords.py} +0 -8
  16. schemathesis/core/jsonschema/references.py +122 -0
  17. schemathesis/core/jsonschema/types.py +41 -0
  18. schemathesis/core/media_types.py +6 -4
  19. schemathesis/core/parameters.py +37 -0
  20. schemathesis/core/transforms.py +25 -2
  21. schemathesis/core/validation.py +19 -0
  22. schemathesis/engine/context.py +1 -1
  23. schemathesis/engine/errors.py +11 -18
  24. schemathesis/engine/phases/stateful/_executor.py +1 -1
  25. schemathesis/engine/phases/unit/_executor.py +30 -13
  26. schemathesis/errors.py +4 -0
  27. schemathesis/filters.py +2 -2
  28. schemathesis/generation/coverage.py +89 -13
  29. schemathesis/generation/hypothesis/__init__.py +4 -1
  30. schemathesis/generation/hypothesis/builder.py +108 -70
  31. schemathesis/generation/meta.py +5 -14
  32. schemathesis/generation/overrides.py +17 -17
  33. schemathesis/pytest/lazy.py +1 -1
  34. schemathesis/pytest/plugin.py +1 -6
  35. schemathesis/schemas.py +22 -72
  36. schemathesis/specs/graphql/schemas.py +27 -16
  37. schemathesis/specs/openapi/_hypothesis.py +83 -68
  38. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  39. schemathesis/specs/openapi/adapter/parameters.py +504 -0
  40. schemathesis/specs/openapi/adapter/protocol.py +57 -0
  41. schemathesis/specs/openapi/adapter/references.py +19 -0
  42. schemathesis/specs/openapi/adapter/responses.py +329 -0
  43. schemathesis/specs/openapi/adapter/security.py +141 -0
  44. schemathesis/specs/openapi/adapter/v2.py +28 -0
  45. schemathesis/specs/openapi/adapter/v3_0.py +28 -0
  46. schemathesis/specs/openapi/adapter/v3_1.py +28 -0
  47. schemathesis/specs/openapi/checks.py +99 -90
  48. schemathesis/specs/openapi/converter.py +114 -27
  49. schemathesis/specs/openapi/examples.py +210 -168
  50. schemathesis/specs/openapi/negative/__init__.py +12 -7
  51. schemathesis/specs/openapi/negative/mutations.py +68 -40
  52. schemathesis/specs/openapi/references.py +2 -175
  53. schemathesis/specs/openapi/schemas.py +142 -490
  54. schemathesis/specs/openapi/serialization.py +15 -7
  55. schemathesis/specs/openapi/stateful/__init__.py +17 -12
  56. schemathesis/specs/openapi/stateful/inference.py +13 -11
  57. schemathesis/specs/openapi/stateful/links.py +5 -20
  58. schemathesis/specs/openapi/types/__init__.py +3 -0
  59. schemathesis/specs/openapi/types/v3.py +68 -0
  60. schemathesis/specs/openapi/utils.py +1 -13
  61. schemathesis/transport/requests.py +3 -11
  62. schemathesis/transport/serialization.py +63 -27
  63. schemathesis/transport/wsgi.py +1 -8
  64. {schemathesis-4.1.3.dist-info → schemathesis-4.2.0.dist-info}/METADATA +2 -2
  65. {schemathesis-4.1.3.dist-info → schemathesis-4.2.0.dist-info}/RECORD +68 -53
  66. schemathesis/specs/openapi/parameters.py +0 -405
  67. schemathesis/specs/openapi/security.py +0 -162
  68. {schemathesis-4.1.3.dist-info → schemathesis-4.2.0.dist-info}/WHEEL +0 -0
  69. {schemathesis-4.1.3.dist-info → schemathesis-4.2.0.dist-info}/entry_points.txt +0 -0
  70. {schemathesis-4.1.3.dist-info → schemathesis-4.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,29 +3,36 @@ from __future__ import annotations
3
3
  from contextlib import suppress
4
4
  from dataclasses import dataclass
5
5
  from functools import lru_cache
6
- from itertools import chain, cycle, islice
7
- from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast
6
+ from itertools import cycle, islice
7
+ from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast, overload
8
8
 
9
9
  import requests
10
10
  from hypothesis_jsonschema import from_schema
11
11
 
12
12
  from schemathesis.config import GenerationConfig
13
+ from schemathesis.core.compat import RefResolutionError, RefResolver
14
+ from schemathesis.core.errors import InfiniteRecursiveReference, UnresolvableReference
15
+ from schemathesis.core.jsonschema import references
16
+ from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
13
17
  from schemathesis.core.transforms import deepclone
14
18
  from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
15
19
  from schemathesis.generation.case import Case
16
20
  from schemathesis.generation.hypothesis import examples
17
21
  from schemathesis.generation.meta import TestPhase
18
22
  from schemathesis.schemas import APIOperation
23
+ from schemathesis.specs.openapi.adapter import OpenApiResponses
24
+ from schemathesis.specs.openapi.adapter.parameters import OpenApiBody, OpenApiParameter
25
+ from schemathesis.specs.openapi.adapter.security import OpenApiSecurityParameters
19
26
  from schemathesis.specs.openapi.serialization import get_serializers_for_operation
20
27
 
21
28
  from ._hypothesis import get_default_format_strategies, openapi_cases
22
- from .constants import LOCATION_TO_CONTAINER
23
29
  from .formats import STRING_FORMATS
24
- from .parameters import OpenAPIBody, OpenAPIParameter
25
30
 
26
31
  if TYPE_CHECKING:
27
32
  from hypothesis.strategies import SearchStrategy
28
33
 
34
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
35
+
29
36
 
30
37
  @dataclass
31
38
  class ParameterExample:
@@ -66,7 +73,7 @@ def merge_kwargs(left: dict[str, Any], right: dict[str, Any]) -> dict[str, Any]:
66
73
 
67
74
 
68
75
  def get_strategies_from_examples(
69
- operation: APIOperation[OpenAPIParameter], **kwargs: Any
76
+ operation: APIOperation[OpenApiParameter, OpenApiResponses, OpenApiSecurityParameters], **kwargs: Any
70
77
  ) -> list[SearchStrategy[Case]]:
71
78
  """Build a set of strategies that generate test cases based on explicit examples in the schema."""
72
79
  maps = get_serializers_for_operation(operation)
@@ -93,67 +100,118 @@ def get_strategies_from_examples(
93
100
  ]
94
101
 
95
102
 
96
- def extract_top_level(operation: APIOperation[OpenAPIParameter]) -> Generator[Example, None, None]:
103
+ def extract_top_level(
104
+ operation: APIOperation[OpenApiParameter, OpenApiResponses, OpenApiSecurityParameters],
105
+ ) -> Generator[Example, None, None]:
97
106
  """Extract top-level parameter examples from `examples` & `example` fields."""
98
- responses = find_in_responses(operation)
107
+ from .schemas import BaseOpenAPISchema
108
+
109
+ assert isinstance(operation.schema, BaseOpenAPISchema)
110
+
111
+ responses = list(operation.responses.iter_examples())
112
+ seen_references: set[str] = set()
99
113
  for parameter in operation.iter_parameters():
100
114
  if "schema" in parameter.definition:
101
- definitions = [parameter.definition, *_expand_subschemas(parameter.definition["schema"])]
115
+ schema = parameter.definition["schema"]
116
+ resolver = RefResolver.from_schema(schema)
117
+ seen_references.clear()
118
+ definitions = [
119
+ parameter.definition,
120
+ *_expand_subschemas(schema=schema, resolver=resolver, seen_references=seen_references),
121
+ ]
102
122
  else:
103
123
  definitions = [parameter.definition]
104
124
  for definition in definitions:
105
125
  # Open API 2 also supports `example`
106
- for example_field in {"example", parameter.example_field}:
107
- if isinstance(definition, dict) and example_field in definition:
126
+ for example_keyword in {"example", parameter.adapter.example_keyword}:
127
+ if isinstance(definition, dict) and example_keyword in definition:
108
128
  yield ParameterExample(
109
- container=LOCATION_TO_CONTAINER[parameter.location],
129
+ container=parameter.location.container_name,
110
130
  name=parameter.name,
111
- value=definition[example_field],
131
+ value=definition[example_keyword],
112
132
  )
113
- if parameter.examples_field in parameter.definition:
114
- unresolved_definition = _find_parameter_examples_definition(
115
- operation, parameter.name, parameter.examples_field
116
- )
117
- for value in extract_inner_examples(parameter.definition[parameter.examples_field], unresolved_definition):
118
- yield ParameterExample(
119
- container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
120
- )
133
+ if parameter.adapter.examples_container_keyword in parameter.definition:
134
+ for value in extract_inner_examples(
135
+ parameter.definition[parameter.adapter.examples_container_keyword], operation.schema
136
+ ):
137
+ yield ParameterExample(container=parameter.location.container_name, name=parameter.name, value=value)
121
138
  if "schema" in parameter.definition:
122
- for schema in _expand_subschemas(parameter.definition["schema"]):
123
- if isinstance(schema, dict) and parameter.examples_field in schema:
124
- for value in schema[parameter.examples_field]:
139
+ schema = parameter.definition["schema"]
140
+ resolver = RefResolver.from_schema(schema)
141
+ seen_references.clear()
142
+ for expanded in _expand_subschemas(schema=schema, resolver=resolver, seen_references=seen_references):
143
+ if isinstance(expanded, dict) and parameter.adapter.examples_container_keyword in expanded:
144
+ for value in expanded[parameter.adapter.examples_container_keyword]:
125
145
  yield ParameterExample(
126
- container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
146
+ container=parameter.location.container_name, name=parameter.name, value=value
127
147
  )
128
148
  for value in find_matching_in_responses(responses, parameter.name):
129
- yield ParameterExample(
130
- container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
131
- )
149
+ yield ParameterExample(container=parameter.location.container_name, name=parameter.name, value=value)
132
150
  for alternative in operation.body:
133
- alternative = cast(OpenAPIBody, alternative)
134
- if "schema" in alternative.definition:
135
- definitions = [alternative.definition, *_expand_subschemas(alternative.definition["schema"])]
151
+ body = cast(OpenApiBody, alternative)
152
+ if "schema" in body.definition:
153
+ schema = body.definition["schema"]
154
+ resolver = RefResolver.from_schema(schema)
155
+ seen_references.clear()
156
+ definitions = [
157
+ body.definition,
158
+ *_expand_subschemas(schema=schema, resolver=resolver, seen_references=seen_references),
159
+ ]
136
160
  else:
137
- definitions = [alternative.definition]
161
+ definitions = [body.definition]
138
162
  for definition in definitions:
139
163
  # Open API 2 also supports `example`
140
- for example_field in {"example", alternative.example_field}:
141
- if isinstance(definition, dict) and example_field in definition:
142
- yield BodyExample(value=definition[example_field], media_type=alternative.media_type)
143
- if alternative.examples_field in alternative.definition:
144
- unresolved_definition = _find_request_body_examples_definition(operation, alternative)
164
+ for example_keyword in {"example", body.adapter.example_keyword}:
165
+ if isinstance(definition, dict) and example_keyword in definition:
166
+ yield BodyExample(value=definition[example_keyword], media_type=body.media_type)
167
+ if body.adapter.examples_container_keyword in body.definition:
145
168
  for value in extract_inner_examples(
146
- alternative.definition[alternative.examples_field], unresolved_definition
169
+ body.definition[body.adapter.examples_container_keyword], operation.schema
147
170
  ):
148
- yield BodyExample(value=value, media_type=alternative.media_type)
149
- if "schema" in alternative.definition:
150
- for schema in _expand_subschemas(alternative.definition["schema"]):
151
- if isinstance(schema, dict) and alternative.examples_field in schema:
152
- for value in schema[alternative.examples_field]:
153
- yield BodyExample(value=value, media_type=alternative.media_type)
171
+ yield BodyExample(value=value, media_type=body.media_type)
172
+ if "schema" in body.definition:
173
+ schema = body.definition["schema"]
174
+ resolver = RefResolver.from_schema(schema)
175
+ seen_references.clear()
176
+ for expanded in _expand_subschemas(schema=schema, resolver=resolver, seen_references=seen_references):
177
+ if isinstance(expanded, dict) and body.adapter.examples_container_keyword in expanded:
178
+ for value in expanded[body.adapter.examples_container_keyword]:
179
+ yield BodyExample(value=value, media_type=body.media_type)
154
180
 
155
181
 
156
- def _expand_subschemas(schema: dict[str, Any] | bool) -> Generator[dict[str, Any] | bool, None, None]:
182
+ @overload
183
+ def _resolve_bundled(
184
+ schema: dict[str, Any], resolver: RefResolver, seen_references: set[str]
185
+ ) -> dict[str, Any]: ... # pragma: no cover
186
+
187
+
188
+ @overload
189
+ def _resolve_bundled(schema: bool, resolver: RefResolver, seen_references: set[str]) -> bool: ... # pragma: no cover
190
+
191
+
192
+ def _resolve_bundled(
193
+ schema: dict[str, Any] | bool, resolver: RefResolver, seen_references: set[str]
194
+ ) -> dict[str, Any] | bool:
195
+ if isinstance(schema, dict):
196
+ reference = schema.get("$ref")
197
+ if isinstance(reference, str):
198
+ if reference in seen_references:
199
+ # Try to remove recursive references to avoid infinite recursion
200
+ remaining_references = references.sanitize(schema)
201
+ if reference in remaining_references:
202
+ raise InfiniteRecursiveReference(reference)
203
+ seen_references.add(reference)
204
+ try:
205
+ _, schema = resolver.resolve(schema["$ref"])
206
+ except RefResolutionError as exc:
207
+ raise UnresolvableReference(reference) from exc
208
+ return schema
209
+
210
+
211
+ def _expand_subschemas(
212
+ *, schema: dict[str, Any] | bool, resolver: RefResolver, seen_references: set[str]
213
+ ) -> Generator[dict[str, Any] | bool, None, None]:
214
+ schema = _resolve_bundled(schema, resolver, seen_references)
157
215
  yield schema
158
216
  if isinstance(schema, dict):
159
217
  for key in ("anyOf", "oneOf"):
@@ -162,8 +220,10 @@ def _expand_subschemas(schema: dict[str, Any] | bool) -> Generator[dict[str, Any
162
220
  yield subschema
163
221
  if "allOf" in schema:
164
222
  subschema = deepclone(schema["allOf"][0])
223
+ subschema = _resolve_bundled(subschema, resolver, seen_references)
165
224
  for sub in schema["allOf"][1:]:
166
225
  if isinstance(sub, dict):
226
+ sub = _resolve_bundled(sub, resolver, seen_references)
167
227
  for key, value in sub.items():
168
228
  if key == "properties":
169
229
  subschema.setdefault("properties", {}).update(value)
@@ -178,63 +238,21 @@ def _expand_subschemas(schema: dict[str, Any] | bool) -> Generator[dict[str, Any
178
238
  yield subschema
179
239
 
180
240
 
181
- def _find_parameter_examples_definition(
182
- operation: APIOperation[OpenAPIParameter], parameter_name: str, field_name: str
183
- ) -> dict[str, Any]:
184
- """Find the original, unresolved `examples` definition of a parameter."""
185
- from .schemas import BaseOpenAPISchema
186
-
187
- schema = cast(BaseOpenAPISchema, operation.schema)
188
- raw_schema = schema.raw_schema
189
- path_data = raw_schema["paths"][operation.path]
190
- parameters = chain(path_data[operation.method].get("parameters", []), path_data.get("parameters", []))
191
- for parameter in parameters:
192
- if "$ref" in parameter:
193
- _, parameter = schema.resolver.resolve(parameter["$ref"])
194
- if parameter["name"] == parameter_name:
195
- return parameter[field_name]
196
- raise RuntimeError("Example definition is not found. It should not happen")
197
-
198
-
199
- def _find_request_body_examples_definition(
200
- operation: APIOperation[OpenAPIParameter], alternative: OpenAPIBody
201
- ) -> dict[str, Any]:
202
- """Find the original, unresolved `examples` definition of a request body variant."""
203
- from .schemas import BaseOpenAPISchema
204
-
205
- schema = cast(BaseOpenAPISchema, operation.schema)
206
- if schema.specification.version == "2.0":
207
- raw_schema = schema.raw_schema
208
- path_data = raw_schema["paths"][operation.path]
209
- parameters = chain(path_data[operation.method].get("parameters", []), path_data.get("parameters", []))
210
- for parameter in parameters:
211
- if "$ref" in parameter:
212
- _, parameter = schema.resolver.resolve(parameter["$ref"])
213
- if parameter["in"] == "body":
214
- return parameter[alternative.examples_field]
215
- raise RuntimeError("Example definition is not found. It should not happen")
216
- request_body = operation.definition.raw["requestBody"]
217
- while "$ref" in request_body:
218
- _, request_body = schema.resolver.resolve(request_body["$ref"])
219
- return request_body["content"][alternative.media_type][alternative.examples_field]
220
-
221
-
222
- def extract_inner_examples(
223
- examples: dict[str, Any] | list, unresolved_definition: dict[str, Any]
224
- ) -> Generator[Any, None, None]:
241
+ def extract_inner_examples(examples: dict[str, Any] | list, schema: BaseOpenAPISchema) -> Generator[Any, None, None]:
225
242
  """Extract exact examples values from the `examples` dictionary."""
226
243
  if isinstance(examples, dict):
227
- for name, example in examples.items():
228
- if "$ref" in unresolved_definition[name] and "value" not in example and "externalValue" not in example:
229
- # The example here is a resolved example and should be yielded as is
230
- yield example
244
+ for example in examples.values():
231
245
  if isinstance(example, dict):
246
+ if "$ref" in example:
247
+ _, example = schema.resolver.resolve(example["$ref"])
232
248
  if "value" in example:
233
249
  yield example["value"]
234
250
  elif "externalValue" in example:
235
251
  with suppress(requests.RequestException):
236
252
  # Report a warning if not available?
237
253
  yield load_external_example(example["externalValue"])
254
+ elif example:
255
+ yield example
238
256
  elif isinstance(examples, list):
239
257
  yield from examples
240
258
 
@@ -247,47 +265,93 @@ def load_external_example(url: str) -> bytes:
247
265
  return response.content
248
266
 
249
267
 
250
- def extract_from_schemas(operation: APIOperation[OpenAPIParameter]) -> Generator[Example, None, None]:
268
+ def extract_from_schemas(
269
+ operation: APIOperation[OpenApiParameter, OpenApiResponses, OpenApiSecurityParameters],
270
+ ) -> Generator[Example, None, None]:
251
271
  """Extract examples from parameters' schema definitions."""
272
+ seen_references: set[str] = set()
252
273
  for parameter in operation.iter_parameters():
253
- schema = parameter.as_json_schema(operation)
254
- for value in extract_from_schema(operation, schema, parameter.example_field, parameter.examples_field):
255
- yield ParameterExample(
256
- container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
257
- )
274
+ schema = parameter.optimized_schema
275
+ if isinstance(schema, bool):
276
+ continue
277
+ resolver = RefResolver.from_schema(schema)
278
+ seen_references.clear()
279
+ bundle_storage = schema.get(BUNDLE_STORAGE_KEY)
280
+ for value in extract_from_schema(
281
+ operation=operation,
282
+ schema=schema,
283
+ example_keyword=parameter.adapter.example_keyword,
284
+ examples_container_keyword=parameter.adapter.examples_container_keyword,
285
+ resolver=resolver,
286
+ seen_references=seen_references,
287
+ bundle_storage=bundle_storage,
288
+ ):
289
+ yield ParameterExample(container=parameter.location.container_name, name=parameter.name, value=value)
258
290
  for alternative in operation.body:
259
- alternative = cast(OpenAPIBody, alternative)
260
- schema = alternative.as_json_schema(operation)
261
- for example_field, examples_field in (("example", "examples"), ("x-example", "x-examples")):
262
- for value in extract_from_schema(operation, schema, example_field, examples_field):
263
- yield BodyExample(value=value, media_type=alternative.media_type)
291
+ body = cast(OpenApiBody, alternative)
292
+ schema = body.optimized_schema
293
+ if isinstance(schema, bool):
294
+ continue
295
+ resolver = RefResolver.from_schema(schema)
296
+ bundle_storage = schema.get(BUNDLE_STORAGE_KEY)
297
+ for example_keyword, examples_container_keyword in (("example", "examples"), ("x-example", "x-examples")):
298
+ seen_references.clear()
299
+ for value in extract_from_schema(
300
+ operation=operation,
301
+ schema=schema,
302
+ example_keyword=example_keyword,
303
+ examples_container_keyword=examples_container_keyword,
304
+ resolver=resolver,
305
+ seen_references=seen_references,
306
+ bundle_storage=bundle_storage,
307
+ ):
308
+ yield BodyExample(value=value, media_type=body.media_type)
264
309
 
265
310
 
266
311
  def extract_from_schema(
267
- operation: APIOperation[OpenAPIParameter],
312
+ *,
313
+ operation: APIOperation[OpenApiParameter, OpenApiResponses, OpenApiSecurityParameters],
268
314
  schema: dict[str, Any],
269
- example_field_name: str,
270
- examples_field_name: str,
315
+ example_keyword: str,
316
+ examples_container_keyword: str,
317
+ resolver: RefResolver,
318
+ seen_references: set[str],
319
+ bundle_storage: dict[str, Any] | None,
271
320
  ) -> Generator[Any, None, None]:
272
321
  """Extract all examples from a single schema definition."""
273
322
  # This implementation supports only `properties` and `items`
323
+ schema = _resolve_bundled(schema, resolver, seen_references)
274
324
  if "properties" in schema:
275
325
  variants = {}
276
326
  required = schema.get("required", [])
277
327
  to_generate: dict[str, Any] = {}
278
328
  for name, subschema in schema["properties"].items():
279
329
  values = []
280
- for subsubschema in _expand_subschemas(subschema):
330
+ for subsubschema in _expand_subschemas(
331
+ schema=subschema, resolver=resolver, seen_references=seen_references
332
+ ):
281
333
  if isinstance(subsubschema, bool):
282
334
  to_generate[name] = subsubschema
283
335
  continue
284
- if example_field_name in subsubschema:
285
- values.append(subsubschema[example_field_name])
286
- if examples_field_name in subsubschema and isinstance(subsubschema[examples_field_name], list):
336
+ if example_keyword in subsubschema:
337
+ values.append(subsubschema[example_keyword])
338
+ if examples_container_keyword in subsubschema and isinstance(
339
+ subsubschema[examples_container_keyword], list
340
+ ):
287
341
  # These are JSON Schema examples, which is an array of values
288
- values.extend(subsubschema[examples_field_name])
342
+ values.extend(subsubschema[examples_container_keyword])
289
343
  # Check nested examples as well
290
- values.extend(extract_from_schema(operation, subsubschema, example_field_name, examples_field_name))
344
+ values.extend(
345
+ extract_from_schema(
346
+ operation=operation,
347
+ schema=subsubschema,
348
+ example_keyword=example_keyword,
349
+ examples_container_keyword=examples_container_keyword,
350
+ resolver=resolver,
351
+ seen_references=seen_references,
352
+ bundle_storage=bundle_storage,
353
+ )
354
+ )
291
355
  if not values:
292
356
  if name in required:
293
357
  # Defer generation to only generate these variants if at least one property has examples
@@ -300,7 +364,9 @@ def extract_from_schema(
300
364
  if name in variants:
301
365
  # Generated by one of `anyOf` or similar sub-schemas
302
366
  continue
303
- subschema = operation.schema.prepare_schema(subschema)
367
+ if bundle_storage is not None:
368
+ subschema = dict(subschema)
369
+ subschema[BUNDLE_STORAGE_KEY] = bundle_storage
304
370
  generated = _generate_single_example(subschema, config)
305
371
  variants[name] = [generated]
306
372
  # Calculate the maximum number of examples any property has
@@ -313,7 +379,15 @@ def extract_from_schema(
313
379
  }
314
380
  elif "items" in schema and isinstance(schema["items"], dict):
315
381
  # Each inner value should be wrapped in an array
316
- for value in extract_from_schema(operation, schema["items"], example_field_name, examples_field_name):
382
+ for value in extract_from_schema(
383
+ operation=operation,
384
+ schema=schema["items"],
385
+ example_keyword=example_keyword,
386
+ examples_container_keyword=examples_container_keyword,
387
+ resolver=resolver,
388
+ seen_references=seen_references,
389
+ bundle_storage=bundle_storage,
390
+ ):
317
391
  yield [value]
318
392
 
319
393
 
@@ -378,69 +452,37 @@ def _produce_parameter_combinations(parameters: dict[str, dict[str, list]]) -> G
378
452
  }
379
453
 
380
454
 
381
- def find_in_responses(operation: APIOperation) -> dict[str, list[dict[str, Any]]]:
382
- """Find schema examples in responses."""
383
- output: dict[str, list[dict[str, Any]]] = {}
384
- for status_code, response in operation.definition.raw.get("responses", {}).items():
385
- if not str(status_code).startswith("2"):
386
- # Check only 2xx responses
387
- continue
388
- if isinstance(response, dict) and "$ref" in response:
389
- _, response = operation.schema.resolver.resolve_in_scope(response, operation.definition.scope) # type:ignore[attr-defined]
390
- for media_type, definition in response.get("content", {}).items():
391
- schema_ref = definition.get("schema", {}).get("$ref")
392
- if schema_ref:
393
- name = schema_ref.split("/")[-1]
394
- else:
395
- name = f"{status_code}/{media_type}"
396
- for examples_field, example_field in (
397
- ("examples", "example"),
398
- ("x-examples", "x-example"),
399
- ):
400
- examples = definition.get(examples_field, {})
401
- if isinstance(examples, dict):
402
- for example in examples.values():
403
- if "value" in example:
404
- output.setdefault(name, []).append(example["value"])
405
- elif isinstance(examples, list):
406
- output.setdefault(name, []).extend(examples)
407
- if example_field in definition:
408
- output.setdefault(name, []).append(definition[example_field])
409
- return output
410
-
411
-
412
455
  NOT_FOUND = object()
413
456
 
414
457
 
415
- def find_matching_in_responses(examples: dict[str, list], param: str) -> Iterator[Any]:
458
+ def find_matching_in_responses(examples: list[tuple[str, object]], param: str) -> Iterator[Any]:
416
459
  """Find matching parameter examples."""
417
460
  normalized = param.lower()
418
461
  is_id_param = normalized.endswith("id")
419
462
  # Extract values from response examples that match input parameters.
420
463
  # E.g., for `GET /orders/{id}/`, use "id" or "orderId" from `Order` response
421
464
  # as examples for the "id" path parameter.
422
- for schema_name, schema_examples in examples.items():
423
- for example in schema_examples:
424
- if not isinstance(example, dict):
425
- continue
426
- # Unwrapping example from `{"item": [{...}]}`
427
- if isinstance(example, dict):
428
- inner = next((value for key, value in example.items() if key.lower() == schema_name.lower()), None)
429
- if inner is not None:
430
- if isinstance(inner, list):
431
- for sub_example in inner:
432
- if isinstance(sub_example, dict):
433
- for found in _find_matching_in_responses(
434
- sub_example, schema_name, param, normalized, is_id_param
435
- ):
436
- if found is not NOT_FOUND:
437
- yield found
438
- continue
439
- if isinstance(inner, dict):
440
- example = inner
441
- for found in _find_matching_in_responses(example, schema_name, param, normalized, is_id_param):
442
- if found is not NOT_FOUND:
443
- yield found
465
+ for schema_name, example in examples:
466
+ if not isinstance(example, dict):
467
+ continue
468
+ # Unwrapping example from `{"item": [{...}]}`
469
+ if isinstance(example, dict):
470
+ inner = next((value for key, value in example.items() if key.lower() == schema_name.lower()), None)
471
+ if inner is not None:
472
+ if isinstance(inner, list):
473
+ for sub_example in inner:
474
+ if isinstance(sub_example, dict):
475
+ for found in _find_matching_in_responses(
476
+ sub_example, schema_name, param, normalized, is_id_param
477
+ ):
478
+ if found is not NOT_FOUND:
479
+ yield found
480
+ continue
481
+ if isinstance(inner, dict):
482
+ example = inner
483
+ for found in _find_matching_in_responses(example, schema_name, param, normalized, is_id_param):
484
+ if found is not NOT_FOUND:
485
+ yield found
444
486
 
445
487
 
446
488
  def _find_matching_in_responses(
@@ -10,8 +10,10 @@ from hypothesis import strategies as st
10
10
  from hypothesis_jsonschema import from_schema
11
11
 
12
12
  from schemathesis.config import GenerationConfig
13
+ from schemathesis.core.jsonschema import ALL_KEYWORDS
14
+ from schemathesis.core.jsonschema.types import JsonSchema
15
+ from schemathesis.core.parameters import ParameterLocation
13
16
 
14
- from ..constants import ALL_KEYWORDS
15
17
  from .mutations import MutationContext
16
18
 
17
19
  if TYPE_CHECKING:
@@ -27,7 +29,7 @@ class CacheKey:
27
29
 
28
30
  operation_name: str
29
31
  location: str
30
- schema: Schema
32
+ schema: JsonSchema
31
33
  validator_cls: type[jsonschema.Validator]
32
34
 
33
35
  __slots__ = ("operation_name", "location", "schema", "validator_cls")
@@ -50,7 +52,8 @@ def split_schema(cache_key: CacheKey) -> tuple[Schema, Schema]:
50
52
  The first one contains only validation JSON Schema keywords, the second one everything else.
51
53
  """
52
54
  keywords, non_keywords = {}, {}
53
- for keyword, value in cache_key.schema.items():
55
+ schema = {} if isinstance(cache_key.schema, bool) else cache_key.schema
56
+ for keyword, value in schema.items():
54
57
  if keyword in ALL_KEYWORDS:
55
58
  keywords[keyword] = value
56
59
  else:
@@ -59,9 +62,9 @@ def split_schema(cache_key: CacheKey) -> tuple[Schema, Schema]:
59
62
 
60
63
 
61
64
  def negative_schema(
62
- schema: Schema,
65
+ schema: JsonSchema,
63
66
  operation_name: str,
64
- location: str,
67
+ location: ParameterLocation,
65
68
  media_type: str | None,
66
69
  generation_config: GenerationConfig,
67
70
  *,
@@ -78,7 +81,7 @@ def negative_schema(
78
81
  validator = get_validator(cache_key)
79
82
  keywords, non_keywords = split_schema(cache_key)
80
83
 
81
- if location == "query":
84
+ if location == ParameterLocation.QUERY:
82
85
 
83
86
  def filter_values(value: dict[str, Any]) -> bool:
84
87
  return is_non_empty_query(value) and not validator.is_valid(value)
@@ -113,7 +116,9 @@ def is_non_empty_query(query: dict[str, Any]) -> bool:
113
116
 
114
117
 
115
118
  @st.composite # type: ignore
116
- def mutated(draw: Draw, keywords: Schema, non_keywords: Schema, location: str, media_type: str | None) -> Any:
119
+ def mutated(
120
+ draw: Draw, keywords: Schema, non_keywords: Schema, location: ParameterLocation, media_type: str | None
121
+ ) -> Any:
117
122
  return MutationContext(
118
123
  keywords=keywords, non_keywords=non_keywords, location=location, media_type=media_type
119
124
  ).mutate(draw)