schemathesis 4.3.7__py3-none-any.whl → 4.3.9__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.

Potentially problematic release.


This version of schemathesis might be problematic. Click here for more details.

@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from dataclasses import dataclass
3
4
  from typing import TYPE_CHECKING, Any
4
5
 
5
6
  from schemathesis.core.errors import InfiniteRecursiveReference
@@ -24,6 +25,14 @@ class BundleError(Exception):
24
25
  return f"Cannot bundle `{self.reference}`: expected JSON Schema (object or boolean), got {to_json_type_name(self.value)}"
25
26
 
26
27
 
28
+ @dataclass
29
+ class Bundle:
30
+ schema: JsonSchema
31
+ name_to_uri: dict[str, str]
32
+
33
+ __slots__ = ("schema", "name_to_uri")
34
+
35
+
27
36
  class Bundler:
28
37
  """Bundler tracks schema ids stored in a bundle."""
29
38
 
@@ -34,16 +43,16 @@ class Bundler:
34
43
  def __init__(self) -> None:
35
44
  self.counter = 0
36
45
 
37
- def bundle(self, schema: JsonSchema, resolver: RefResolver, *, inline_recursive: bool) -> JsonSchema:
46
+ def bundle(self, schema: JsonSchema, resolver: RefResolver, *, inline_recursive: bool) -> Bundle:
38
47
  """Bundle a JSON Schema by embedding all references."""
39
48
  # Inlining recursive reference is required (for now) for data generation, but is unsound for data validation
40
49
  if not isinstance(schema, dict):
41
- return schema
50
+ return Bundle(schema=schema, name_to_uri={})
42
51
 
43
52
  # Track visited URIs and their local definition names
44
53
  inlining_for_recursion: set[str] = set()
45
54
  visited: set[str] = set()
46
- uri_to_def_name: dict[str, str] = {}
55
+ uri_to_name: dict[str, str] = {}
47
56
  defs = {}
48
57
 
49
58
  has_recursive_references = False
@@ -52,11 +61,11 @@ class Bundler:
52
61
 
53
62
  def get_def_name(uri: str) -> str:
54
63
  """Generate or retrieve the local definition name for a URI."""
55
- name = uri_to_def_name.get(uri)
64
+ name = uri_to_name.get(uri)
56
65
  if name is None:
57
66
  self.counter += 1
58
67
  name = f"schema{self.counter}"
59
- uri_to_def_name[uri] = name
68
+ uri_to_name[uri] = name
60
69
  return name
61
70
 
62
71
  def bundle_recursive(current: JsonSchema | list[JsonSchema]) -> JsonSchema | list[JsonSchema]:
@@ -162,13 +171,13 @@ class Bundler:
162
171
  for value in defs.values():
163
172
  if isinstance(value, dict):
164
173
  result.update(value)
165
- return result
174
+ return Bundle(schema=result, name_to_uri={})
166
175
 
167
176
  if defs:
168
177
  bundled[BUNDLE_STORAGE_KEY] = defs
169
- return bundled
178
+ return Bundle(schema=bundled, name_to_uri={v: k for k, v in uri_to_name.items()})
170
179
 
171
180
 
172
- def bundle(schema: JsonSchema, resolver: RefResolver, *, inline_recursive: bool) -> JsonSchema:
181
+ def bundle(schema: JsonSchema, resolver: RefResolver, *, inline_recursive: bool) -> Bundle:
173
182
  """Bundle a JSON Schema by embedding all references."""
174
183
  return Bundler().bundle(schema, resolver, inline_recursive=inline_recursive)
@@ -7,6 +7,8 @@ from dataclasses import dataclass
7
7
  from functools import lru_cache, partial
8
8
  from itertools import combinations
9
9
 
10
+ from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
11
+
10
12
  try:
11
13
  from json.encoder import _make_iterencode # type: ignore[attr-defined]
12
14
  except ImportError:
@@ -346,6 +348,19 @@ class CoverageContext:
346
348
  if isinstance(schema, dict) and "examples" in schema:
347
349
  # Examples may contain binary data which will fail the canonicalisation process in `hypothesis-jsonschema`
348
350
  schema = {key: value for key, value in schema.items() if key != "examples"}
351
+ # Prevent some hard to satisfy schemas
352
+ if isinstance(schema, dict) and schema.get("additionalProperties") is False and "required" in schema:
353
+ # Set required properties to any value to simplify generation
354
+ schema = dict(schema)
355
+ properties = schema.setdefault("properties", {})
356
+ for key in schema["required"]:
357
+ properties.setdefault(key, {})
358
+
359
+ # Add bundled schemas if any
360
+ if isinstance(schema, dict) and BUNDLE_STORAGE_KEY in self.root_schema:
361
+ schema = dict(schema)
362
+ schema[BUNDLE_STORAGE_KEY] = self.root_schema[BUNDLE_STORAGE_KEY]
363
+
349
364
  return self.generate_from(from_schema(schema, custom_formats=self.custom_formats))
350
365
 
351
366
 
