schemathesis 3.28.0__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
 
@@ -4,22 +4,24 @@ Based on https://swagger.io/docs/specification/links/
4
4
  """
5
5
 
6
6
  from __future__ import annotations
7
+
7
8
  from dataclasses import dataclass, field
8
9
  from difflib import get_close_matches
9
- from typing import Any, Generator, NoReturn, Sequence, Union, TYPE_CHECKING
10
+ from typing import TYPE_CHECKING, Any, Generator, NoReturn, Sequence, Union
11
+
12
+ from jsonschema import RefResolver
10
13
 
14
+ from ...constants import NOT_SET
15
+ from ...internal.copy import fast_deepcopy
11
16
  from ...models import APIOperation, Case
12
17
  from ...parameters import ParameterSet
13
- from ...stateful import ParsedData, StatefulTest
18
+ from ...stateful import ParsedData, StatefulTest, UnresolvableLink
14
19
  from ...stateful.state_machine import Direction
15
20
  from ...types import NotSet
16
-
17
- from ...constants import NOT_SET
18
- from ...internal.copy import fast_deepcopy
19
21
  from . import expressions
20
22
  from .constants import LOCATION_TO_CONTAINER
21
23
  from .parameters import OpenAPI20Body, OpenAPI30Body, OpenAPIParameter
22
-
24
+ from .references import Unresolvable
23
25
 
24
26
  if TYPE_CHECKING:
25
27
  from ...transports.responses import GenericResponse
@@ -61,16 +63,17 @@ class Link(StatefulTest):
61
63
  def parse(self, case: Case, response: GenericResponse) -> ParsedData:
62
64
  """Parse data into a structure expected by links definition."""
63
65
  context = expressions.ExpressionContext(case=case, response=response)
64
- parameters = {
65
- parameter: expressions.evaluate(expression, context) for parameter, expression in self.parameters.items()
66
- }
67
- return ParsedData(
68
- parameters=parameters,
69
- # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#link-object
70
- # > A literal value or {expression} to use as a request body when calling the target operation.
71
- # In this case all literals will be passed as is, and expressions will be evaluated
72
- body=expressions.evaluate(self.request_body, context),
73
- )
66
+ parameters = {}
67
+ for parameter, expression in self.parameters.items():
68
+ evaluated = expressions.evaluate(expression, context)
69
+ if isinstance(evaluated, Unresolvable):
70
+ raise UnresolvableLink(f"Unresolvable reference in the link: {expression}")
71
+ parameters[parameter] = evaluated
72
+ # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#link-object
73
+ # > A literal value or {expression} to use as a request body when calling the target operation.
74
+ # In this case all literals will be passed as is, and expressions will be evaluated
75
+ body = expressions.evaluate(self.request_body, context)
76
+ return ParsedData(parameters=parameters, body=body)
74
77
 
75
78
  def make_operation(self, collected: list[ParsedData]) -> APIOperation:
76
79
  """Create a modified version of the original API operation with additional data merged in."""
@@ -153,14 +156,17 @@ class Link(StatefulTest):
153
156
 
154
157
  def get_links(response: GenericResponse, operation: APIOperation, field: str) -> Sequence[Link]:
155
158
  """Get `x-links` / `links` definitions from the schema."""
156
- responses = operation.definition.resolved["responses"]
159
+ responses = operation.definition.raw["responses"]
157
160
  if str(response.status_code) in responses:
158
- response_definition = responses[str(response.status_code)]
161
+ definition = responses[str(response.status_code)]
159
162
  elif response.status_code in responses:
160
- response_definition = responses[response.status_code]
163
+ definition = responses[response.status_code]
161
164
  else:
162
- response_definition = responses.get("default", {})
163
- 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, {})
164
170
  return [Link.from_definition(name, definition, operation) for name, definition in links.items()]
165
171
 
166
172
 
@@ -200,7 +206,7 @@ class OpenAPILink(Direction):
200
206
  # Therefore the container is empty, otherwise it will be at least an empty object
201
207
  if container is None:
202
208
  message = f"No such parameter in `{case.operation.method.upper()} {case.operation.path}`: `{name}`."
203
- possibilities = [param.name for param in case.operation.definition.parameters]
209
+ possibilities = [param.name for param in case.operation.iter_parameters()]
204
210
  matches = get_close_matches(name, possibilities)
205
211
  if matches:
206
212
  message += f" Did you mean `{matches[0]}`?"
@@ -222,7 +228,7 @@ def get_container(case: Case, location: str | None, name: str) -> dict[str, Any]
222
228
  if location:
223
229
  container_name = LOCATION_TO_CONTAINER[location]
224
230
  else:
225
- for param in case.operation.definition.parameters:
231
+ for param in case.operation.iter_parameters():
226
232
  if param.name == name:
227
233
  container_name = LOCATION_TO_CONTAINER[param.location]
228
234
  break
@@ -247,7 +253,8 @@ def normalize_parameter(parameter: str, expression: str) -> tuple[str | None, st
247
253
 
248
254
 
249
255
  def get_all_links(operation: APIOperation) -> Generator[tuple[str, OpenAPILink], None, None]:
250
- 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]
251
258
  for name, link_definition in definition.get(operation.schema.links_field, {}).items(): # type: ignore
252
259
  yield status_code, OpenAPILink(name, status_code, link_definition, operation)
253
260
 
@@ -274,6 +281,7 @@ def _get_response_by_status_code(responses: dict[StatusCode, dict[str, Any]], st
274
281
 
275
282
 
276
283
  def add_link(
284
+ resolver: RefResolver,
277
285
  responses: dict[StatusCode, dict[str, Any]],
278
286
  links_field: str,
279
287
  parameters: dict[str, str] | None,
@@ -283,6 +291,8 @@ def add_link(
283
291
  name: str | None = None,
284
292
  ) -> None:
285
293
  response = _get_response_by_status_code(responses, status_code)
294
+ if "$ref" in response:
295
+ _, response = resolver.resolve(response["$ref"])
286
296
  links_definition = response.setdefault(links_field, {})
287
297
  new_link: dict[str, str | dict[str, str]] = {}
288
298
  if parameters is not None:
@@ -296,8 +306,8 @@ def add_link(
296
306
  name = name or f"{target.method.upper()} {target.path}"
297
307
  # operationId is a dict lookup which is more efficient than using `operationRef`, since it
298
308
  # doesn't involve reference resolving when we will look up for this target during testing.
299
- if "operationId" in target.definition.resolved:
300
- new_link["operationId"] = target.definition.resolved["operationId"]
309
+ if "operationId" in target.definition.raw:
310
+ new_link["operationId"] = target.definition.raw["operationId"]
301
311
  else:
302
312
  new_link["operationRef"] = target.operation_reference
303
313
  # The name is arbitrary, so we don't really case what it is,