schemathesis 4.1.3__py3-none-any.whl → 4.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. schemathesis/cli/commands/run/executor.py +1 -1
  2. schemathesis/cli/commands/run/handlers/base.py +28 -1
  3. schemathesis/cli/commands/run/handlers/cassettes.py +10 -12
  4. schemathesis/cli/commands/run/handlers/junitxml.py +5 -6
  5. schemathesis/cli/commands/run/handlers/output.py +7 -1
  6. schemathesis/cli/ext/fs.py +1 -1
  7. schemathesis/config/_diff_base.py +3 -1
  8. schemathesis/config/_operations.py +2 -0
  9. schemathesis/config/_phases.py +21 -4
  10. schemathesis/config/_projects.py +10 -2
  11. schemathesis/core/adapter.py +34 -0
  12. schemathesis/core/errors.py +29 -5
  13. schemathesis/core/jsonschema/__init__.py +13 -0
  14. schemathesis/core/jsonschema/bundler.py +163 -0
  15. schemathesis/{specs/openapi/constants.py → core/jsonschema/keywords.py} +0 -8
  16. schemathesis/core/jsonschema/references.py +122 -0
  17. schemathesis/core/jsonschema/types.py +41 -0
  18. schemathesis/core/media_types.py +6 -4
  19. schemathesis/core/parameters.py +37 -0
  20. schemathesis/core/transforms.py +25 -2
  21. schemathesis/core/validation.py +19 -0
  22. schemathesis/engine/context.py +1 -1
  23. schemathesis/engine/errors.py +11 -18
  24. schemathesis/engine/phases/stateful/_executor.py +1 -1
  25. schemathesis/engine/phases/unit/_executor.py +30 -13
  26. schemathesis/errors.py +4 -0
  27. schemathesis/filters.py +2 -2
  28. schemathesis/generation/coverage.py +89 -13
  29. schemathesis/generation/hypothesis/__init__.py +4 -1
  30. schemathesis/generation/hypothesis/builder.py +108 -70
  31. schemathesis/generation/meta.py +5 -14
  32. schemathesis/generation/overrides.py +17 -17
  33. schemathesis/pytest/lazy.py +1 -1
  34. schemathesis/pytest/plugin.py +1 -6
  35. schemathesis/schemas.py +22 -72
  36. schemathesis/specs/graphql/schemas.py +27 -16
  37. schemathesis/specs/openapi/_hypothesis.py +83 -68
  38. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  39. schemathesis/specs/openapi/adapter/parameters.py +504 -0
  40. schemathesis/specs/openapi/adapter/protocol.py +57 -0
  41. schemathesis/specs/openapi/adapter/references.py +19 -0
  42. schemathesis/specs/openapi/adapter/responses.py +329 -0
  43. schemathesis/specs/openapi/adapter/security.py +141 -0
  44. schemathesis/specs/openapi/adapter/v2.py +28 -0
  45. schemathesis/specs/openapi/adapter/v3_0.py +28 -0
  46. schemathesis/specs/openapi/adapter/v3_1.py +28 -0
  47. schemathesis/specs/openapi/checks.py +99 -90
  48. schemathesis/specs/openapi/converter.py +114 -27
  49. schemathesis/specs/openapi/examples.py +210 -168
  50. schemathesis/specs/openapi/negative/__init__.py +12 -7
  51. schemathesis/specs/openapi/negative/mutations.py +68 -40
  52. schemathesis/specs/openapi/references.py +2 -175
  53. schemathesis/specs/openapi/schemas.py +142 -490
  54. schemathesis/specs/openapi/serialization.py +15 -7
  55. schemathesis/specs/openapi/stateful/__init__.py +17 -12
  56. schemathesis/specs/openapi/stateful/inference.py +13 -11
  57. schemathesis/specs/openapi/stateful/links.py +5 -20
  58. schemathesis/specs/openapi/types/__init__.py +3 -0
  59. schemathesis/specs/openapi/types/v3.py +68 -0
  60. schemathesis/specs/openapi/utils.py +1 -13
  61. schemathesis/transport/requests.py +3 -11
  62. schemathesis/transport/serialization.py +63 -27
  63. schemathesis/transport/wsgi.py +1 -8
  64. {schemathesis-4.1.3.dist-info → schemathesis-4.2.0.dist-info}/METADATA +2 -2
  65. {schemathesis-4.1.3.dist-info → schemathesis-4.2.0.dist-info}/RECORD +68 -53
  66. schemathesis/specs/openapi/parameters.py +0 -405
  67. schemathesis/specs/openapi/security.py +0 -162
  68. {schemathesis-4.1.3.dist-info → schemathesis-4.2.0.dist-info}/WHEEL +0 -0
  69. {schemathesis-4.1.3.dist-info → schemathesis-4.2.0.dist-info}/entry_points.txt +0 -0
  70. {schemathesis-4.1.3.dist-info → schemathesis-4.2.0.dist-info}/licenses/LICENSE +0 -0
