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.
@@ -552,10 +552,11 @@ def extract_requests_exception_details(exc: RequestException) -> tuple[str, list
552
552
  message = "Connection failed"
553
553
  inner = exc.args[0]
554
554
  if isinstance(inner, MaxRetryError) and inner.reason is not None:
555
- if ":" not in inner.reason.args[0]:
556
- reason = inner.reason.args[0]
555
+ arg = str(inner.reason.args[0])
556
+ if ":" not in arg:
557
+ reason = arg
557
558
  else:
558
- _, reason = inner.reason.args[0].split(":", maxsplit=1)
559
+ _, reason = arg.split(":", maxsplit=1)
559
560
  extra = [reason.strip()]
560
561
  else:
561
562
  extra = [" ".join(map(str, inner.args))]
schemathesis/models.py CHANGED
@@ -539,7 +539,7 @@ D = TypeVar("D", bound=dict)
539
539
 
540
540
 
541
541
  @dataclass
542
- class OperationDefinition(Generic[P, D]):
542
+ class OperationDefinition(Generic[D]):
543
543
  """A wrapper to store not resolved API operation definitions.
544
544
 
545
545
  To prevent recursion errors we need to store definitions without resolving references. But operation definitions
@@ -550,16 +550,8 @@ class OperationDefinition(Generic[P, D]):
550
550
  raw: D
551
551
  resolved: D
552
552
  scope: str
553
- parameters: Sequence[P]
554
553
 
555
- def __contains__(self, item: str | int) -> bool:
556
- return item in self.resolved
557
-
558
- def __getitem__(self, item: str | int) -> None | bool | float | str | list | dict[str, Any]:
559
- return self.resolved[item]
560
-
561
- def get(self, item: str | int, default: Any = None) -> None | bool | float | str | list | dict[str, Any]:
562
- return self.resolved.get(item, default)
554
+ __slots__ = ("raw", "resolved", "scope")
563
555
 
564
556
 
565
557
  C = TypeVar("C", bound=Case)
@@ -615,7 +615,7 @@ def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> No
615
615
  # Hypothesis's function digest depends on the test function signature. To reflect it for the web API case,
616
616
  # we use all API operation parameters in the digest.
617
617
  extra = operation.verbose_name.encode("utf8")
618
- for parameter in operation.definition.parameters:
618
+ for parameter in operation.iter_parameters():
619
619
  extra += parameter.serialize(operation).encode("utf8")
620
620
  test.hypothesis.inner_test._hypothesis_internal_add_digest = extra # type: ignore
621
621
 
schemathesis/schemas.py CHANGED
@@ -9,7 +9,7 @@ They give only static definitions of paths.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
- from collections.abc import Mapping, MutableMapping
12
+ from collections.abc import Mapping
13
13
  from contextlib import nullcontext
14
14
  from dataclasses import dataclass, field
15
15
  from functools import lru_cache
@@ -99,20 +99,23 @@ class BaseSchema(Mapping):
99
99
  sanitize_output: bool = True
100
100
 
101
101
  def __iter__(self) -> Iterator[str]:
102
- return iter(self.operations)
102
+ raise NotImplementedError
103
103
 
104
104
  def __getitem__(self, item: str) -> APIOperationMap:
105
105
  __tracebackhide__ = True
106
106
  try:
107
- return self.operations[item]
107
+ return self._get_operation_map(item)
108
108
  except KeyError as exc:
109
109
  self.on_missing_operation(item, exc)
110
110
 
111
+ def _get_operation_map(self, key: str) -> APIOperationMap:
112
+ raise NotImplementedError
113
+
111
114
  def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
112
115
  raise NotImplementedError
113
116
 
114
117
  def __len__(self) -> int:
115
- return len(self.operations)
118
+ return self.operations_count
116
119
 
117
120
  def hook(self, hook: str | Callable) -> Callable:
118
121
  return self.hooks.register(hook)
@@ -155,18 +158,6 @@ class BaseSchema(Mapping):
155
158
  def validate(self) -> None:
156
159
  raise NotImplementedError
157
160
 
158
- @property
159
- def operations(self) -> dict[str, APIOperationMap]:
160
- if not hasattr(self, "_operations"):
161
- operations = self.get_all_operations()
162
- self._operations = self._store_operations(operations)
163
- return self._operations
164
-
165
- def _store_operations(
166
- self, operations: Generator[Result[APIOperation, OperationSchemaError], None, None]
167
- ) -> dict[str, APIOperationMap]:
168
- raise NotImplementedError
169
-
170
161
  @property
171
162
  def operations_count(self) -> int:
172
163
  raise NotImplementedError
@@ -446,39 +437,34 @@ class BaseSchema(Mapping):
446
437
  **kwargs: Any,
447
438
  ) -> SearchStrategy:
448
439
  """Build a strategy for generating test cases for all defined API operations."""
449
- assert len(self.operations) > 0, "No API operations found"
440
+ assert len(self) > 0, "No API operations found"
450
441
  strategies = [
451
- operation.as_strategy(
442
+ operation.ok().as_strategy(
452
443
  hooks=hooks,
453
444
  auth_storage=auth_storage,
454
445
  data_generation_method=data_generation_method,
455
446
  generation_config=generation_config,
456
447
  **kwargs,
457
448
  )
458
- for operations in self.operations.values()
459
- for operation in operations.values()
449
+ for operation in self.get_all_operations(hooks=hooks)
450
+ if isinstance(operation, Ok)
460
451
  ]
461
452
  return combine_strategies(strategies)
462
453
 
463
454
 
464
455
  @dataclass
465
- class APIOperationMap(MutableMapping):
466
- data: MutableMapping
467
-
468
- def __setitem__(self, key: str, value: APIOperation) -> None:
469
- self.data[key] = value
456
+ class APIOperationMap(Mapping):
457
+ _schema: BaseSchema
458
+ _data: Mapping
470
459
 
471
460
  def __getitem__(self, item: str) -> APIOperation:
472
- return self.data[item]
473
-
474
- def __delitem__(self, key: str) -> None:
475
- del self.data[key]
461
+ return self._data[item]
476
462
 
477
463
  def __len__(self) -> int:
478
- return len(self.data)
464
+ return len(self._data)
479
465
 
480
466
  def __iter__(self) -> Iterator[str]:
481
- return iter(self.data)
467
+ return iter(self._data)
482
468
 
483
469
  def as_strategy(
484
470
  self,
@@ -489,7 +475,7 @@ class APIOperationMap(MutableMapping):
489
475
  **kwargs: Any,
490
476
  ) -> SearchStrategy:
491
477
  """Build a strategy for generating test cases for all API operations defined in this subset."""
492
- assert len(self.data) > 0, "No API operations found"
478
+ assert len(self._data) > 0, "No API operations found"
493
479
  strategies = [
494
480
  operation.as_strategy(
495
481
  hooks=hooks,
@@ -498,6 +484,6 @@ class APIOperationMap(MutableMapping):
498
484
  generation_config=generation_config,
499
485
  **kwargs,
500
486
  )
501
- for operation in self.data.values()
487
+ for operation in self._data.values()
502
488
  ]
503
489
  return combine_strategies(strategies)
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from ...schemas import APIOperationMap
8
+ from ...models import APIOperation
9
+
10
+
11
+ @dataclass
12
+ class OperationCache:
13
+ _maps: dict[str, APIOperationMap] = field(default_factory=dict)
14
+ _operations: dict[str, APIOperation] = field(default_factory=dict)
15
+
16
+ def get_map(self, key: str) -> APIOperationMap | None:
17
+ return self._maps.get(key)
18
+
19
+ def insert_map(self, key: str, value: APIOperationMap) -> None:
20
+ self._maps[key] = value
21
+
22
+ def get_operation(self, key: str) -> APIOperation | None:
23
+ return self._operations.get(key)
24
+
25
+ def insert_operation(self, key: str, value: APIOperation) -> None:
26
+ self._operations[key] = value
@@ -14,13 +14,12 @@ from typing import (
14
14
  cast,
15
15
  TYPE_CHECKING,
16
16
  NoReturn,
17
- MutableMapping,
17
+ Mapping,
18
18
  Iterator,
19
19
  )
20
20
  from urllib.parse import urlsplit, urlunsplit
21
21
 
22
22
  import graphql
23
- from graphql import GraphQLNamedType
24
23
  from hypothesis import strategies as st
25
24
  from hypothesis.strategies import SearchStrategy
26
25
  from hypothesis_graphql import strategies as gql_st
@@ -45,6 +44,7 @@ from ...models import APIOperation, Case, CheckFunction, OperationDefinition
45
44
  from ...schemas import BaseSchema, APIOperationMap
46
45
  from ...stateful import Stateful, StatefulTest
47
46
  from ...types import Body, Cookies, Headers, NotSet, PathParameters, Query
47
+ from ._cache import OperationCache
48
48
  from .scalars import CUSTOM_SCALARS, get_extra_scalar_strategies
49
49
 
50
50
  if TYPE_CHECKING:
@@ -103,9 +103,37 @@ class GraphQLOperationDefinition(OperationDefinition):
103
103
 
104
104
  @dataclass
105
105
  class GraphQLSchema(BaseSchema):
106
+ _operation_cache: OperationCache = field(default_factory=OperationCache)
107
+
106
108
  def __repr__(self) -> str:
107
109
  return f"<{self.__class__.__name__}>"
108
110
 
111
+ def __iter__(self) -> Iterator[str]:
112
+ schema = self.client_schema
113
+ for operation_type in (
114
+ schema.query_type,
115
+ schema.mutation_type,
116
+ ):
117
+ if operation_type is not None:
118
+ yield operation_type.name
119
+
120
+ def _get_operation_map(self, key: str) -> APIOperationMap:
121
+ cache = self._operation_cache
122
+ map = cache.get_map(key)
123
+ if map is not None:
124
+ return map
125
+ schema = self.client_schema
126
+ for root_type, operation_type in (
127
+ (RootType.QUERY, schema.query_type),
128
+ (RootType.MUTATION, schema.mutation_type),
129
+ ):
130
+ if operation_type and operation_type.name == key:
131
+ map = APIOperationMap(self, {})
132
+ map._data = FieldMap(map, root_type, operation_type)
133
+ cache.insert_map(key, map)
134
+ return map
135
+ raise KeyError(key)
136
+
109
137
  def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
110
138
  raw_schema = self.raw_schema["__schema"]
111
139
  type_names = [type_def["name"] for type_def in raw_schema.get("types", [])]
@@ -115,19 +143,6 @@ class GraphQLSchema(BaseSchema):
115
143
  message += f". Did you mean `{matches[0]}`?"
116
144
  raise OperationNotFound(message=message, item=item) from exc
117
145
 
118
- def _store_operations(
119
- self, operations: Generator[Result[APIOperation, OperationSchemaError], None, None]
120
- ) -> dict[str, APIOperationMap]:
121
- output: dict[str, APIOperationMap] = {}
122
- for result in operations:
123
- if isinstance(result, Ok):
124
- operation = result.ok()
125
- definition = cast(GraphQLOperationDefinition, operation.definition)
126
- type_name = definition.type_.name if isinstance(definition.type_, GraphQLNamedType) else "Unknown"
127
- for_type = output.setdefault(type_name, APIOperationMap(FieldMap()))
128
- for_type[definition.field_name] = operation
129
- return output
130
-
131
146
  def get_full_path(self, path: str) -> str:
132
147
  return self.base_path
133
148
 
@@ -178,26 +193,8 @@ class GraphQLSchema(BaseSchema):
178
193
  ):
179
194
  if operation_type is None:
180
195
  continue
181
- for field_name, definition in operation_type.fields.items():
182
- operation: APIOperation = APIOperation(
183
- base_url=self.get_base_url(),
184
- path=self.base_path,
185
- verbose_name=f"{operation_type.name}.{field_name}",
186
- method="POST",
187
- app=self.app,
188
- schema=self,
189
- # Parameters are not yet supported
190
- definition=GraphQLOperationDefinition(
191
- raw=definition,
192
- resolved=definition,
193
- scope="",
194
- parameters=[],
195
- type_=operation_type,
196
- field_name=field_name,
197
- root_type=root_type,
198
- ),
199
- case_cls=GraphQLCase,
200
- )
196
+ for field_name, field_ in operation_type.fields.items():
197
+ operation = self._build_operation(root_type, operation_type, field_name, field_)
201
198
  context = HookContext(operation=operation)
202
199
  if (
203
200
  should_skip_operation(GLOBAL_HOOK_DISPATCHER, context)
@@ -207,6 +204,32 @@ class GraphQLSchema(BaseSchema):
207
204
  continue
208
205
  yield Ok(operation)
209
206
 
207
+ def _build_operation(
208
+ self,
209
+ root_type: RootType,
210
+ operation_type: graphql.GraphQLObjectType,
211
+ field_name: str,
212
+ field: graphql.GraphQlField,
213
+ ) -> APIOperation:
214
+ return APIOperation(
215
+ base_url=self.get_base_url(),
216
+ path=self.base_path,
217
+ verbose_name=f"{operation_type.name}.{field_name}",
218
+ method="POST",
219
+ app=self.app,
220
+ schema=self,
221
+ # Parameters are not yet supported
222
+ definition=GraphQLOperationDefinition(
223
+ raw=field,
224
+ resolved=field,
225
+ scope="",
226
+ type_=operation_type,
227
+ field_name=field_name,
228
+ root_type=root_type,
229
+ ),
230
+ case_cls=GraphQLCase,
231
+ )
232
+
210
233
  def get_case_strategy(
211
234
  self,
212
235
  operation: APIOperation,
@@ -262,31 +285,41 @@ class GraphQLSchema(BaseSchema):
262
285
 
263
286
 
264
287
  @dataclass
265
- class FieldMap(MutableMapping):
288
+ class FieldMap(Mapping):
266
289
  """Container for accessing API operations.
267
290
 
268
291
  Provides a more specific error message if API operation is not found.
269
292
  """
270
293
 
271
- data: dict[str, APIOperation] = field(default_factory=dict)
272
-
273
- def __setitem__(self, key: str, value: APIOperation) -> None:
274
- self.data[key] = value
294
+ _parent: APIOperationMap
295
+ _root_type: RootType
296
+ _operation_type: graphql.GraphQLObjectType
275
297
 
276
- def __delitem__(self, key: str) -> None:
277
- del self.data[key]
298
+ __slots__ = ("_parent", "_root_type", "_operation_type")
278
299
 
279
300
  def __len__(self) -> int:
280
- return len(self.data)
301
+ return len(self._operation_type.fields)
281
302
 
282
303
  def __iter__(self) -> Iterator[str]:
283
- return iter(self.data)
304
+ return iter(self._operation_type.fields)
305
+
306
+ def _init_operation(self, field_name: str) -> APIOperation:
307
+ schema = cast(GraphQLSchema, self._parent._schema)
308
+ cache = schema._operation_cache
309
+ operation = cache.get_operation(field_name)
310
+ if operation is not None:
311
+ return operation
312
+ operation_type = self._operation_type
313
+ field_ = operation_type.fields[field_name]
314
+ operation = schema._build_operation(self._root_type, operation_type, field_name, field_)
315
+ cache.insert_operation(field_name, operation)
316
+ return operation
284
317
 
285
318
  def __getitem__(self, item: str) -> APIOperation:
286
319
  try:
287
- return self.data[item]
320
+ return self._init_operation(item)
288
321
  except KeyError as exc:
289
- field_names = [operation.definition.field_name for operation in self.data.values()] # type: ignore[attr-defined]
322
+ field_names = list(self._operation_type.fields)
290
323
  matches = get_close_matches(item, field_names)
291
324
  message = f"`{item}` field not found"
292
325
  if matches:
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING, Any, Tuple
5
+
6
+ if TYPE_CHECKING:
7
+ from ...models import APIOperation
8
+ from ...schemas import APIOperationMap
9
+
10
+
11
+ @dataclass
12
+ class OperationCacheEntry:
13
+ path: str
14
+ method: str
15
+ # The resolution scope of the operation
16
+ scope: str
17
+ # Parent path item
18
+ path_item: dict[str, Any]
19
+ # Unresolved operation definition
20
+ operation: dict[str, Any]
21
+ __slots__ = ("path", "method", "scope", "path_item", "operation")
22
+
23
+
24
+ # During traversal, we need to keep track of the scope, path, and method
25
+ TraversalKey = Tuple[str, str, str]
26
+ OperationId = str
27
+ Reference = str
28
+
29
+
30
+ @dataclass
31
+ class OperationCache:
32
+ """Cache for Open API operations.
33
+
34
+ This cache contains multiple levels to avoid unnecessary parsing of the schema.
35
+ """
36
+
37
+ # Cache to avoid schema traversal on every access
38
+ _id_to_definition: dict[OperationId, OperationCacheEntry] = field(default_factory=dict)
39
+ # Map map between 1st & 2nd level cache keys
40
+ # Even though 1st level keys could be directly mapped to Python objects in memory, we need to keep them separate
41
+ # to ensure a single owner of the operation instance.
42
+ _id_to_operation: dict[OperationId, int] = field(default_factory=dict)
43
+ _traversal_key_to_operation: dict[TraversalKey, int] = field(default_factory=dict)
44
+ _reference_to_operation: dict[Reference, int] = field(default_factory=dict)
45
+ # The actual operations
46
+ _operations: list[APIOperation] = field(default_factory=list)
47
+ # Cache for operation maps
48
+ _maps: dict[str, APIOperationMap] = field(default_factory=dict)
49
+
50
+ @property
51
+ def known_operation_ids(self) -> list[str]:
52
+ return list(self._id_to_definition)
53
+
54
+ @property
55
+ def has_ids_to_definitions(self) -> bool:
56
+ return bool(self._id_to_definition)
57
+
58
+ def _append_operation(self, operation: APIOperation) -> int:
59
+ idx = len(self._operations)
60
+ self._operations.append(operation)
61
+ return idx
62
+
63
+ def insert_definition_by_id(
64
+ self,
65
+ operation_id: str,
66
+ path: str,
67
+ method: str,
68
+ scope: str,
69
+ path_item: dict[str, Any],
70
+ operation: dict[str, Any],
71
+ ) -> None:
72
+ """Insert a new operation definition into cache."""
73
+ self._id_to_definition[operation_id] = OperationCacheEntry(
74
+ path=path, method=method, scope=scope, path_item=path_item, operation=operation
75
+ )
76
+
77
+ def get_definition_by_id(self, operation_id: str) -> OperationCacheEntry:
78
+ """Get an operation definition by its ID."""
79
+ # TODO: Avoid KeyError in the future
80
+ return self._id_to_definition[operation_id]
81
+
82
+ def insert_operation(
83
+ self,
84
+ operation: APIOperation,
85
+ *,
86
+ traversal_key: TraversalKey,
87
+ operation_id: str | None = None,
88
+ reference: str | None = None,
89
+ ) -> None:
90
+ """Insert a new operation into cache by one or multiple keys."""
91
+ idx = self._append_operation(operation)
92
+ self._traversal_key_to_operation[traversal_key] = idx
93
+ if operation_id is not None:
94
+ self._id_to_operation[operation_id] = idx
95
+ if reference is not None:
96
+ self._reference_to_operation[reference] = idx
97
+
98
+ def get_operation_by_id(self, operation_id: str) -> APIOperation | None:
99
+ """Get an operation by its ID."""
100
+ idx = self._id_to_operation.get(operation_id)
101
+ if idx is not None:
102
+ return self._operations[idx]
103
+ return None
104
+
105
+ def get_operation_by_reference(self, reference: str) -> APIOperation | None:
106
+ """Get an operation by its reference."""
107
+ idx = self._reference_to_operation.get(reference)
108
+ if idx is not None:
109
+ return self._operations[idx]
110
+ return None
111
+
112
+ def get_operation_by_traversal_key(self, key: TraversalKey) -> APIOperation | None:
113
+ """Get an operation by its traverse key."""
114
+ idx = self._traversal_key_to_operation.get(key)
115
+ if idx is not None:
116
+ return self._operations[idx]
117
+ return None
118
+
119
+ def get_map(self, key: str) -> APIOperationMap | None:
120
+ return self._maps.get(key)
121
+
122
+ def insert_map(self, key: str, value: APIOperationMap) -> None:
123
+ self._maps[key] = value
@@ -431,6 +431,8 @@ def _build_custom_formats(
431
431
  custom_formats = {**get_default_format_strategies(), **STRING_FORMATS, **(custom_formats or {})}
432
432
  if generation_config.headers.strategy is not None:
433
433
  custom_formats[HEADER_FORMAT] = generation_config.headers.strategy
434
+ elif not generation_config.allow_x00:
435
+ custom_formats[HEADER_FORMAT] = header_values(blacklist_characters="\n\r\x00")
434
436
  return custom_formats
435
437
 
436
438
 
@@ -9,6 +9,8 @@ from dataclasses import dataclass, field
9
9
  from difflib import get_close_matches
10
10
  from typing import TYPE_CHECKING, Any, Generator, NoReturn, Sequence, Union
11
11
 
12
+ from jsonschema import RefResolver
13
+
12
14
  from ...constants import NOT_SET
13
15
  from ...internal.copy import fast_deepcopy
14
16
  from ...models import APIOperation, Case
@@ -154,14 +156,17 @@ class Link(StatefulTest):
154
156
 
155
157
  def get_links(response: GenericResponse, operation: APIOperation, field: str) -> Sequence[Link]:
156
158
  """Get `x-links` / `links` definitions from the schema."""
157
- responses = operation.definition.resolved["responses"]
159
+ responses = operation.definition.raw["responses"]
158
160
  if str(response.status_code) in responses:
159
- response_definition = responses[str(response.status_code)]
161
+ definition = responses[str(response.status_code)]
160
162
  elif response.status_code in responses:
161
- response_definition = responses[response.status_code]
163
+ definition = responses[response.status_code]
162
164
  else:
163
- response_definition = responses.get("default", {})
164
- links = response_definition.get(field, {})
165
+ definition = responses.get("default", {})
166
+ if not definition:
167
+ return []
168
+ _, definition = operation.schema.resolver.resolve_in_scope(definition, operation.definition.scope) # type: ignore[attr-defined]
169
+ links = definition.get(field, {})
165
170
  return [Link.from_definition(name, definition, operation) for name, definition in links.items()]
166
171
 
167
172
 
@@ -201,7 +206,7 @@ class OpenAPILink(Direction):
201
206
  # Therefore the container is empty, otherwise it will be at least an empty object
202
207
  if container is None:
203
208
  message = f"No such parameter in `{case.operation.method.upper()} {case.operation.path}`: `{name}`."
204
- possibilities = [param.name for param in case.operation.definition.parameters]
209
+ possibilities = [param.name for param in case.operation.iter_parameters()]
205
210
  matches = get_close_matches(name, possibilities)
206
211
  if matches:
207
212
  message += f" Did you mean `{matches[0]}`?"
@@ -223,7 +228,7 @@ def get_container(case: Case, location: str | None, name: str) -> dict[str, Any]
223
228
  if location:
224
229
  container_name = LOCATION_TO_CONTAINER[location]
225
230
  else:
226
- for param in case.operation.definition.parameters:
231
+ for param in case.operation.iter_parameters():
227
232
  if param.name == name:
228
233
  container_name = LOCATION_TO_CONTAINER[param.location]
229
234
  break
@@ -248,7 +253,8 @@ def normalize_parameter(parameter: str, expression: str) -> tuple[str | None, st
248
253
 
249
254
 
250
255
  def get_all_links(operation: APIOperation) -> Generator[tuple[str, OpenAPILink], None, None]:
251
- for status_code, definition in operation.definition.resolved["responses"].items():
256
+ for status_code, definition in operation.definition.raw["responses"].items():
257
+ definition = operation.schema.resolver.resolve_all(definition) # type: ignore[attr-defined]
252
258
  for name, link_definition in definition.get(operation.schema.links_field, {}).items(): # type: ignore
253
259
  yield status_code, OpenAPILink(name, status_code, link_definition, operation)
254
260
 
@@ -275,6 +281,7 @@ def _get_response_by_status_code(responses: dict[StatusCode, dict[str, Any]], st
275
281
 
276
282
 
277
283
  def add_link(
284
+ resolver: RefResolver,
278
285
  responses: dict[StatusCode, dict[str, Any]],
279
286
  links_field: str,
280
287
  parameters: dict[str, str] | None,
@@ -284,6 +291,8 @@ def add_link(
284
291
  name: str | None = None,
285
292
  ) -> None:
286
293
  response = _get_response_by_status_code(responses, status_code)
294
+ if "$ref" in response:
295
+ _, response = resolver.resolve(response["$ref"])
287
296
  links_definition = response.setdefault(links_field, {})
288
297
  new_link: dict[str, str | dict[str, str]] = {}
289
298
  if parameters is not None:
@@ -297,8 +306,8 @@ def add_link(
297
306
  name = name or f"{target.method.upper()} {target.path}"
298
307
  # operationId is a dict lookup which is more efficient than using `operationRef`, since it
299
308
  # doesn't involve reference resolving when we will look up for this target during testing.
300
- if "operationId" in target.definition.resolved:
301
- new_link["operationId"] = target.definition.resolved["operationId"]
309
+ if "operationId" in target.definition.raw:
310
+ new_link["operationId"] = target.definition.raw["operationId"]
302
311
  else:
303
312
  new_link["operationRef"] = target.operation_reference
304
313
  # The name is arbitrary, so we don't really case what it is,
@@ -65,10 +65,13 @@ class InliningResolver(jsonschema.RefResolver):
65
65
 
66
66
  def resolve_all(self, item: JSONType, recursion_level: int = 0) -> JSONType:
67
67
  """Recursively resolve all references in the given object."""
68
+ resolve = self.resolve_all
68
69
  if isinstance(item, dict):
69
70
  ref = item.get("$ref")
70
- if ref is not None and isinstance(ref, str):
71
- with self.resolving(ref) as resolved:
71
+ if isinstance(ref, str):
72
+ url, resolved = self.resolve(ref)
73
+ self.push_scope(url)
74
+ try:
72
75
  # If the next level of recursion exceeds the limit, then we need to copy it explicitly
73
76
  # In other cases, this method create new objects for mutable types (dict & list)
74
77
  next_recursion_level = recursion_level + 1
@@ -76,10 +79,18 @@ class InliningResolver(jsonschema.RefResolver):
76
79
  copied = fast_deepcopy(resolved)
77
80
  remove_optional_references(copied)
78
81
  return copied
79
- return self.resolve_all(resolved, next_recursion_level)
80
- return {key: self.resolve_all(sub_item, recursion_level) for key, sub_item in item.items()}
82
+ return resolve(resolved, next_recursion_level)
83
+ finally:
84
+ self.pop_scope()
85
+ return {
86
+ key: resolve(sub_item, recursion_level) if isinstance(sub_item, (dict, list)) else sub_item
87
+ for key, sub_item in item.items()
88
+ }
81
89
  if isinstance(item, list):
82
- return [self.resolve_all(sub_item, recursion_level) for sub_item in item]
90
+ return [
91
+ self.resolve_all(sub_item, recursion_level) if isinstance(sub_item, (dict, list)) else sub_item
92
+ for sub_item in item
93
+ ]
83
94
  return item
84
95
 
85
96
  def resolve_in_scope(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any]]:
@@ -89,7 +100,7 @@ class InliningResolver(jsonschema.RefResolver):
89
100
  if "$ref" in definition:
90
101
  self.push_scope(scope)
91
102
  try:
92
- new_scope, definition = fast_deepcopy(self.resolve(definition["$ref"]))
103
+ new_scope, definition = self.resolve(definition["$ref"])
93
104
  finally:
94
105
  self.pop_scope()
95
106
  scopes.append(new_scope)