schemathesis 3.28.1__py3-none-any.whl → 3.29.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.
@@ -15,9 +15,12 @@ from typing import (
15
15
  ClassVar,
16
16
  Generator,
17
17
  Iterable,
18
+ Iterator,
19
+ Mapping,
18
20
  NoReturn,
19
21
  Sequence,
20
22
  TypeVar,
23
+ cast,
21
24
  )
22
25
  from urllib.parse import urlsplit
23
26
 
@@ -28,33 +31,34 @@ from requests.structures import CaseInsensitiveDict
28
31
 
29
32
  from ... import experimental, failures
30
33
  from ..._compat import MultipleFailures
31
- from ..._override import CaseOverride, set_override_mark, check_no_override_mark
34
+ from ..._override import CaseOverride, check_no_override_mark, set_override_mark
32
35
  from ...auths import AuthStorage
33
- from ...generation import DataGenerationMethod, GenerationConfig
34
36
  from ...constants import HTTP_METHODS, NOT_SET
35
37
  from ...exceptions import (
36
38
  InternalError,
39
+ OperationNotFound,
37
40
  OperationSchemaError,
41
+ SchemaError,
42
+ SchemaErrorType,
38
43
  UsageError,
39
44
  get_missing_content_type_error,
40
45
  get_response_parsing_error,
41
46
  get_schema_validation_error,
42
- SchemaError,
43
- SchemaErrorType,
44
- OperationNotFound,
45
47
  )
48
+ from ...generation import DataGenerationMethod, GenerationConfig
46
49
  from ...hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, should_skip_operation
47
50
  from ...internal.copy import fast_deepcopy
48
51
  from ...internal.jsonschema import traverse_schema
49
52
  from ...internal.result import Err, Ok, Result
50
53
  from ...models import APIOperation, Case, OperationDefinition
51
- from ...schemas import BaseSchema, APIOperationMap
54
+ from ...schemas import APIOperationMap, BaseSchema
52
55
  from ...stateful import Stateful, StatefulTest
53
56
  from ...stateful.state_machine import APIStateMachine
54
57
  from ...transports.content_types import is_json_media_type, parse_content_type
55
58
  from ...transports.responses import get_json
56
- from ...types import Body, Cookies, FormData, Headers, NotSet, PathParameters, Query, GenericTest
59
+ from ...types import Body, Cookies, FormData, GenericTest, Headers, NotSet, PathParameters, Query
57
60
  from . import links, serialization
61
+ from ._cache import OperationCache
58
62
  from ._hypothesis import get_case_strategy
59
63
  from .converter import to_json_schema, to_json_schema_recursive
60
64
  from .definitions import OPENAPI_30_VALIDATOR, OPENAPI_31_VALIDATOR, SWAGGER_20_VALIDATOR
@@ -74,7 +78,7 @@ from .parameters import (
74
78
  OpenAPI30Parameter,
75
79
  OpenAPIParameter,
76
80
  )
77
- from .references import RECURSION_DEPTH_LIMIT, ConvertingResolver, InliningResolver, resolve_pointer, UNRESOLVABLE
81
+ from .references import RECURSION_DEPTH_LIMIT, UNRESOLVABLE, ConvertingResolver, InliningResolver, resolve_pointer
78
82
  from .security import BaseSecurityProcessor, OpenAPISecurityProcessor, SwaggerSecurityProcessor
79
83
  from .stateful import create_state_machine
80
84
 
@@ -91,7 +95,7 @@ class BaseOpenAPISchema(BaseSchema):
91
95
  links_field: ClassVar[str] = ""
92
96
  header_required_field: ClassVar[str] = ""
93
97
  security: ClassVar[BaseSecurityProcessor] = None # type: ignore
94
- _operations_by_id: dict[str, APIOperation] = field(init=False)
98
+ _operation_cache: OperationCache = field(default_factory=OperationCache)
95
99
  _inline_reference_cache: dict[str, Any] = field(default_factory=dict)
96
100
  # Inline references cache can be populated from multiple threads, therefore we need some synchronisation to avoid