schemathesis/schemas.py CHANGED
@@ -18,7 +18,8 @@ from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
18
18
 
19
19
  from schemathesis import transport
20
20
  from schemathesis.config import ProjectConfig
21
- from schemathesis.core import NOT_SET, NotSet
21
+ from schemathesis.core import NOT_SET, NotSet, media_types
22
+ from schemathesis.core.adapter import OperationParameter, ResponsesContainer
22
23
  from schemathesis.core.errors import IncorrectUsage, InvalidSchema
23
24
  from schemathesis.core.result import Ok, Result
24
25
  from schemathesis.core.transport import Response
@@ -302,9 +303,6 @@ class BaseSchema(Mapping):
302
303
  def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
303
304
  raise NotImplementedError
304
305
 
305
- def get_security_requirements(self, operation: APIOperation) -> list[str]:
306
- raise NotImplementedError
307
-
308
306
  def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
309
307
  raise NotImplementedError
310
308
 
@@ -432,21 +430,12 @@ class BaseSchema(Mapping):
432
430
  """
433
431
  raise NotImplementedError
434
432
 
435
- def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
436
- raise NotImplementedError
437
-
438
433
  def get_tags(self, operation: APIOperation) -> list[str] | None:
439
434
  raise NotImplementedError
440
435
 
441
436
  def validate_response(self, operation: APIOperation, response: Response) -> bool | None:
442
437
  raise NotImplementedError
443
438
 
444
- def prepare_schema(self, schema: Any) -> Any:
445
- raise NotImplementedError
446
-
447
- def _get_payload_schema(self, definition: dict[str, Any], media_type: str) -> dict[str, Any] | None:
448
- raise NotImplementedError
449
-
450
439
  def as_strategy(
451
440
  self,
452
441
  generation_mode: GenerationMode = GenerationMode.POSITIVE,
@@ -514,39 +503,7 @@ class APIOperationMap(Mapping):
514
503
  return strategies.combine(_strategies)
515
504
 
516
505
 
517
- @dataclass(eq=False)
518
- class Parameter:
519
- """A logically separate parameter bound to a location (e.g., to "query string").
520
-
521
- For example, if the API requires multiple headers to be present, each header is presented as a separate
522
- `Parameter` instance.
523
- """
524
-
525
- # The parameter definition in the language acceptable by the API
526
- definition: Any
527
-
528
- __slots__ = ("definition",)
529
-
530
- @property
531
- def location(self) -> str:
532
- """Where this parameter is located.
533
-
534
- E.g. "query" or "body"
535
- """
536
- raise NotImplementedError
537
-
538
- @property
539
- def name(self) -> str:
540
- """Parameter name."""
541
- raise NotImplementedError
542
-
543
- @property
544
- def is_required(self) -> bool:
545
- """Whether the parameter is required for a successful API call."""
546
- raise NotImplementedError
547
-
548
-
549
- P = TypeVar("P", bound=Parameter)
506
+ P = TypeVar("P", bound=OperationParameter)
550
507
 
551
508
 
552
509
  @dataclass
@@ -572,14 +529,11 @@ class ParameterSet(Generic[P]):
572
529
  return parameter
573
530
  return None
574
531
 
575
- def contains(self, name: str) -> bool:
576
- return self.get(name) is not None
577
-
578
- def __contains__(self, item: str) -> bool:
579
- return self.contains(item)
580
-
581
- def __bool__(self) -> bool:
582
- return bool(self.items)
532
+ def __contains__(self, name: str) -> bool:
533
+ for parameter in self.items:
534
+ if parameter.name == name:
535
+ return True
536
+ return False
583
537
 
584
538
  def __iter__(self) -> Generator[P, None, None]:
585
539
  yield from iter(self.items)
@@ -595,6 +549,8 @@ class PayloadAlternatives(ParameterSet[P]):
595
549
  """A set of alternative payloads."""
596
550
 
597
551
 
552
+ R = TypeVar("R", bound=ResponsesContainer)
553
+ S = TypeVar("S")
598
554
  D = TypeVar("D", bound=dict)
599
555
 
600
556
 
@@ -608,16 +564,14 @@ class OperationDefinition(Generic[D]):
608
564
  """
609
565
 