@@ -529,7 +529,6 @@ def _iter_coverage_cases(
529
529
 
530
530
  instant = Instant()
531
531
  responses = list(operation.responses.iter_examples())
532
- # NOTE: The HEAD method is excluded
533
532
  custom_formats = _build_custom_formats(generation_config)
534
533
 
535
534
  seen_negative = coverage.HashSet()
@@ -38,11 +38,13 @@ FORM_MEDIA_TYPES = frozenset(["multipart/form-data", "application/x-www-form-url
38
38
  class OpenApiComponent(ABC):
39
39
  definition: Mapping[str, Any]
40
40
  is_required: bool
41
+ name_to_uri: dict[str, str]
41
42
  adapter: SpecificationAdapter
42
43
 
43
44
  __slots__ = (
44
45
  "definition",
45
46
  "is_required",
47
+ "name_to_uri",
46
48
  "adapter",
47
49
  "_optimized_schema",
48
50
  "_unoptimized_schema",
@@ -142,9 +144,11 @@ class OpenApiParameter(OpenApiComponent):
142
144
  """OpenAPI operation parameter."""
143
145
 
144
146
  @classmethod
145
- def from_definition(cls, *, definition: Mapping[str, Any], adapter: SpecificationAdapter) -> OpenApiParameter:
147
+ def from_definition(
148
+ cls, *, definition: Mapping[str, Any], name_to_uri: dict[str, str], adapter: SpecificationAdapter
149
+ ) -> OpenApiParameter:
146
150
  is_required = definition.get("required", False)
147
- return cls(definition=definition, is_required=is_required, adapter=adapter)
151
+ return cls(definition=definition, is_required=is_required, name_to_uri=name_to_uri, adapter=adapter)
148
152
 
149
153
  @property
150
154
  def name(self) -> str:
@@ -174,12 +178,14 @@ class OpenApiBody(OpenApiComponent):
174
178
 
175
179
  media_type: str
176
180
  resource_name: str | None
181
+ name_to_uri: dict[str, str]
177
182
 
178
183
  __slots__ = (
179
184
  "definition",
180
185
  "is_required",
181
186
  "media_type",
182
187
  "resource_name",
188
+ "name_to_uri",
183
189
  "adapter",
184
190
  "_optimized_schema",
185
191
  "_unoptimized_schema",
@@ -195,6 +201,7 @@ class OpenApiBody(OpenApiComponent):
195
201
  is_required: bool,
196
202
  media_type: str,
197
203
  resource_name: str | None,
204
+ name_to_uri: dict[str, str],
198
205
  adapter: SpecificationAdapter,
199
206
  ) -> OpenApiBody:
200
207
  return cls(
@@ -202,6 +209,7 @@ class OpenApiBody(OpenApiComponent):
202
209
  is_required=is_required,
203
210
  media_type=media_type,
204
211
  resource_name=resource_name,
212
+ name_to_uri=name_to_uri,
205
213
  adapter=adapter,
206
214
  )
207
215
 
@@ -211,6 +219,7 @@ class OpenApiBody(OpenApiComponent):
211
219
  *,
212
220
  definition: Mapping[str, Any],
213
221
  media_type: str,
222
+ name_to_uri: dict[str, str],
214
223
  adapter: SpecificationAdapter,
215
224
  ) -> OpenApiBody:
216
225
  return cls(
@@ -218,6 +227,7 @@ class OpenApiBody(OpenApiComponent):
218
227
  is_required=True,
219
228
  media_type=media_type,
220
229
  resource_name=None,
230
+ name_to_uri=name_to_uri,
221
231
  adapter=adapter,
222
232
  )
223
233
 
@@ -273,19 +283,24 @@ def extract_parameter_schema_v3(parameter: Mapping[str, Any]) -> JsonSchema:
273
283
  return media_type_object.get("schema", {})
274
284
 
275
285
 
276
- def _bundle_parameter(parameter: Mapping, resolver: RefResolver, bundler: Bundler) -> dict:
286
+ def _bundle_parameter(
287
+ parameter: Mapping, resolver: RefResolver, bundler: Bundler
288
+ ) -> tuple[dict[str, Any], dict[str, str]]:
277
289
  """Bundle a parameter definition to make it self-contained."""
278
290
  _, definition = maybe_resolve(parameter, resolver, "")
279
291
  schema = definition.get("schema")
292
+ name_to_uri = {}
280
293
  if schema is not None:
281
294
  definition = {k: v for k, v in definition.items() if k != "schema"}
282
295
  try:
283
- definition["schema"] = bundler.bundle(schema, resolver, inline_recursive=True)
296
+ bundled = bundler.bundle(schema, resolver, inline_recursive=True)
297
+ definition["schema"] = bundled.schema
298
+ name_to_uri = bundled.name_to_uri
284
299
  except BundleError as exc:
285
300
  location = parameter.get("in", "")
286
301
  name = parameter.get("name", "<UNKNOWN>")
287
302
  raise InvalidSchema.from_bundle_error(exc, location, name) from exc
288
- return cast(dict, definition)
303
+ return cast(dict, definition), name_to_uri
289
304
 
290
305
 
291
306
  OPENAPI_20_DEFAULT_BODY_MEDIA_TYPE = "application/json"
@@ -308,15 +323,17 @@ def iter_parameters_v2(
308
323
  form_data_media_types = media_types or (OPENAPI_20_DEFAULT_FORM_MEDIA_TYPE,)
309
324
 
310
325
  form_parameters = []
326
+ form_name_to_uri = {}
311
327
  bundler = Bundler()
312
328
  for parameter in chain(definition.get("parameters", []), shared_parameters):
313
- parameter = _bundle_parameter(parameter, resolver, bundler)
329
+ parameter, name_to_uri = _bundle_parameter(parameter, resolver, bundler)
314
330
  if parameter["in"] in HEADER_LOCATIONS:
315
331
  check_header_name(parameter["name"])
316
332
 
317
333
  if parameter["in"] == "formData":
318
334
  # We need to gather form parameters first before creating a composite parameter for them
319
335
  form_parameters.append(parameter)
336
+ form_name_to_uri.update(name_to_uri)
320
337
  elif parameter["in"] == ParameterLocation.BODY:
321
338
  # Take the original definition & extract the resource_name from there
322
339
  resource_name = None
@@ -330,17 +347,20 @@ def iter_parameters_v2(
330
347
  definition=parameter,
331
348
  is_required=parameter.get("required", False),
332
349
  media_type=media_type,
350
+ name_to_uri=name_to_uri,
333
351
  resource_name=resource_name,
334
352
  adapter=adapter,
335
353
  )
336
354
  else:
337
- yield OpenApiParameter.from_definition(definition=parameter, adapter=adapter)
355
+ yield OpenApiParameter.from_definition(definition=parameter, name_to_uri=name_to_uri, adapter=adapter)
338
356
 
339
357
  if form_parameters:
340
358
  form_data = form_data_to_json_schema(form_parameters)
341
359
  for media_type in form_data_media_types:
342
360
  # Individual `formData` parameters are joined into a single "composite" one.
343
- yield OpenApiBody.from_form_parameters(definition=form_data, media_type=media_type, adapter=adapter)
361
+ yield OpenApiBody.from_form_parameters(
362
+ definition=form_data, media_type=media_type, name_to_uri=form_name_to_uri, adapter=adapter
363
+ )
344
364
 
345
365
 
346
366
  def iter_parameters_v3(
@@ -356,11 +376,11 @@ def iter_parameters_v3(
356
376
 
357
377
  bundler = Bundler()
358
378
  for parameter in chain(definition.get("parameters", []), shared_parameters):
359
- parameter = _bundle_parameter(parameter, resolver, bundler)
379
+ parameter, name_to_uri = _bundle_parameter(parameter, resolver, bundler)
360
380
  if parameter["in"] in HEADER_LOCATIONS:
361
381
  check_header_name(parameter["name"])
362
382
 
363
- yield OpenApiParameter.from_definition(definition=parameter, adapter=adapter)
383
+ yield OpenApiParameter.from_definition(definition=parameter, name_to_uri=name_to_uri, adapter=adapter)
364
384
 
365
385
  request_body_or_ref = operation.get("requestBody")
366
386
  if request_body_or_ref is not None:
@@ -372,6 +392,7 @@ def iter_parameters_v3(
372
392
  for media_type, content in request_body["content"].items():
373
393
  resource_name = None
374
394
  schema = content.get("schema")
395
+ name_to_uri = {}
375
396
  if isinstance(schema, dict):
376
397
  content = dict(content)
377
398
  if "$ref" in schema:
@@ -379,7 +400,8 @@ def iter_parameters_v3(
379
400
  try:
380
401
  to_bundle = cast(dict[str, Any], schema)
381
402
  bundled = bundler.bundle(to_bundle, resolver, inline_recursive=True)
382
- content["schema"] = bundled
403
+ content["schema"] = bundled.schema
404
+ name_to_uri = bundled.name_to_uri
383
405
  except BundleError as exc:
384
406
  raise InvalidSchema.from_bundle_error(exc, "body") from exc
385
407
  yield OpenApiBody.from_definition(
@@ -387,6 +409,7 @@ def iter_parameters_v3(
387
409
  is_required=required,
388
410
  media_type=media_type,
389
411
  resource_name=resource_name,
412
+ name_to_uri=name_to_uri,
390
413
  adapter=adapter,
391
414
  )
392
415
 
@@ -400,6 +423,7 @@ def build_path_parameter_v2(kwargs: Mapping[str, Any]) -> OpenApiParameter:
400
423
 
401
424
  return OpenApiParameter.from_definition(
402
425
  definition={"in": ParameterLocation.PATH.value, "required": True, "type": "string", "minLength": 1, **kwargs},
426
+ name_to_uri={},
403
427
  adapter=v2,
404
428
  )
405
429
 
@@ -414,6 +438,7 @@ def build_path_parameter_v3_0(kwargs: Mapping[str, Any]) -> OpenApiParameter:
414
438
  "schema": {"type": "string", "minLength": 1},
415
439
  **kwargs,
416
440
  },
441
+ name_to_uri={},
417
442
  adapter=v3_0,
418
443
  )
419
444
 
@@ -428,6 +453,7 @@ def build_path_parameter_v3_1(kwargs: Mapping[str, Any]) -> OpenApiParameter:
428
453
  "schema": {"type": "string", "minLength": 1},
429
454
  **kwargs,
430
455
  },
456
+ name_to_uri={},
431
457
  adapter=v3_1,
432
458
  )
433
459
 
@@ -226,7 +226,7 @@ def _prepare_schema(schema: JsonSchema, resolver: RefResolver, scope: str, nulla
226
226
  def _bundle_in_scope(schema: JsonSchema, resolver: RefResolver, scope: str) -> JsonSchema:
227
227
  resolver.push_scope(scope)
228
228
  try:
229
- return bundle(schema, resolver, inline_recursive=False)
229
+ return bundle(schema, resolver, inline_recursive=False).schema
230
230
  except RefResolutionError as exc:
231
231
  raise InvalidSchema.from_reference_resolution_error(exc, None, None) from None
232
232
  finally:
@@ -14,7 +14,7 @@ from schemathesis.core.failures import Failure
14
14
  from schemathesis.core.parameters import ParameterLocation
15
15
  from schemathesis.core.transport import Response
16
16
  from schemathesis.generation.case import Case
17
- from schemathesis.generation.meta import CoveragePhaseData
17
+ from schemathesis.generation.meta import CoveragePhaseData, TestPhase
18
18
  from schemathesis.openapi.checks import (
19
19
  AcceptedNegativeData,
20
20
  EnsureResourceAvailability,
@@ -227,9 +227,27 @@ def negative_data_rejection(ctx: CheckContext, response: Response, case: Case) -
227
227
  and response.status_code not in allowed_statuses
228
228
  and not has_only_additional_properties_in_non_body_parameters(case)
229
229
  ):
230
+ extra_info = ""
231
+ phase = case.meta.phase
232
+ if phase and phase.name == TestPhase.COVERAGE and isinstance(phase.data, CoveragePhaseData):
233
+ parts: list[str] = []
234
+ if "Missing" in phase.data.description:
235
+ extra_info = f"\nInvalid component: {phase.data.description}"
236
+ else:
237
+ if phase.data.parameter:
238
+ parts.append(f"parameter `{phase.data.parameter}`")
239
+ location = phase.data.parameter_location
240
+ if location:
241
+ parts.append(f"in {location.name.lower()}")
242
+ description = phase.data.description.lower()
243
+ if parts:
244
+ parts.append(f"({description})")
245
+ else:
246
+ parts.append(f"{description}")
247
+ extra_info = "\nInvalid component: " + " ".join(parts)
230
248
  raise AcceptedNegativeData(
231
249
  operation=case.operation.label,
232
- message=f"Invalid data should have been rejected\nExpected: {', '.join(config.expected_statuses)}",
250
+ message=f"Invalid data should have been rejected\nExpected: {', '.join(config.expected_statuses)}{extra_info}",
233
251
  status_code=response.status_code,
234
252
  expected_statuses=config.expected_statuses,
235
253
  )
@@ -406,7 +406,9 @@ class BaseOpenAPISchema(BaseSchema):
406
406
  and operation.get_parameter(name=param_name, location=param_location) is not None
407
407
  ):
408
408
  continue
409
- operation.add_parameter(OpenApiParameter.from_definition(definition=param, adapter=self.adapter))
409
+ operation.add_parameter(
410
+ OpenApiParameter.from_definition(definition=param, name_to_uri={}, adapter=self.adapter)
411
+ )
410
412
  self.dispatch_hook("before_init_operation", HookContext(operation=operation), operation)
411
413
  return operation
412
414
 
@@ -1,12 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Iterator
3
+ from typing import TYPE_CHECKING, Any, Iterator
4
4
 
5
5
  from schemathesis.core import media_types
6
6
  from schemathesis.core.errors import MalformedMediaType
7
7
  from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
8
8
  from schemathesis.core.jsonschema.types import get_type
9
9
  from schemathesis.core.parameters import ParameterLocation
10
+ from schemathesis.specs.openapi.adapter.parameters import resource_name_from_ref
10
11
  from schemathesis.specs.openapi.stateful.dependencies import naming
11
12
  from schemathesis.specs.openapi.stateful.dependencies.models import (
12
13
  CanonicalizationCache,
@@ -179,6 +180,14 @@ GENERIC_FIELD_NAMES = frozenset(
179
180
  )
180
181
 
181
182
 
183
+ def _maybe_resolve_bundled(root: dict[str, Any], schema: dict[str, Any]) -> dict[str, Any]:
184
+ # Right now, the body schema comes bundled to dependency analysis
185
+ if BUNDLE_STORAGE_KEY in root and "$ref" in schema:
186
+ key = schema["$ref"].split("/")[-1]
187
+ return root[BUNDLE_STORAGE_KEY][key]
188
+ return schema
189
+
190
+
182
191
  def _resolve_body_dependencies(
183
192
  *,
184
193
  body: OpenApiBody,
@@ -190,12 +199,30 @@ def _resolve_body_dependencies(
190
199
  if not isinstance(schema, dict):
191
200
  return
192
201
 
193
- # Right now, the body schema comes bundled to dependency analysis
194
- if BUNDLE_STORAGE_KEY in schema and "$ref" in schema:
195
- schema_key = schema["$ref"].split("/")[-1]
196
- resolved = schema[BUNDLE_STORAGE_KEY][schema_key]
197
- else:
198
- resolved = schema
202
+ resolved = _maybe_resolve_bundled(schema, schema)
203
+
204
+ # For `items`, we'll inject an array with extracted resource
205
+ items = resolved.get("items", {})
206
+ if items is not None:
207
+ resource_name = naming.from_path(operation.path)
208
+
209
+ if "$ref" in items:
210
+ schema_key = items["$ref"].split("/")[-1]
211
+ original_ref = body.name_to_uri[schema_key]
212
+ resource_name = resource_name_from_ref(original_ref)
213
+ resource = resources.get(resource_name)
214
+ if resource is None:
215
+ resource = ResourceDefinition.inferred_from_parameter(name=resource_name, parameter_name=None)
216
+ resources[resource_name] = resource
217
+ field = None
218
+ else:
219
+ field = None
220
+ yield InputSlot(
221
+ resource=resource,
222
+ resource_field=field,
223
+ parameter_name=0,
224
+ parameter_location=ParameterLocation.BODY,
225
+ )
199
226
 
200
227
  # Inspect each property that could be a part of some other resource
201
228
  properties = resolved.get("properties", {})
@@ -272,7 +299,7 @@ def update_input_field_bindings(resource_name: str, operations: OperationMap) ->
272
299
  for operation in operations.values():
273
300
  for input_slot in operation.inputs:
274
301
  # Skip inputs not using this resource
275
- if input_slot.resource.name != resource_name:
302
+ if input_slot.resource.name != resource_name or isinstance(input_slot.parameter_name, int):
276
303
  continue
277
304
 
278
305
  # Re-match parameter to upgraded resource fields
@@ -58,17 +58,24 @@ class DependencyGraph:
58
58
  links: dict[str, LinkDefinition] = {}
59
59
  for input_slot in consumer.inputs:
60
60
  if input_slot.resource is output_slot.resource:
61
- body_pointer = extend_pointer(
62
- output_slot.pointer, input_slot.resource_field, output_slot.cardinality
63
- )
61
+ if input_slot.resource_field is not None:
62
+ body_pointer = extend_pointer(
63
+ output_slot.pointer, input_slot.resource_field, output_slot.cardinality
64
+ )
65
+ else:
66
+ # No resource field means use the whole resource
67
+ body_pointer = output_slot.pointer
64
68
  link_name = f"{consumer.method.capitalize()}{input_slot.resource.name}"
65
69
  parameters = {}
66
- request_body = {}
70
+ request_body: dict[str, Any] | list = {}
67
71
  # Data is extracted from response body
68
72
  if input_slot.parameter_location == ParameterLocation.BODY:
69
- request_body = {
70
- input_slot.parameter_name: f"$response.body#{body_pointer}",
71
- }
73
+ if isinstance(input_slot.parameter_name, int):
74
+ request_body = [f"$response.body#{body_pointer}"]
75
+ else:
76
+ request_body = {
77
+ input_slot.parameter_name: f"$response.body#{body_pointer}",
78
+ }
72
79
  else:
73
80
  parameters = {
74
81
  f"{input_slot.parameter_location.value}.{input_slot.parameter_name}": f"$response.body#{body_pointer}",
@@ -76,7 +83,10 @@ class DependencyGraph:
76
83
  existing = links.get(link_name)
77
84
  if existing is not None:
78
85
  existing.parameters.update(parameters)
79
- existing.request_body.update(request_body)
86
+ if isinstance(existing.request_body, dict) and isinstance(request_body, dict):
87
+ existing.request_body.update(request_body)
88
+ else:
89
+ existing.request_body = request_body
80
90
  continue
81
91
  links[link_name] = LinkDefinition(
82
92
  operation_ref=f"#/paths/{consumer_path}/{consumer.method}",
@@ -110,7 +120,9 @@ class DependencyGraph:
110
120
  continue
111
121
  resource = self.resources[input.resource.name]
112
122
  if (
113
- input.resource_field not in resource.fields and resource.name not in known_mismatches
123
+ input.resource_field not in resource.fields
124
+ and resource.name not in known_mismatches
125
+ and input.resource_field is not None
114
126
  ): # pragma: no cover
115
127
  message = (
116
128
  f"Operation '{operation.method.upper()} {operation.path}': "
@@ -120,7 +132,7 @@ class DependencyGraph:
120
132
  matches = difflib.get_close_matches(input.resource_field, resource.fields, n=1, cutoff=0.6)
121
133
  if matches:
122
134
  message += f". Closest field - `{matches[0]}`"
123
- elif resource.fields:
135
+ if resource.fields:
124
136
  message += f". Available fields - {', '.join(resource.fields)}"
125
137
  else:
126
138
  message += ". Resource has no fields"
@@ -151,7 +163,7 @@ class LinkDefinition:
151
163
  parameters: dict[str, str]
152
164
  """Parameter mappings (e.g., {'path.id': '$response.body#/id'})"""
153
165
 
154
- request_body: dict[str, str]
166
+ request_body: dict[str, str] | list
155
167
  """Request body (e.g., {'path.id': '$response.body#/id'})"""
156
168
 
157
169
  __slots__ = ("operation_ref", "parameters", "request_body")
@@ -240,10 +252,12 @@ class InputSlot:
240
252
 
241
253
  # Which resource is needed
242
254
  resource: ResourceDefinition
243
- # Which field from that resource (e.g., "id")
244
- resource_field: str
255
+ # Which field from that resource (e.g., "id").
256
+ # None if passing the whole resource
257
+ resource_field: str | None
245
258
  # Where it goes in the request (e.g., "userId")
246
- parameter_name: str
259
+ # Integer means index in an array (only single items are supported)
260
+ parameter_name: str | int
247
261
  parameter_location: ParameterLocation
248
262
 
249
263
  __slots__ = ("resource", "resource_field", "parameter_name", "parameter_location")
@@ -284,8 +298,9 @@ class ResourceDefinition:
284
298
  return cls(name=name, fields=[], types={}, source=DefinitionSource.SCHEMA_WITHOUT_PROPERTIES)
285
299
 
286
300
  @classmethod
287
- def inferred_from_parameter(cls, name: str, parameter_name: str) -> ResourceDefinition:
288
- return cls(name=name, fields=[parameter_name], types={}, source=DefinitionSource.PARAMETER_INFERENCE)
301
+ def inferred_from_parameter(cls, name: str, parameter_name: str | None) -> ResourceDefinition:
302
+ fields = [parameter_name] if parameter_name is not None else []
303
+ return cls(name=name, fields=fields, types={}, source=DefinitionSource.PARAMETER_INFERENCE)
289
304
 
290
305
 
291
306
  class DefinitionSource(enum.IntEnum):
@@ -93,7 +93,7 @@ def canonicalize(schema: dict[str, Any], resolver: RefResolver) -> Mapping[str,
93
93
 
94
94
  # Canonicalisation in `hypothesis_jsonschema` requires all references to be resovable and non-recursive
95
95
  # On the Schemathesis side bundling solves this problem
96
- bundled = bundle(schema, resolver, inline_recursive=True)
96
+ bundled = bundle(schema, resolver, inline_recursive=True).schema
97
97
  canonicalized = canonicalish(bundled)
98
98
  resolved = resolve_all_refs(canonicalized)
99
99
  resolved.pop(BUNDLE_STORAGE_KEY, None)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schemathesis
3
- Version: 4.3.7
3
+ Version: 4.3.9
4
4
  Summary: Property-based testing framework for Open API and GraphQL based apps
5
5
  Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
6
6
  Project-URL: Changelog, https://github.com/schemathesis/schemathesis/blob/master/CHANGELOG.md
@@ -69,7 +69,7 @@ schemathesis/core/transport.py,sha256=LQcamAkFqJ0HuXQzepevAq2MCJW-uq5Nm-HE9yc7HM
69
69
  schemathesis/core/validation.py,sha256=b0USkKzkWvdz3jOW1JXYc_TfYshfKZeP7xAUnMqcNoc,2303
70
70
  schemathesis/core/version.py,sha256=dOBUWrY3-uA2NQXJp9z7EtZgkR6jYeLg8sMhQCL1mcI,205
71
71
  schemathesis/core/jsonschema/__init__.py,sha256=gBZGsXIpK2EFfcp8x0b69dqzWAm2OeZHepKImkkLvoE,320
72
- schemathesis/core/jsonschema/bundler.py,sha256=rHaNAVgBn0XvAk3t9dHsyym1xK4FyBW7zR1GLejPD0A,8204
72
+ schemathesis/core/jsonschema/bundler.py,sha256=a8cQMJ7EQe3eWvS3haS2ZpxOUwzA4PY4OLI0Uyx045A,8472
73
73
  schemathesis/core/jsonschema/keywords.py,sha256=pjseXTfH9OItNs_Qq6ubkhNWQOrxTnwHmrP_jxrHeJU,631
74
74
  schemathesis/core/jsonschema/references.py,sha256=c2Q4IKWUbwENNtkbFaqf8r3LLZu6GFE5YLnYQlg5tPg,6069
75
75
  schemathesis/core/jsonschema/types.py,sha256=C7f9g8yKFuoxC5_0YNIh8QAyGU0-tj8pzTMfMDjjjVM,1248
@@ -93,13 +93,13 @@ schemathesis/engine/phases/unit/_executor.py,sha256=YDibV3lkC2UMHLvh1FSmnlaQ-SJS
93
93
  schemathesis/engine/phases/unit/_pool.py,sha256=iU0hdHDmohPnEv7_S1emcabuzbTf-Cznqwn0pGQ5wNQ,2480
94
94
  schemathesis/generation/__init__.py,sha256=tvNO2FLiY8z3fZ_kL_QJhSgzXfnT4UqwSXMHCwfLI0g,645
95
95
  schemathesis/generation/case.py,sha256=SLMw6zkzmeiZdaIij8_0tjTF70BrMlRSWREaqWii0uM,12508
96
- schemathesis/generation/coverage.py,sha256=dZYX0gkHDHDenrubVQ58P3ww2xf91fEqk9s54AIokw4,59699
96
+ schemathesis/generation/coverage.py,sha256=3eU3HaHr_FCodATskfQ1k4_HHR3z_Pf0Gk_IcVGxLyU,60415
97
97
  schemathesis/generation/meta.py,sha256=tXhUZBEdpQMn68uMx1SW8Vv59Uf6Wl6yzs-VB9lu_8o,2589
98
98
  schemathesis/generation/metrics.py,sha256=cZU5HdeAMcLFEDnTbNE56NuNq4P0N4ew-g1NEz5-kt4,2836
99
99
  schemathesis/generation/modes.py,sha256=Q1fhjWr3zxabU5qdtLvKfpMFZJAwlW9pnxgenjeXTyU,481
100
100
  schemathesis/generation/overrides.py,sha256=xI2djHsa42fzP32xpxgxO52INixKagf5DjDAWJYswM8,3890
101
101
  schemathesis/generation/hypothesis/__init__.py,sha256=68BHULoXQC1WjFfw03ga5lvDGZ-c-J7H_fNEuUzFWRw,4976
102
- schemathesis/generation/hypothesis/builder.py,sha256=j7R_X9Z_50xjwIl_Z8DVBe6P1r8leVNvQME7mRLXM1A,38520
102
+ schemathesis/generation/hypothesis/builder.py,sha256=wOCjNAScpiN-wJ4EM0QnJ_5o9nczW0WILFNfIR1qeKQ,38480
103
103
  schemathesis/generation/hypothesis/examples.py,sha256=6eGaKUEC3elmKsaqfKj1sLvM8EHc-PWT4NRBq4NI0Rs,1409
104
104
  schemathesis/generation/hypothesis/given.py,sha256=sTZR1of6XaHAPWtHx2_WLlZ50M8D5Rjux0GmWkWjDq4,2337
105
105
  schemathesis/generation/hypothesis/reporting.py,sha256=uDVow6Ya8YFkqQuOqRsjbzsbyP4KKfr3jA7ZaY4FuKY,279
@@ -130,7 +130,7 @@ schemathesis/specs/graphql/schemas.py,sha256=GKJcnTAT1wUzzUr3r6wiTfiAdFLcgFQjYRR
130
130
  schemathesis/specs/graphql/validation.py,sha256=-W1Noc1MQmTb4RX-gNXMeU2qkgso4mzVfHxtdLkCPKM,1422
131
131
  schemathesis/specs/openapi/__init__.py,sha256=C5HOsfuDJGq_3mv8CRBvRvb0Diy1p0BFdqyEXMS-loE,238
132
132
  schemathesis/specs/openapi/_hypothesis.py,sha256=O8vN-koBjzBVZfpD3pmgIt6ecU4ddAPHOxTAORd23Lo,22642
133
- schemathesis/specs/openapi/checks.py,sha256=YYV6j6idyw2ubY4sLp-avs2OVEkAWeIihjT0xiV1RRA,30669
133
+ schemathesis/specs/openapi/checks.py,sha256=12ks0V2F8-YKPkItgAc0ZrxsHufWWlsgj-jpj-cF40A,31578
134
134
  schemathesis/specs/openapi/converter.py,sha256=4a6-8STT5snF7B-t6IsOIGdK5rV16oNqsdvWL7VFf2M,6472
135
135
  schemathesis/specs/openapi/definitions.py,sha256=8htclglV3fW6JPBqs59lgM4LnA25Mm9IptXBPb_qUT0,93949
136
136
  schemathesis/specs/openapi/examples.py,sha256=moFFfOfzepjlJOrqLc60BrEmJ4oRzwJ3SM03y_nJNMU,24097
@@ -138,14 +138,14 @@ schemathesis/specs/openapi/formats.py,sha256=4tYRdckauHxkJCmOhmdwDq_eOpHPaKloi89
138
138
  schemathesis/specs/openapi/media_types.py,sha256=F5M6TKl0s6Z5X8mZpPsWDEdPBvxclKRcUOc41eEwKbo,2472
139
139
  schemathesis/specs/openapi/patterns.py,sha256=GqPZEXMRdWENQxanWjBOalIZ2MQUjuxk21kmdiI703E,18027
140
140
  schemathesis/specs/openapi/references.py,sha256=AW1laU23BkiRf0EEFM538vyVFLXycGUiucGVV461le0,1927
141
- schemathesis/specs/openapi/schemas.py,sha256=vwBgw7kznPlQJIufgJ48oRxCzDFkUWKfb4uK7rMZUT4,33704
141
+ schemathesis/specs/openapi/schemas.py,sha256=uTxwggtWi5dWjuKlSDs0DthlNNfcRvSJtrND-PaTkrg,33758
142
142
  schemathesis/specs/openapi/serialization.py,sha256=RPNdadne5wdhsGmjSvgKLRF58wpzpRx3wura8PsHM3o,12152
143
143
  schemathesis/specs/openapi/utils.py,sha256=XkOJT8qD-6uhq-Tmwxk_xYku1Gy5F9pKL3ldNg_DRZw,522
144
144
  schemathesis/specs/openapi/adapter/__init__.py,sha256=YEovBgLjnXd3WGPMJXq0KbSGHezkRlEv4dNRO7_evfk,249
145
- schemathesis/specs/openapi/adapter/parameters.py,sha256=MYEQUxhtv23e1uhoDq5bDHMUT3Q64bW7aZloJuz13QY,18626
145
+ schemathesis/specs/openapi/adapter/parameters.py,sha256=bVo7sgN5oCH2GVDXlqAMrwN0z5pb8VKf3_FOkSgTGDA,19567
146
146
  schemathesis/specs/openapi/adapter/protocol.py,sha256=VDF6COcilHEUnmw76YBVur8bFiTFQHsNvaO9pR_i_KM,2709
147
147
  schemathesis/specs/openapi/adapter/references.py,sha256=6M59pJy_U_sLh3Xzgu6-izWXtz3bjXnqJYSD65wRHtk,549
148
- schemathesis/specs/openapi/adapter/responses.py,sha256=0iC0i6AF3VMVlTiKxBNLFHG_W22-5K731fVMdhUk9Kk,13269
148
+ schemathesis/specs/openapi/adapter/responses.py,sha256=UXcYb048SeS0MhydQY518IgYD0s0Q5YpLsBbdX5-5-s,13276
149
149
  schemathesis/specs/openapi/adapter/security.py,sha256=W3cqlbs80NxF9SAavOi7BhtNGzdxHO476lYxiWN0D08,4945
150
150
  schemathesis/specs/openapi/adapter/v2.py,sha256=2Rd1cTv7_I5QrBPLVfa2yD80NAErxV3tdeACjtEfXAA,1280
151
151
  schemathesis/specs/openapi/adapter/v3_0.py,sha256=8bOE9WUDrvPivGs0w-S1PP2TXgWuaoTzMdg2_WWbi-E,1272
@@ -165,12 +165,12 @@ schemathesis/specs/openapi/stateful/control.py,sha256=QaXLSbwQWtai5lxvvVtQV3BLJ8
165
165
  schemathesis/specs/openapi/stateful/inference.py,sha256=B99jSTDVi2yKxU7-raIb91xpacOrr0nZkEZY5Ej3eCY,9783
166
166
  schemathesis/specs/openapi/stateful/links.py,sha256=SSA66mU50FFBz7e6sA37CfL-Vt0OY3gont72oFSvZYU,8163
167
167
  schemathesis/specs/openapi/stateful/dependencies/__init__.py,sha256=0JM-FrY6Awv6gl-qDHaaK7pXbt_GKutBKPyIaph8apA,7842
168
- schemathesis/specs/openapi/stateful/dependencies/inputs.py,sha256=1qVVIlzx52qsy55Pht9dYNtn2dewRSiHegfrBO1RD8c,10347
169
- schemathesis/specs/openapi/stateful/dependencies/models.py,sha256=P1GsX0P8YQboQJDG7DXEksJU279Vt_wYqpHb6uk1Hkg,11280
168
+ schemathesis/specs/openapi/stateful/dependencies/inputs.py,sha256=PSactImp4OqsYMHUl2gB2pgvUlZCCKJRJKeaalclFzU,11511
169
+ schemathesis/specs/openapi/stateful/dependencies/models.py,sha256=Kl482Hwq2M8lYAdqGmf_8Yje3voSj1WLDUIujRUDWDQ,12286
170
170
  schemathesis/specs/openapi/stateful/dependencies/naming.py,sha256=HfpkCB1GglX1BAKXer3llvPkQsk8wx0QZhZq7ANcdMM,12214
171
171
  schemathesis/specs/openapi/stateful/dependencies/outputs.py,sha256=zvVUfQWNIuhMkKDpz5hsVGkkvkefLt1EswpJAnHajOw,1186
172
172
  schemathesis/specs/openapi/stateful/dependencies/resources.py,sha256=p58XoADpMKFAun0Bx_rul-kiUlfA9PXjxHJ97dT2tBE,11202
173
- schemathesis/specs/openapi/stateful/dependencies/schemas.py,sha256=yMu13RsXIPDeZT1tATTxI1vkpYhjs-XFSFEvx3_Xh_Q,14094
173
+ schemathesis/specs/openapi/stateful/dependencies/schemas.py,sha256=snDmf2SWsvo9Oqk_X8xF8GB0Fbwx429UA6ZL2IokzDY,14101
174
174
  schemathesis/specs/openapi/types/__init__.py,sha256=VPsWtLJle__Kodw_QqtQ3OuvBzBcCIKsTOrXy3eA7OU,66
175
175
  schemathesis/specs/openapi/types/v3.py,sha256=Vondr9Amk6JKCIM6i6RGcmTUjFfPgOOqzBXqerccLpo,1468
176
176
  schemathesis/transport/__init__.py,sha256=6yg_RfV_9L0cpA6qpbH-SL9_3ggtHQji9CZrpIkbA6s,5321
@@ -179,8 +179,8 @@ schemathesis/transport/prepare.py,sha256=erYXRaxpQokIDzaIuvt_csHcw72iHfCyNq8VNEz
179
179
  schemathesis/transport/requests.py,sha256=wriRI9fprTplE_qEZLEz1TerX6GwkE3pwr6ZnU2o6vQ,10648
180
180
  schemathesis/transport/serialization.py,sha256=GwO6OAVTmL1JyKw7HiZ256tjV4CbrRbhQN0ep1uaZwI,11157
181
181
  schemathesis/transport/wsgi.py,sha256=kQtasFre6pjdJWRKwLA_Qb-RyQHCFNpaey9ubzlFWKI,5907
182
- schemathesis-4.3.7.dist-info/METADATA,sha256=hxCdew3yx_fJyUv0iunB3526jFD0HhlSnKCv9DCXV_o,8540
183
- schemathesis-4.3.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
184
- schemathesis-4.3.7.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
185
- schemathesis-4.3.7.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
186
- schemathesis-4.3.7.dist-info/RECORD,,
182
+ schemathesis-4.3.9.dist-info/METADATA,sha256=xqdweDGbn3i_jr_ZEgDMgh0vU8alhih5bINIKkPW0fU,8540
183
+ schemathesis-4.3.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
184
+ schemathesis-4.3.9.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
185
+ schemathesis-4.3.9.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
186
+ schemathesis-4.3.9.dist-info/RECORD,,