97
101
  # excessive resolving
@@ -114,13 +118,24 @@ class BaseOpenAPISchema(BaseSchema):
114
118
  info = self.raw_schema["info"]
115
119
  return f"<{self.__class__.__name__} for {info['title']} {info['version']}>"
116
120
 
117
- def _store_operations(
118
- self, operations: Generator[Result[APIOperation, OperationSchemaError], None, None]
119
- ) -> dict[str, APIOperationMap]:
120
- return operations_to_dict(operations)
121
+ def __iter__(self) -> Iterator[str]:
122
+ return iter(self.raw_schema.get("paths", {}))
123
+
124
+ def _get_operation_map(self, path: str) -> APIOperationMap:
125
+ cache = self._operation_cache
126
+ map = cache.get_map(path)
127
+ if map is not None:
128
+ return map
129
+ path_item = self.raw_schema.get("paths", {})[path]
130
+ scope, path_item = self._resolve_path_item(path_item)
131
+ self.dispatch_hook("before_process_path", HookContext(), path, path_item)
132
+ map = APIOperationMap(self, {})
133
+ map._data = MethodMap(map, scope, path, CaseInsensitiveDict(path_item))
134
+ cache.insert_map(path, map)
135
+ return map
121
136
 
122
137
  def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
123
- matches = get_close_matches(item, list(self.operations))
138
+ matches = get_close_matches(item, list(self))
124
139
  self._on_missing_operation(item, exc, matches)
125
140
 
126
141
  def _on_missing_operation(self, item: str, exc: KeyError, matches: list[str]) -> NoReturn:
@@ -143,19 +158,20 @@ class BaseOpenAPISchema(BaseSchema):
143
158
  paths = self.raw_schema["paths"]
144
159
  except KeyError:
145
160
  return
161
+ get_full_path = self.get_full_path
162
+ endpoint = self.endpoint
146
163
  resolve = self.resolver.resolve
147
- for path, methods in paths.items():
148
- full_path = self.get_full_path(path)
149
- if should_skip_endpoint(full_path, self.endpoint):
164
+ should_skip = self._should_skip
165
+ for path, path_item in paths.items():
166
+ full_path = get_full_path(path)
167
+ if should_skip_endpoint(full_path, endpoint):
150
168
  continue
151
169
  try:
152
- if "$ref" in methods:
153
- _, resolved_methods = resolve(methods["$ref"])
154
- else:
155
- resolved_methods = methods
170
+ if "$ref" in path_item:
171
+ _, path_item = resolve(path_item["$ref"])
156
172
  # Straightforward iteration is faster than converting to a set & calculating length.
157
- for method, definition in resolved_methods.items():
158
- if self._should_skip(method, definition):
173
+ for method, definition in path_item.items():
174
+ if should_skip(method, definition):
159
175
  continue
160
176
  yield definition
161
177
  except SCHEMA_PARSING_ERRORS:
@@ -173,11 +189,13 @@ class BaseOpenAPISchema(BaseSchema):
173
189
  @property
174
190
  def links_count(self) -> int:
175
191
  total = 0
192
+ resolve = self.resolver.resolve
193
+ links_field = self.links_field
176
194
  for definition in self._operation_iter():
177
195
  for response in definition.get("responses", {}).values():
178
196
  if "$ref" in response:
179
- _, response = self.resolver.resolve(response["$ref"])
180
- defined_links = response.get(self.links_field)
197
+ _, response = resolve(response["$ref"])
198
+ defined_links = response.get(links_field)
181
199
  if defined_links is not None:
182
200
  total += len(defined_links)
183
201
  return total
@@ -202,6 +220,24 @@ class BaseOpenAPISchema(BaseSchema):
202
220
 
203
221
  return _add_override
204
222
 