610
566
  raw: D
611
- resolved: D
612
- scope: str
613
567
 
614
- __slots__ = ("raw", "resolved", "scope")
568
+ __slots__ = ("raw",)
615
569
 
616
570
  def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
617
571
 
618
572
 
619
573
  @dataclass()
620
- class APIOperation(Generic[P]):
574
+ class APIOperation(Generic[P, R, S]):
621
575
  """An API operation (e.g., `GET /users`)."""
622
576
 
623
577
  # `path` does not contain `basePath`
@@ -627,6 +581,8 @@ class APIOperation(Generic[P]):
627
581
  method: str
628
582
  definition: OperationDefinition = field(repr=False)
629
583
  schema: BaseSchema
584
+ responses: R
585
+ security: S
630
586
  label: str = None # type: ignore
631
587
  app: Any = None
632
588
  base_url: str | None = None
@@ -640,7 +596,7 @@ class APIOperation(Generic[P]):
640
596
  if self.label is None:
641
597
  self.label = f"{self.method.upper()} {self.path}" # type: ignore
642
598
 
643
- def __deepcopy__(self, memo: dict) -> APIOperation[P]:
599
+ def __deepcopy__(self, memo: dict) -> APIOperation[P, R, S]:
644
600
  return self
645
601
 
646
602
  def __hash__(self) -> int:
@@ -655,10 +611,6 @@ class APIOperation(Generic[P]):
655
611
  def full_path(self) -> str:
656
612
  return self.schema.get_full_path(self.path)
657
613
 
658
- @property
659
- def links(self) -> dict[str, dict[str, Any]]:
660
- return self.schema.get_links(self)
661
-
662
614
  @property
663
615
  def tags(self) -> list[str] | None:
664
616
  return self.schema.get_tags(self)
@@ -690,6 +642,13 @@ class APIOperation(Generic[P]):
690
642
  return container.get(name)
691
643
  return None
692
644
 