223
+ def _resolve_until_no_references(self, value: dict[str, Any]) -> dict[str, Any]:
224
+ while "$ref" in value:
225
+ _, value = self.resolver.resolve(value["$ref"])
226
+ return value
227
+
228
+ def _resolve_shared_parameters(self, path_item: Mapping[str, Any]) -> list[dict[str, Any]]:
229
+ return self.resolver.resolve_all(path_item.get("parameters", []), RECURSION_DEPTH_LIMIT - 8)
230
+
231
+ def _resolve_operation(self, operation: dict[str, Any]) -> dict[str, Any]:
232
+ return self.resolver.resolve_all(operation, RECURSION_DEPTH_LIMIT - 8)
233
+
234
+ def _collect_operation_parameters(
235
+ self, path_item: Mapping[str, Any], operation: dict[str, Any]
236
+ ) -> list[OpenAPIParameter]:
237
+ shared_parameters = self._resolve_shared_parameters(path_item)
238
+ parameters = operation.get("parameters", ())
239
+ return self.collect_parameters(itertools.chain(parameters, shared_parameters), operation)
240
+
205
241
  def get_all_operations(
206
242
  self, hooks: HookDispatcher | None = None
207
243
  ) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
@@ -231,41 +267,40 @@ class BaseOpenAPISchema(BaseSchema):
231
267
  self._raise_invalid_schema(exc)
232
268
 
233
269
  context = HookContext()
234
- for path, methods in paths.items():
270
+ # Optimization: local variables are faster than attribute access
271
+ get_full_path = self.get_full_path
272
+ endpoint = self.endpoint
273
+ dispatch_hook = self.dispatch_hook
274
+ resolve_path_item = self._resolve_path_item
275
+ resolve_shared_parameters = self._resolve_shared_parameters
276
+ resolve_operation = self._resolve_operation
277
+ should_skip = self._should_skip
278
+ collect_parameters = self.collect_parameters
279
+ make_operation = self.make_operation
280
+ hooks = self.hooks
281
+ for path, path_item in paths.items():
235
282
  method = None
236
283
  try:
237
- full_path = self.get_full_path(path) # Should be available for later use
238
- if should_skip_endpoint(full_path, self.endpoint):
284
+ full_path = get_full_path(path) # Should be available for later use
285
+ if should_skip_endpoint(full_path, endpoint):
239
286
  continue
240
- self.dispatch_hook("before_process_path", context, path, methods)
241
- scope, raw_methods = self._resolve_methods(methods)
242
- common_parameters = self.resolver.resolve_all(methods.get("parameters", []), RECURSION_DEPTH_LIMIT - 8)
243
- for method, definition in raw_methods.items():
287
+ dispatch_hook("before_process_path", context, path, path_item)
288
+ scope, path_item = resolve_path_item(path_item)
289
+ shared_parameters = resolve_shared_parameters(path_item)
290
+ for method, entry in path_item.items():
291
+ if method not in HTTP_METHODS:
292
+ continue
244
293
  try:
245
- # Setting a low recursion limit doesn't solve the problem with recursive references & inlining
246
- # too much but decreases the number of cases when Schemathesis stuck on this step.
247
- self.resolver.push_scope(scope)
248
- try:
249
- resolved_definition = self.resolver.resolve_all(definition, RECURSION_DEPTH_LIMIT - 8)
250
- finally:
251
- self.resolver.pop_scope()
252
- # Only method definitions are parsed
253
- if self._should_skip(method, resolved_definition):
294
+ resolved = resolve_operation(entry)
295
+ if should_skip(method, resolved):
254
296
  continue
255
- parameters = self.collect_parameters(
256
- itertools.chain(resolved_definition.get("parameters", ()), common_parameters),
257
- resolved_definition,
258
- )
259
- # To prevent recursion errors we need to pass not resolved schema as well
260
- # It could be used for response validation
261
- raw_definition = OperationDefinition(
262
- raw_methods[method], resolved_definition, scope, parameters
263
- )
264
- operation = self.make_operation(path, method, parameters, raw_definition)
297
+ parameters = resolved.get("parameters", ())
298
+ parameters = collect_parameters(itertools.chain(parameters, shared_parameters), resolved)
299
+ operation = make_operation(path, method, parameters, entry, resolved, scope)
265
300
  context = HookContext(operation=operation)
266
301
  if (
267
302
  should_skip_operation(GLOBAL_HOOK_DISPATCHER, context)
268
- or should_skip_operation(self.hooks, context)
303
+ or should_skip_operation(hooks, context)
269
304
  or (hooks and should_skip_operation(hooks, context))
270
305
  ):
271
306
  continue
@@ -320,20 +355,22 @@ class BaseOpenAPISchema(BaseSchema):
320
355
  """
321
356
  raise NotImplementedError
322
357
 
323
- def _resolve_methods(self, methods: dict[str, Any]) -> tuple[str, dict[str, Any]]:
324
- # We need to know a proper scope in what methods are.
325
- # It will allow us to provide a proper reference resolving in `response_schema_conformance` and avoid
326
- # recursion errors
358
+ def _resolve_path_item(self, methods: dict[str, Any]) -> tuple[str, dict[str, Any]]:
359
+ # The path item could be behind a reference
360
+ # In this case, we need to resolve it to get the proper scope for reference inside the item.
361
+ # It is mostly for validating responses.
327
362
  if "$ref" in methods:
328
- return fast_deepcopy(self.resolver.resolve(methods["$ref"]))
329
- return self.resolver.resolution_scope, fast_deepcopy(methods)
363
+ return self.resolver.resolve(methods["$ref"])
364
+ return self.resolver.resolution_scope, methods
330
365
 
331
366
  def make_operation(
332
367
  self,
333
368
  path: str,
334
369
  method: str,
335
370
  parameters: list[OpenAPIParameter],
336
- raw_definition: OperationDefinition,
371
+ raw: dict[str, Any],
372
+ resolved: dict[str, Any],
373
+ scope: str,
337
374
  ) -> APIOperation:
338
375
  """Create JSON schemas for the query, body, etc from Swagger parameters definitions."""
339
376
  __tracebackhide__ = True
@@ -341,7 +378,7 @@ class BaseOpenAPISchema(BaseSchema):
341
378
  operation: APIOperation[OpenAPIParameter, Case] = APIOperation(
342
379
  path=path,
343
380
  method=method,
344
- definition=raw_definition,
381
+ definition=OperationDefinition(raw, resolved, scope),
345
382
  base_url=base_url,
346
383
  app=self.app,
347
384
  schema=self,
@@ -376,49 +413,77 @@ class BaseOpenAPISchema(BaseSchema):
376
413
 
377
414
  def get_operation_by_id(self, operation_id: str) -> APIOperation:
378
415
  """Get an `APIOperation` instance by its `operationId`."""
379
- if not hasattr(self, "_operations_by_id"):
380
- self._operations_by_id = dict(self._group_operations_by_id())
416
+ cache = self._operation_cache
417
+ cached = cache.get_operation_by_id(operation_id)
418
+ if cached is not None:
419
+ return cached
420
+ # Operation has not been accessed yet, need to populate the cache
421
+ if not cache.has_ids_to_definitions:
422
+ self._populate_operation_id_cache(cache)
381
423
  try:
382
- return self._operations_by_id[operation_id]
424
+ entry = cache.get_definition_by_id(operation_id)
383
425
  except KeyError as exc:
384
- matches = get_close_matches(operation_id, list(self._operations_by_id))
426
+ matches = get_close_matches(operation_id, cache.known_operation_ids)
385
427
  self._on_missing_operation(operation_id, exc, matches)
386
-
387
- def _group_operations_by_id(self) -> Generator[tuple[str, APIOperation], None, None]:
388
- for path, methods in self.raw_schema["paths"].items():
389
- scope, raw_methods = self._resolve_methods(methods)
390
- common_parameters = self.resolver.resolve_all(methods.get("parameters", []), RECURSION_DEPTH_LIMIT - 8)
391
- for method, definition in methods.items():
392
- if method not in HTTP_METHODS or "operationId" not in definition:
428
+ # It could've been already accessed in a different place
429
+ traversal_key = (entry.scope, entry.path, entry.method)
430
+ instance = cache.get_operation_by_traversal_key(traversal_key)
431
+ if instance is not None:
432
+ return instance
433
+ resolved = self._resolve_operation(entry.operation)
434
+ parameters = self._collect_operation_parameters(entry.path_item, resolved)
435
+ initialized = self.make_operation(entry.path, entry.method, parameters, entry.operation, resolved, entry.scope)
436
+ cache.insert_operation(initialized, traversal_key=traversal_key, operation_id=operation_id)
437
+ return initialized
438
+
439
+ def _populate_operation_id_cache(self, cache: OperationCache) -> None:
440
+ """Collect all operation IDs from the schema."""
441
+ resolve = self.resolver.resolve
442
+ default_scope = self.resolver.resolution_scope
443
+ for path, path_item in self.raw_schema.get("paths", {}).items():
444
+ # If the path is behind a reference we have to keep the scope
445
+ # The scope is used to resolve nested components later on
446
+ if "$ref" in path_item:
447
+ scope, path_item = resolve(path_item["$ref"])
448
+ else:
449
+ scope = default_scope
450
+ for key, entry in path_item.items():
451
+ if key not in HTTP_METHODS:
393
452
  continue
394
- self.resolver.push_scope(scope)
395
- try:
396
- resolved_definition = self.resolver.resolve_all(definition, RECURSION_DEPTH_LIMIT - 8)
397
- finally:
398
- self.resolver.pop_scope()
399
- parameters = self.collect_parameters(
400
- itertools.chain(resolved_definition.get("parameters", ()), common_parameters), resolved_definition
401
- )
402
- raw_definition = OperationDefinition(raw_methods[method], resolved_definition, scope, parameters)
403
- yield resolved_definition["operationId"], self.make_operation(path, method, parameters, raw_definition)
453
+ if "operationId" in entry:
454
+ cache.insert_definition_by_id(
455
+ entry["operationId"],
456
+ path=path,
457
+ method=key,
458
+ scope=scope,
459
+ path_item=path_item,
460
+ operation=entry,
461
+ )
404
462
 
405
463
  def get_operation_by_reference(self, reference: str) -> APIOperation:
406
464
  """Get local or external `APIOperation` instance by reference.