645
+ def get_bodies_for_media_type(self, media_type: str) -> Iterator[P]:
646
+ main_target, sub_target = media_types.parse(media_type)
647
+ for body in self.body:
648
+ main, sub = media_types.parse(body.media_type) # type:ignore[attr-defined]
649
+ if main in ("*", main_target) and sub in ("*", sub_target):
650
+ yield body
651
+
693
652
  def as_strategy(
694
653
  self,
695
654
  generation_mode: GenerationMode = GenerationMode.POSITIVE,
@@ -739,9 +698,6 @@ class APIOperation(Generic[P]):
739
698
  strategy = _apply_hooks(hooks, strategy)
740
699
  return strategy
741
700
 
742
- def get_security_requirements(self) -> list[str]:
743
- return self.schema.get_security_requirements(self)
744
-
745
701
  def get_strategies_from_examples(self, **kwargs: Any) -> list[SearchStrategy[Case]]:
746
702
  return self.schema.get_strategies_from_examples(self, **kwargs)
747
703
 
@@ -839,9 +795,3 @@ class APIOperation(Generic[P]):
839
795
  return True
840
796
  except AssertionError:
841
797
  return False
842
-
843
- def get_raw_payload_schema(self, media_type: str) -> dict[str, Any] | None:
844
- return self.schema._get_payload_schema(self.definition.raw, media_type)
845
-
846
- def get_resolved_payload_schema(self, media_type: str) -> dict[str, Any] | None:
847
- return self.schema._get_payload_schema(self.definition.resolved, media_type)
@@ -27,13 +27,13 @@ from requests.structures import CaseInsensitiveDict
27
27
  from schemathesis import auths
28
28
  from schemathesis.core import NOT_SET, NotSet, Specification
29
29
  from schemathesis.core.errors import InvalidSchema, OperationNotFound
30
+ from schemathesis.core.parameters import ParameterLocation
30
31
  from schemathesis.core.result import Ok, Result
31
32
  from schemathesis.generation import GenerationMode
32
33
  from schemathesis.generation.case import Case
33
34
  from schemathesis.generation.meta import (
34
35
  CaseMetadata,
35
36
  ComponentInfo,
36
- ComponentKind,
37
37
  ExamplesPhaseData,
38
38
  FuzzingPhaseData,
39
39
  GenerationInfo,
@@ -48,7 +48,6 @@ from schemathesis.schemas import (
48
48
  BaseSchema,
49
49
  OperationDefinition,
50
50
  )
51
- from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
52
51
 
53
52
  from .scalars import CUSTOM_SCALARS, get_extra_scalar_strategies
54
53
 
@@ -70,7 +69,7 @@ class GraphQLOperationDefinition(OperationDefinition):
70
69
  type_: graphql.GraphQLType
71
70
  root_type: RootType
72
71
 
73
- __slots__ = ("raw", "resolved", "scope", "field_name", "type_", "root_type")
72
+ __slots__ = ("raw", "field_name", "type_", "root_type")
74
73
 
75
74
  def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
76
75
 
@@ -83,6 +82,14 @@ class GraphQLOperationDefinition(OperationDefinition):
83
82
  return self.root_type == RootType.MUTATION
84
83
 
85
84
 
85
+ class GraphQLResponses:
86
+ def find_by_status_code(self, status_code: int) -> None:
87
+ return None # pragma: no cover
88
+
89
+ def add(self, status_code: str, definition: dict[str, Any]) -> None:
90
+ return None # pragma: no cover
91
+
92
+
86
93
  @dataclass
87
94
  class GraphQLSchema(BaseSchema):
88
95
  def __repr__(self) -> str:
@@ -158,6 +165,8 @@ class GraphQLSchema(BaseSchema):
158
165
  label="",
159
166
  method="POST",
160
167
  schema=self,
168
+ responses=GraphQLResponses(),
169
+ security=None,
161
170
  definition=None, # type: ignore
162
171
  )
163
172
 
@@ -210,11 +219,11 @@ class GraphQLSchema(BaseSchema):
210
219
  method="POST",
211
220
  app=self.app,
212
221
  schema=self,
222
+ responses=GraphQLResponses(),
223
+ security=None,
213
224
  # Parameters are not yet supported
214
225
  definition=GraphQLOperationDefinition(
215
226
  raw=field,
216
- resolved=field,
217
- scope="",
218
227
  type_=operation_type,
219
228
  field_name=field_name,
220
229
  root_type=root_type,
@@ -348,10 +357,12 @@ def graphql_cases(
348
357
  strategy = apply_to_all_dispatchers(operation, hook_context, hooks, strategy, "body").map(graphql.print_ast)
349
358
  body = draw(strategy)
350
359
 
351
- path_parameters_ = _generate_parameter("path", path_parameters, draw, operation, hook_context, hooks)
352
- headers_ = _generate_parameter("header", headers, draw, operation, hook_context, hooks)
353
- cookies_ = _generate_parameter("cookie", cookies, draw, operation, hook_context, hooks)
354
- query_ = _generate_parameter("query", query, draw, operation, hook_context, hooks)
360
+ path_parameters_ = _generate_parameter(
361
+ ParameterLocation.PATH, path_parameters, draw, operation, hook_context, hooks
362
+ )
363
+ headers_ = _generate_parameter(ParameterLocation.HEADER, headers, draw, operation, hook_context, hooks)
364
+ cookies_ = _generate_parameter(ParameterLocation.COOKIE, cookies, draw, operation, hook_context, hooks)
365
+ query_ = _generate_parameter(ParameterLocation.QUERY, query, draw, operation, hook_context, hooks)
355
366
 
356
367
  _phase_data = {
357
368
  TestPhase.EXAMPLES: ExamplesPhaseData(),
@@ -373,11 +384,11 @@ def graphql_cases(
373
384
  components={
374
385
  kind: ComponentInfo(mode=generation_mode)
375
386
  for kind, value in [
376
- (ComponentKind.QUERY, query_),
377
- (ComponentKind.PATH_PARAMETERS, path_parameters_),
378
- (ComponentKind.HEADERS, headers_),
379
- (ComponentKind.COOKIES, cookies_),
380
- (ComponentKind.BODY, body),
387
+ (ParameterLocation.QUERY, query_),
388
+ (ParameterLocation.PATH, path_parameters_),
389
+ (ParameterLocation.HEADER, headers_),
390
+ (ParameterLocation.COOKIE, cookies_),
391
+ (ParameterLocation.BODY, body),
381
392
  ]
382
393
  if value is not NOT_SET
383
394
  },
@@ -393,7 +404,7 @@ def graphql_cases(
393
404
 
394
405
 
395
406
  def _generate_parameter(
396
- location: str,
407
+ location: ParameterLocation,
397
408
  explicit: NotSet | dict[str, Any],
398
409
  draw: Callable,
399
410
  operation: APIOperation,
@@ -401,7 +412,7 @@ def _generate_parameter(
401
412
  hooks: HookDispatcher | None,
402
413
  ) -> Any:
403
414
  # Schemathesis does not generate anything but `body` for GraphQL, hence use `None`
404
- container = LOCATION_TO_CONTAINER[location]
415
+ container = location.container_name
405
416
  if isinstance(explicit, NotSet):
406
417
  strategy = apply_to_all_dispatchers(operation, context, hooks, st.none(), container)
407
418
  else:
@@ -1,9 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import time
4
- from contextlib import suppress
5
4
  from dataclasses import dataclass
6
- from typing import Any, Callable, Dict, Iterable, Optional, Union, cast
5
+ from typing import Any, Callable, Iterable, Optional, Union, cast
7
6
  from urllib.parse import quote_plus
8
7
 
9
8
  import jsonschema.protocols
@@ -16,12 +15,13 @@ from schemathesis.config import GenerationConfig
16
15
  from schemathesis.core import NOT_SET, media_types
17
16
  from schemathesis.core.control import SkipTest
18
17
  from schemathesis.core.errors import SERIALIZERS_SUGGESTION_MESSAGE, SerializationNotPossible
18
+ from schemathesis.core.jsonschema.types import JsonSchema
19
+ from schemathesis.core.parameters import ParameterLocation
19
20
  from schemathesis.core.transforms import deepclone
20
21
  from schemathesis.core.transport import prepare_urlencoded
21
22
  from schemathesis.generation.meta import (
22
23
  CaseMetadata,
23
24
  ComponentInfo,
24
- ComponentKind,
25
25
  ExamplesPhaseData,
26
26
  FuzzingPhaseData,
27
27
  GenerationInfo,
@@ -31,11 +31,11 @@ from schemathesis.generation.meta import (
31
31
  )
32
32
  from schemathesis.openapi.generation.filters import is_valid_header, is_valid_path, is_valid_query, is_valid_urlencoded
33
33
  from schemathesis.schemas import APIOperation
34
+ from schemathesis.specs.openapi.adapter.parameters import OpenApiBody, OpenApiParameterSet
34
35
 
35
36
  from ... import auths
36
37
  from ...generation import GenerationMode
37
38
  from ...hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
38
- from .constants import LOCATION_TO_CONTAINER
39
39
  from .formats import (
40
40
  DEFAULT_HEADER_EXCLUDE_CHARACTERS,
41
41
  HEADER_FORMAT,
@@ -46,12 +46,11 @@ from .formats import (
46
46
  from .media_types import MEDIA_TYPES
47
47
  from .negative import negative_schema
48
48
  from .negative.utils import can_negate
49
- from .parameters import OpenAPIBody, OpenAPIParameter, parameters_to_json_schema
50
- from .utils import is_header_location
51
49
 
52
50
  SLASH = "/"
53
51
  StrategyFactory = Callable[
54
- [Dict[str, Any], str, str, Optional[str], GenerationConfig, type[jsonschema.protocols.Validator]], st.SearchStrategy
52
+ [JsonSchema, str, ParameterLocation, Optional[str], GenerationConfig, type[jsonschema.protocols.Validator]],
53
+ st.SearchStrategy,
55
54
  ]
56
55
 
57
56
 
@@ -91,18 +90,24 @@ def openapi_cases(
91
90
  ctx = HookContext(operation=operation)
92
91
 
93
92
  path_parameters_ = generate_parameter(
94
- "path", path_parameters, operation, draw, ctx, hooks, generation_mode, generation_config
93
+ ParameterLocation.PATH, path_parameters, operation, draw, ctx, hooks, generation_mode, generation_config
94
+ )
95
+ headers_ = generate_parameter(
96
+ ParameterLocation.HEADER, headers, operation, draw, ctx, hooks, generation_mode, generation_config
97
+ )
98
+ cookies_ = generate_parameter(
99
+ ParameterLocation.COOKIE, cookies, operation, draw, ctx, hooks, generation_mode, generation_config
100
+ )
101
+ query_ = generate_parameter(
102
+ ParameterLocation.QUERY, query, operation, draw, ctx, hooks, generation_mode, generation_config
95
103
  )
96
- headers_ = generate_parameter("header", headers, operation, draw, ctx, hooks, generation_mode, generation_config)
97
- cookies_ = generate_parameter("cookie", cookies, operation, draw, ctx, hooks, generation_mode, generation_config)
98
- query_ = generate_parameter("query", query, operation, draw, ctx, hooks, generation_mode, generation_config)
99
104
 
100
105
  if body is NOT_SET:
101
106
  if operation.body:
102
107
  body_generator = generation_mode
103
108
  if generation_mode.is_negative:
104
109
  # Consider only schemas that are possible to negate
105
- candidates = [item for item in operation.body.items if can_negate(item.as_json_schema(operation))]
110
+ candidates = [item for item in operation.body.items if can_negate(item.optimized_schema)]
106
111
  # Not possible to negate body, fallback to positive data generation
107
112
  if not candidates:
108
113
  candidates = operation.body.items
@@ -112,7 +117,7 @@ def openapi_cases(
112
117
  candidates = operation.body.items
113
118
  parameter = draw(st.sampled_from(candidates))
114
119
  strategy = _get_body_strategy(parameter, strategy_factory, operation, generation_config)
115
- strategy = apply_hooks(operation, ctx, hooks, strategy, "body")
120
+ strategy = apply_hooks(operation, ctx, hooks, strategy, ParameterLocation.BODY)
116
121
  # Parameter may have a wildcard media type. In this case, choose any supported one
117
122
  possible_media_types = sorted(
118
123
  operation.schema.transport.get_matching_media_types(parameter.media_type), key=lambda x: x[0]
@@ -177,11 +182,11 @@ def openapi_cases(
177
182
  components={
178
183
  kind: ComponentInfo(mode=value.generator)
179
184
  for kind, value in [
180
- (ComponentKind.QUERY, query_),
181
- (ComponentKind.PATH_PARAMETERS, path_parameters_),
182
- (ComponentKind.HEADERS, headers_),
183
- (ComponentKind.COOKIES, cookies_),
184
- (ComponentKind.BODY, body_),
185
+ (ParameterLocation.QUERY, query_),
186
+ (ParameterLocation.PATH, path_parameters_),
187
+ (ParameterLocation.HEADER, headers_),
188
+ (ParameterLocation.COOKIE, cookies_),
189
+ (ParameterLocation.BODY, body_),
185
190
  ]
186
191
  if value.generator is not None
187
192
  },
@@ -196,7 +201,7 @@ def openapi_cases(
196
201
 
197
202
 
198
203
  def _get_body_strategy(
199
- parameter: OpenAPIBody,
204
+ parameter: OpenApiBody,
200
205
  strategy_factory: StrategyFactory,
201
206
  operation: APIOperation,
202
207
  generation_config: GenerationConfig,
@@ -205,11 +210,15 @@ def _get_body_strategy(
205
210
 
206
211
  if parameter.media_type in MEDIA_TYPES:
207
212
  return MEDIA_TYPES[parameter.media_type]
208
- schema = parameter.as_json_schema(operation)
209
- schema = operation.schema.prepare_schema(schema)
213
+ schema = parameter.optimized_schema
210
214
  assert isinstance(operation.schema, BaseOpenAPISchema)
211
215
  strategy = strategy_factory(
212
- schema, operation.label, "body", parameter.media_type, generation_config, operation.schema.validator_cls
216
+ schema,
217
+ operation.label,
218
+ ParameterLocation.BODY,
219
+ parameter.media_type,
220
+ generation_config,
221
+ operation.schema.adapter.jsonschema_validator_cls,
213
222
  )
214
223
  if not parameter.is_required:
215
224
  strategy |= st.just(NOT_SET)
@@ -218,7 +227,7 @@ def _get_body_strategy(
218
227
 
219
228
  def get_parameters_value(
220
229
  value: dict[str, Any] | None,
221
- location: str,
230
+ location: ParameterLocation,
222
231
  draw: Callable,
223
232
  operation: APIOperation,
224
233
  ctx: HookContext,
@@ -239,7 +248,7 @@ def get_parameters_value(
239
248
  strategy = apply_hooks(operation, ctx, hooks, strategy, location)
240
249
  new = draw(strategy)
241
250
  if new is not None:
242
- copied = deepclone(value)
251
+ copied = dict(value)
243
252
  copied.update(new)
244
253
  return copied
245
254
  return value
@@ -267,7 +276,7 @@ def any_negated_values(values: list[ValueContainer]) -> bool:
267
276
 
268
277
 
269
278
  def generate_parameter(
270
- location: str,
279
+ location: ParameterLocation,
271
280
  explicit: dict[str, Any] | None,
272
281
  operation: APIOperation,
273
282
  draw: Callable,
@@ -281,8 +290,8 @@ def generate_parameter(
281
290
  Fallback to positive data generator if parameter can not be negated.
282
291
  """
283
292
  if generator.is_negative and (
284
- (location == "path" and not can_negate_path_parameters(operation))
285
- or (is_header_location(location) and not can_negate_headers(operation, location))
293
+ (location == ParameterLocation.PATH and not can_negate_path_parameters(operation))
294
+ or (location.is_in_header and not can_negate_headers(operation, location))
286
295
  ):
287
296
  # If we can't negate any parameter, generate positive ones
288
297
  # If nothing else will be negated, then skip the test completely
@@ -295,7 +304,7 @@ def generate_parameter(
295
304
  if value == explicit:
296
305
  # When we pass `explicit`, then its parts are excluded from generation of the final value
297
306
  # If the final value is the same, then other parameters were generated at all
298
- if value is not None and location == "path":
307
+ if value is not None and location == ParameterLocation.PATH:
299
308
  value = quote_all(value)
300
309
  used_generator = None
301
310
  return ValueContainer(value=value, location=location, generator=used_generator)
@@ -303,51 +312,53 @@ def generate_parameter(
303
312
 
304
313
  def can_negate_path_parameters(operation: APIOperation) -> bool:
305
314
  """Check if any path parameter can be negated."""
306
- schema = parameters_to_json_schema(operation, operation.path_parameters)
307
315
  # No path parameters to negate
308
- parameters = schema["properties"]
316
+ parameters = cast(OpenApiParameterSet, operation.path_parameters).schema["properties"]
309
317
  if not parameters:
310
318
  return True
311
319
  return any(can_negate(parameter) for parameter in parameters.values())
312
320
 
313
321
 
314
- def can_negate_headers(operation: APIOperation, location: str) -> bool:
322
+ def can_negate_headers(operation: APIOperation, location: ParameterLocation) -> bool:
315
323
  """Check if any header can be negated."""
316
- parameters = getattr(operation, LOCATION_TO_CONTAINER[location])
317
- schema = parameters_to_json_schema(operation, parameters)
324
+ container = getattr(operation, location.container_name)
318
325
  # No headers to negate
319
- headers = schema["properties"]
326
+ headers = container.schema["properties"]
320
327
  if not headers:
321
328
  return True
322
329
  return any(header != {"type": "string"} for header in headers.values())
323
330
 
324
331
 
325
- def get_schema_for_location(
326
- operation: APIOperation, location: str, parameters: Iterable[OpenAPIParameter]
327
- ) -> dict[str, Any]:
328
- schema = parameters_to_json_schema(operation, parameters)
329
- if location == "path":
332
+ def get_schema_for_location(location: ParameterLocation, parameters: OpenApiParameterSet) -> dict[str, Any]:
333
+ schema = deepclone(parameters.schema)
334
+ if location == ParameterLocation.PATH:
330
335
  schema["required"] = list(schema["properties"])
331
- for prop in schema.get("properties", {}).values():
332
- if prop.get("type") == "string":
333
- prop.setdefault("minLength", 1)
334
- return operation.schema.prepare_schema(schema)
336
+ # Shallow copy properties dict itself and each modified property
337
+ properties = schema.get("properties", {})
338
+ if properties:
339
+ schema["properties"] = {
340
+ key: {**value, "minLength": value.get("minLength", 1)}
341
+ if value.get("type") == "string" and "minLength" not in value
342
+ else value
343
+ for key, value in properties.items()
344
+ }
345
+ return schema
335
346
 
336
347
 
337
348
  def get_parameters_strategy(
338
349
  operation: APIOperation,
339
350
  strategy_factory: StrategyFactory,
340
- location: str,
351
+ location: ParameterLocation,
341
352
  generation_config: GenerationConfig,
342
353
  exclude: Iterable[str] = (),
343
354
  ) -> st.SearchStrategy:
344
355
  """Create a new strategy for the case's component from the API operation parameters."""
345
356
  from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
346
357
 
347
- parameters = getattr(operation, LOCATION_TO_CONTAINER[location])
348
- if parameters:
349
- schema = get_schema_for_location(operation, location, parameters)
350
- if location == "header" and exclude:
358
+ container = getattr(operation, location.container_name)
359
+ if container:
360
+ schema = get_schema_for_location(location, container)
361
+ if location == ParameterLocation.HEADER and exclude:
351
362
  # Remove excluded headers case-insensitively
352
363
  exclude_lower = {name.lower() for name in exclude}
353
364
  schema["properties"] = {
@@ -357,37 +368,42 @@ def get_parameters_strategy(
357
368
  schema["required"] = [key for key in schema["required"] if key.lower() not in exclude_lower]
358
369
  elif exclude:
359
370
  # Non-header locations: remove by exact name
360
- for name in exclude:
361
- schema["properties"].pop(name, None)
362
- with suppress(ValueError):
363
- schema["required"].remove(name)
371
+ schema = dict(schema)
372
+ schema["properties"] = {key: value for key, value in schema["properties"].items() if key not in exclude}
373
+ if "required" in schema:
374
+ schema["required"] = [key for key in schema["required"] if key not in exclude]
364
375
  if not schema["properties"] and strategy_factory is make_negative_strategy:
365
376
  # Nothing to negate - all properties were excluded
366
377
  strategy = st.none()
367
378
  else:
368
379
  assert isinstance(operation.schema, BaseOpenAPISchema)
369
380
  strategy = strategy_factory(
370
- schema, operation.label, location, None, generation_config, operation.schema.validator_cls
381
+ schema,
382
+ operation.label,
383
+ location,
384
+ None,
385
+ generation_config,
386
+ operation.schema.adapter.jsonschema_validator_cls,
371
387
  )
372
388
  serialize = operation.get_parameter_serializer(location)
373
389
  if serialize is not None:
374
390
  strategy = strategy.map(serialize)
375
391
  filter_func = {
376
- "path": is_valid_path,
377
- "header": is_valid_header,
378
- "cookie": is_valid_header,
379
- "query": is_valid_query,
392
+ ParameterLocation.PATH: is_valid_path,
393
+ ParameterLocation.HEADER: is_valid_header,
394
+ ParameterLocation.COOKIE: is_valid_header,
395
+ ParameterLocation.QUERY: is_valid_query,
380
396
  }[location]
381
397
  # Headers with special format do not need filtration
382
- if not (is_header_location(location) and _can_skip_header_filter(schema)):
398
+ if not (location.is_in_header and _can_skip_header_filter(schema)):
383
399
  strategy = strategy.filter(filter_func)
384
400
  # Path & query parameters will be cast to string anyway, but having their JSON equivalents for
385
401
  # `True` / `False` / `None` improves chances of them passing validation in apps
386
402
  # that expect boolean / null types
387
403
  # and not aware of Python-specific representation of those types
388
- if location == "path":
404
+ if location == ParameterLocation.PATH:
389
405
  strategy = strategy.map(quote_all).map(jsonify_python_specific_types)
390
- elif location == "query":
406
+ elif location == ParameterLocation.QUERY:
391
407
  strategy = strategy.map(jsonify_python_specific_types)
392
408
  return strategy
393
409
  # No parameters defined for this location
@@ -431,15 +447,15 @@ def _build_custom_formats(generation_config: GenerationConfig) -> dict[str, st.S
431
447
 
432
448
 
433
449
  def make_positive_strategy(
434
- schema: dict[str, Any],
450
+ schema: JsonSchema,
435
451
  operation_name: str,
436
- location: str,
452
+ location: ParameterLocation,
437
453
  media_type: str | None,
438
454
  generation_config: GenerationConfig,
439
455
  validator_cls: type[jsonschema.protocols.Validator],
440
456
  ) -> st.SearchStrategy:
441
457
  """Strategy for generating values that fit the schema."""
442
- if is_header_location(location):
458
+ if location.is_in_header and isinstance(schema, dict):
443
459
  # We try to enforce the right header values via "format"
444
460
  # This way, only allowed values will be used during data generation, which reduces the amount of filtering later
445
461
  # If a property schema contains `pattern` it leads to heavy filtering and worse performance - therefore, skip it
@@ -461,9 +477,9 @@ def _can_skip_header_filter(schema: dict[str, Any]) -> bool:
461
477
 
462
478
 
463
479
  def make_negative_strategy(
464
- schema: dict[str, Any],
480
+ schema: JsonSchema,
465
481
  operation_name: str,
466
- location: str,
482
+ location: ParameterLocation,
467
483
  media_type: str | None,
468
484
  generation_config: GenerationConfig,
469
485
  validator_cls: type[jsonschema.protocols.Validator],
@@ -510,8 +526,7 @@ def apply_hooks(
510
526
  ctx: HookContext,
511
527
  hooks: HookDispatcher | None,
512
528
  strategy: st.SearchStrategy,
513
- location: str,
529
+ location: ParameterLocation,
514
530
  ) -> st.SearchStrategy:
515
531
  """Apply all hooks related to the given location."""
516
- container = LOCATION_TO_CONTAINER[location]
517
- return apply_to_all_dispatchers(operation, ctx, hooks, strategy, container)
532
+ return apply_to_all_dispatchers(operation, ctx, hooks, strategy, location.container_name)
@@ -0,0 +1,10 @@
1
+ from schemathesis.specs.openapi.adapter import v2, v3_0, v3_1
2
+ from schemathesis.specs.openapi.adapter.responses import OpenApiResponse, OpenApiResponses
3
+
4
+ __all__ = [
5
+ "OpenApiResponse",
6
+ "OpenApiResponses",
7
+ "v2",
8
+ "v3_0",
9
+ "v3_1",
10
+ ]