407
465
 
408
466
  Reference example: #/paths/~1users~1{user_id}/patch
409
467
  """
410
- scope, data = self.resolver.resolve(reference)
468
+ cache = self._operation_cache
469
+ cached = cache.get_operation_by_reference(reference)
470
+ if cached is not None:
471
+ return cached
472
+ scope, operation = self.resolver.resolve(reference)
411
473
  path, method = scope.rsplit("/", maxsplit=2)[-2:]
412
474
  path = path.replace("~1", "/").replace("~0", "~")
413
- resolved_definition = self.resolver.resolve_all(data)
475
+ # Check the traversal cache as it could've been populated in other places
476
+ traversal_key = (self.resolver.resolution_scope, path, method)
477
+ cached = cache.get_operation_by_traversal_key(traversal_key)
478
+ if cached is not None:
479
+ return cached
480
+ resolved = self._resolve_operation(operation)
414
481
  parent_ref, _ = reference.rsplit("/", maxsplit=1)
415
- _, methods = self.resolver.resolve(parent_ref)
416
- common_parameters = self.resolver.resolve_all(methods.get("parameters", []), RECURSION_DEPTH_LIMIT - 8)
417
- parameters = self.collect_parameters(
418
- itertools.chain(resolved_definition.get("parameters", ()), common_parameters), resolved_definition
419
- )
420
- raw_definition = OperationDefinition(data, resolved_definition, scope, parameters)
421
- return self.make_operation(path, method, parameters, raw_definition)
482
+ _, path_item = self.resolver.resolve(parent_ref)
483
+ parameters = self._collect_operation_parameters(path_item, resolved)
484
+ initialized = self.make_operation(path, method, parameters, operation, resolved, scope)
485
+ cache.insert_operation(initialized, traversal_key=traversal_key, reference=reference)
486
+ return initialized
422
487
 
423
488
  def get_case_strategy(
424
489
  self,
@@ -439,7 +504,7 @@ class BaseOpenAPISchema(BaseSchema):
439
504
  )
440
505
 
441
506
  def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
442
- definitions = [item for item in operation.definition.resolved.get("parameters", []) if item["in"] == location]
507
+ definitions = [item.definition for item in operation.iter_parameters() if item.location == location]
443
508
  security_parameters = self.security.get_security_definitions_as_parameters(
444
509
  self.raw_schema, operation, self.resolver, location
445
510
  )
@@ -455,7 +520,7 @@ class BaseOpenAPISchema(BaseSchema):
455
520
 
456
521
  def _get_response_definitions(self, operation: APIOperation, response: GenericResponse) -> dict[str, Any] | None:
457
522
  try:
458
- responses = operation.definition.resolved["responses"]
523
+ responses = operation.definition.raw["responses"]
459
524
  except KeyError as exc:
460
525
  # Possible to get if `validate_schema=False` is passed during schema creation
461
526
  path = operation.path
@@ -463,9 +528,11 @@ class BaseOpenAPISchema(BaseSchema):
463
528
  self._raise_invalid_schema(exc, full_path, path, operation.method)
464
529
  status_code = str(response.status_code)
465
530
  if status_code in responses:
466
- return responses[status_code]
531
+ _, response = self.resolver.resolve_in_scope(responses[status_code], operation.definition.scope)
532
+ return response
467
533
  if "default" in responses:
468
- return responses["default"]
534
+ _, response = self.resolver.resolve_in_scope(responses["default"], operation.definition.scope)
535
+ return response
469
536
  return None
470
537
 
471
538
  def get_headers(self, operation: APIOperation, response: GenericResponse) -> dict[str, dict[str, Any]] | None:
@@ -518,46 +585,16 @@ class BaseOpenAPISchema(BaseSchema):
518
585
  """
519
586
  if parameters is None and request_body is None:
520
587
  raise ValueError("You need to provide `parameters` or `request_body`.")
521
- if hasattr(self, "_operations"):
522
- delattr(self, "_operations")
523
- for operation, methods in self.raw_schema["paths"].items():
524
- if operation == source.path:
525
- # Methods should be completely resolved now, otherwise they might miss a resolving scope when
526
- # they will be fully resolved later
527
- methods = self.resolver.resolve_all(methods, RECURSION_DEPTH_LIMIT - 8)
528
- found = False
529
- for method, definition in methods.items():
530
- if method.upper() == source.method.upper():
531
- found = True
532
- links.add_link(
533
- responses=definition["responses"],
534
- links_field=self.links_field,
535
- parameters=parameters,
536
- request_body=request_body,
537
- status_code=status_code,
538
- target=target,
539
- name=name,
540
- )
541
- # If methods are behind a reference, then on the next resolving they will miss the new link
542
- # Therefore we need to modify it this way
543
- self.raw_schema["paths"][operation][method] = definition
544
- # The reference should be removed completely, otherwise new keys in this dictionary will be ignored
545
- # due to the `$ref` keyword behavior
546
- self.raw_schema["paths"][operation].pop("$ref", None)
547
- if found:
548
- return
549
- name = f"{source.method.upper()} {source.path}"
550
- # Use a name without basePath, as the user doesn't use it.
551
- # E.g. `source=schema["/users/"]["POST"]` without a prefix
552
- message = f"No such API operation: `{name}`."
553
- possibilities = [
554
- f"{op.ok().method.upper()} {op.ok().path}" for op in self.get_all_operations() if isinstance(op, Ok)
555
- ]
556
- matches = get_close_matches(name, possibilities)
557
- if matches:
558
- message += f" Did you mean `{matches[0]}`?"
559
- message += " Check if the requested API operation passes the filters in the schema."
560
- raise ValueError(message)
588
+ links.add_link(
589
+ resolver=self.resolver,
590
+ responses=self[source.path][source.method].definition.raw["responses"],
591
+ links_field=self.links_field,
592
+ parameters=parameters,
593
+ request_body=request_body,
594
+ status_code=status_code,
595
+ target=target,
596
+ name=name,
597
+ )
561
598
 
562
599
  def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
563
600
  result: dict[str, dict[str, Any]] = defaultdict(dict)
@@ -566,7 +603,7 @@ class BaseOpenAPISchema(BaseSchema):
566
603
  return result
567
604
 
568
605
  def get_tags(self, operation: APIOperation) -> list[str] | None:
569
- return operation.definition.resolved.get("tags")
606
+ return operation.definition.raw.get("tags")
570
607
 
571
608
  def validate_response(self, operation: APIOperation, response: GenericResponse) -> bool | None:
572
609
  responses = {str(key): value for key, value in operation.definition.raw.get("responses", {}).items()}
@@ -766,30 +803,58 @@ def in_scopes(resolver: jsonschema.RefResolver, scopes: list[str]) -> Generator[
766
803
  yield
767
804
 
768
805
 
769
- def operations_to_dict(
770
- operations: Generator[Result[APIOperation, OperationSchemaError], None, None],
771
- ) -> dict[str, APIOperationMap]:
772
- output: dict[str, APIOperationMap] = {}
773
- for result in operations:
774
- if isinstance(result, Ok):
775
- operation = result.ok()
776
- output.setdefault(operation.path, APIOperationMap(MethodMap()))
777
- output[operation.path][operation.method] = operation
778
- return output
779
-
780
-
781
- class MethodMap(CaseInsensitiveDict):
806
+ @dataclass
807
+ class MethodMap(Mapping):
782
808
  """Container for accessing API operations.
783
809
 
784
810
  Provides a more specific error message if API operation is not found.
785
811
  """
786
812
 
813
+ _parent: APIOperationMap
814
+ # Reference resolution scope
815
+ _scope: str
816
+ # Methods are stored for this path
817
+ _path: str
818
+ # Storage for definitions
819
+ _path_item: CaseInsensitiveDict
820
+
821
+ __slots__ = ("_parent", "_scope", "_path", "_path_item")
822
+
823
+ def __len__(self) -> int:
824
+ return len(self._path_item)
825
+
826
+ def __iter__(self) -> Iterator[str]:
827
+ return iter(self._path_item)
828
+
829
+ def _init_operation(self, method: str) -> APIOperation:
830
+ method = method.lower()
831
+ operation = self._path_item[method]
832
+ schema = cast(BaseOpenAPISchema, self._parent._schema)
833
+ cache = schema._operation_cache
834
+ path = self._path
835
+ scope = self._scope
836
+ traversal_key = (scope, path, method)
837
+ cached = cache.get_operation_by_traversal_key(traversal_key)
838
+ if cached is not None:
839
+ return cached
840
+ schema.resolver.push_scope(scope)
841
+ try:
842
+ resolved = schema._resolve_operation(operation)
843
+ finally:
844
+ schema.resolver.pop_scope()
845
+ parameters = schema._collect_operation_parameters(self._path_item, resolved)
846
+ initialized = schema.make_operation(path, method, parameters, operation, resolved, scope)
847
+ cache.insert_operation(initialized, traversal_key=traversal_key, operation_id=resolved.get("operationId"))
848
+ return initialized
849
+
787
850
  def __getitem__(self, item: str) -> APIOperation:
788
851
  try:
789
- return super().__getitem__(item)
852
+ return self._init_operation(item)
790
853
  except KeyError as exc:
791
854
  available_methods = ", ".join(map(str.upper, self))
792
- message = f"Method `{item}` not found. Available methods: {available_methods}"
855
+ message = f"Method `{item.upper()}` not found."
856
+ if available_methods:
857
+ message += f" Available methods: {available_methods}"
793
858
  raise KeyError(message) from exc
794
859
 
795
860
 
@@ -864,7 +929,7 @@ class SwaggerV20(BaseOpenAPISchema):
864
929
  return get_strategies_from_examples(operation, self.examples_field)
865
930
 
866
931
  def get_response_schema(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any] | None]:
867
- scopes, definition = self.resolver.resolve_in_scope(fast_deepcopy(definition), scope)
932
+ scopes, definition = self.resolver.resolve_in_scope(definition, scope)
868
933
  schema = definition.get("schema")
869
934
  if not schema:
870
935
  return scopes, None
@@ -903,7 +968,7 @@ class SwaggerV20(BaseOpenAPISchema):
903
968
  else:
904
969
  files.append((name, file_value))
905
970
 
906
- for parameter in operation.definition.parameters:
971
+ for parameter in operation.body:
907
972
  if isinstance(parameter, OpenAPI20CompositeBody):
908
973
  for form_parameter in parameter.definition:
909
974
  name = form_parameter.name
@@ -918,7 +983,7 @@ class SwaggerV20(BaseOpenAPISchema):
918
983
  return files or None, data or None
919
984
 
920
985
  def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
921
- return self._get_consumes_for_operation(operation.definition.resolved)
986
+ return self._get_consumes_for_operation(operation.definition.raw)
922
987
 
923
988
  def make_case(
924
989
  self,
@@ -1024,7 +1089,7 @@ class OpenApi30(SwaggerV20):
1024
1089
  return collected
1025
1090
 
1026
1091
  def get_response_schema(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any] | None]:
1027
- scopes, definition = self.resolver.resolve_in_scope(fast_deepcopy(definition), scope)
1092
+ scopes, definition = self.resolver.resolve_in_scope(definition, scope)
1028
1093
  options = iter(definition.get("content", {}).values())
1029
1094
  option = next(options, None)
1030
1095
  # "schema" is an optional key in the `MediaType` object
@@ -1048,7 +1113,8 @@ class OpenApi30(SwaggerV20):
1048
1113
  return serialization.serialize_openapi3_parameters(definitions)
1049
1114
 
1050
1115
  def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
1051
- return list(operation.definition.resolved["requestBody"]["content"].keys())
1116
+ request_body = self._resolve_until_no_references(operation.definition.raw["requestBody"])
1117
+ return list(request_body["content"])
1052
1118
 
1053
1119
  def prepare_multipart(
1054
1120
  self, form_data: FormData, operation: APIOperation
@@ -1060,7 +1126,12 @@ class OpenApi30(SwaggerV20):
1060
1126
  :return: `files` and `data` values for `requests.request`.
1061
1127
  """
1062
1128
  files = []
1063
- content = operation.definition.resolved["requestBody"]["content"]
1129
+ definition = operation.definition.raw
1130
+ if "$ref" in definition["requestBody"]:
1131
+ body = self.resolver.resolve_all(definition["requestBody"], RECURSION_DEPTH_LIMIT)
1132
+ else:
1133
+ body = definition["requestBody"]
1134
+ content = body["content"]
1064
1135
  # Open API 3.0 requires media types to be present. We can get here only if the schema defines
1065
1136
  # the "multipart/form-data" media type, or any other more general media type that matches it (like `*/*`)
1066
1137
  for media_type, entry in content.items():
@@ -125,12 +125,10 @@ class OpenAPISecurityProcessor(BaseSecurityProcessor):
125
125
  """In Open API 3 security definitions are located in ``components`` and may have references inside."""
126
126
  components = schema.get("components", {})
127
127
  security_schemes = components.get("securitySchemes", {})
128
+ resolve = resolver.resolve
128
129
  if "$ref" in security_schemes:
129
- return resolver.resolve(security_schemes["$ref"])[1]
130
- return {
131
- key: resolver.resolve(value["$ref"])[1] if "$ref" in value else value
132
- for key, value in security_schemes.items()
133
- }
130
+ return resolve(security_schemes["$ref"])[1]
131
+ return {key: resolve(value["$ref"])[1] if "$ref" in value else value for key, value in security_schemes.items()}
134
132
 
135
133
  def _make_http_auth_parameter(self, definition: dict[str, Any]) -> dict[str, Any]:
136
134
  schema = make_auth_header_schema(definition)