schemathesis 4.1.4__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 +87 -11
  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.4.dist-info → schemathesis-4.2.0.dist-info}/METADATA +2 -2
  65. {schemathesis-4.1.4.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.4.dist-info → schemathesis-4.2.0.dist-info}/WHEEL +0 -0
  69. {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/entry_points.txt +0 -0
  70. {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -21,8 +21,15 @@ from schemathesis import auths
21
21
  from schemathesis.auths import AuthStorage, AuthStorageMark
22
22
  from schemathesis.config import GenerationConfig, ProjectConfig
23
23
  from schemathesis.core import INJECTED_PATH_PARAMETER_KEY, NOT_SET, NotSet, SpecificationFeature, media_types
24
- from schemathesis.core.errors import InvalidSchema, SerializationNotPossible
24
+ from schemathesis.core.errors import (
25
+ InfiniteRecursiveReference,
26
+ InvalidSchema,
27
+ MalformedMediaType,
28
+ SerializationNotPossible,
29
+ UnresolvableReference,
30
+ )
25
31
  from schemathesis.core.marks import Mark
32
+ from schemathesis.core.parameters import LOCATION_TO_CONTAINER, ParameterLocation
26
33
  from schemathesis.core.transforms import deepclone
27
34
  from schemathesis.core.transport import prepare_urlencoded
28
35
  from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
@@ -34,7 +41,6 @@ from schemathesis.generation.hypothesis.given import GivenInput
34
41
  from schemathesis.generation.meta import (
35
42
  CaseMetadata,
36
43
  ComponentInfo,
37
- ComponentKind,
38
44
  CoveragePhaseData,
39
45
  GenerationInfo,
40
46
  PhaseInfo,
@@ -263,16 +269,15 @@ def generate_example_cases(
263
269
  **kwargs: Any,
264
270
  ) -> Generator[Case]:
265
271
  """Add examples to the Hypothesis test, if they are specified in the schema."""
266
- from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
267
-
268
272
  try:
269
273
  result: list[Case] = [
270
274
  examples.generate_one(strategy) for strategy in operation.get_strategies_from_examples(**kwargs)
271
275
  ]
272
276
  except (
273
277
  InvalidSchema,
274
- HypothesisRefResolutionError,
278
+ InfiniteRecursiveReference,
275
279
  Unsatisfiable,
280
+ UnresolvableReference,
276
281
  SerializationNotPossible,
277
282
  SchemaError,
278
283
  ) as exc:
@@ -283,6 +288,10 @@ def generate_example_cases(
283
288
  NonSerializableMark.set(test, exc)
284
289
  if isinstance(exc, SchemaError):
285
290
  InvalidRegexMark.set(test, exc)
291
+ if isinstance(exc, InfiniteRecursiveReference):
292
+ InfiniteRecursiveReferenceMark.set(test, exc)
293
+ if isinstance(exc, UnresolvableReference):
294
+ UnresolvableReferenceMark.set(test, exc)
286
295
 
287
296
  if fill_missing and not result:
288
297
  strategy = operation.as_strategy()
@@ -347,7 +356,7 @@ def generate_coverage_cases(
347
356
  unexpected_methods: set[str],
348
357
  generation_config: GenerationConfig,
349
358
  ) -> Generator[Case]:
350
- from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
359
+ from schemathesis.core.parameters import LOCATION_TO_CONTAINER
351
360
 
352
361
  auth_context = auths.AuthContext(
353
362
  operation=operation,
@@ -397,7 +406,7 @@ class Template:
397
406
  __slots__ = ("_components", "_template", "_serializers")
398
407
 
399
408
  def __init__(self, serializers: dict[str, Callable]) -> None:
400
- self._components: dict[ComponentKind, ComponentInfo] = {}
409
+ self._components: dict[ParameterLocation, ComponentInfo] = {}
401
410
  self._template: dict[str, Any] = {}
402
411
  self._serializers = serializers
403
412
 
@@ -410,24 +419,20 @@ class Template:
410
419
  def get(self, key: str, default: Any = None) -> dict:
411
420
  return self._template.get(key, default)
412
421
 
413
- def add_parameter(self, location: str, name: str, value: coverage.GeneratedValue) -> None:
414
- from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
415
-
416
- component_name = LOCATION_TO_CONTAINER[location]
417
- kind = ComponentKind(component_name)
418
- info = self._components.get(kind)
422
+ def add_parameter(self, location: ParameterLocation, name: str, value: coverage.GeneratedValue) -> None:
423
+ info = self._components.get(location)
419
424
  if info is None:
420
- self._components[kind] = ComponentInfo(mode=value.generation_mode)
425
+ self._components[location] = ComponentInfo(mode=value.generation_mode)
421
426
  elif value.generation_mode == GenerationMode.NEGATIVE:
422
427
  info.mode = GenerationMode.NEGATIVE
423
428
 
424
- container = self._template.setdefault(component_name, {})
429
+ container = self._template.setdefault(location.container_name, {})
425
430
  container[name] = value.value
426
431
 
427
432
  def set_body(self, body: coverage.GeneratedValue, media_type: str) -> None:
428
433
  self._template["body"] = body.value
429
434
  self._template["media_type"] = media_type
430
- self._components[ComponentKind.BODY] = ComponentInfo(mode=body.generation_mode)
435
+ self._components[ParameterLocation.BODY] = ComponentInfo(mode=body.generation_mode)
431
436
 
432
437
  def _serialize(self, kwargs: dict[str, Any]) -> dict[str, Any]:
433
438
  from schemathesis.specs.openapi._hypothesis import quote_all
@@ -454,21 +459,24 @@ class Template:
454
459
  def with_body(self, *, media_type: str, value: coverage.GeneratedValue) -> TemplateValue:
455
460
  kwargs = {**self._template, "media_type": media_type, "body": value.value}
456
461
  kwargs = self._serialize(kwargs)
457
- components = {**self._components, ComponentKind.BODY: ComponentInfo(mode=value.generation_mode)}
462
+ components = {**self._components, ParameterLocation.BODY: ComponentInfo(mode=value.generation_mode)}
458
463
  return TemplateValue(kwargs=kwargs, components=components)
459
464
 
460
- def with_parameter(self, *, location: str, name: str, value: coverage.GeneratedValue) -> TemplateValue:
461
- from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
462
-
463
- container_name = LOCATION_TO_CONTAINER[location]
464
- container = self._template[container_name]
465
- return self.with_container(
466
- container_name=container_name, value={**container, name: value.value}, generation_mode=value.generation_mode
465
+ def with_parameter(
466
+ self, *, location: ParameterLocation, name: str, value: coverage.GeneratedValue
467
+ ) -> TemplateValue:
468
+ container = self._template[location.container_name]
469
+ return self.with_location(
470
+ location=location,
471
+ value={**container, name: value.value},
472
+ generation_mode=value.generation_mode,
467
473
  )
468
474
 
469
- def with_container(self, *, container_name: str, value: Any, generation_mode: GenerationMode) -> TemplateValue:
470
- kwargs = {**self._template, container_name: value}
471
- components = {**self._components, ComponentKind(container_name): ComponentInfo(mode=generation_mode)}
475
+ def with_location(
476
+ self, *, location: ParameterLocation, value: Any, generation_mode: GenerationMode
477
+ ) -> TemplateValue:
478
+ kwargs = {**self._template, location.container_name: value}
479
+ components = {**self._components, location: ComponentInfo(mode=generation_mode)}
472
480
  kwargs = self._serialize(kwargs)
473
481
  return TemplateValue(kwargs=kwargs, components=components)
474
482
 
@@ -476,7 +484,7 @@ class Template:
476
484
  @dataclass
477
485
  class TemplateValue:
478
486
  kwargs: dict[str, Any]
479
- components: dict[ComponentKind, ComponentInfo]
487
+ components: dict[ParameterLocation, ComponentInfo]
480
488
 
481
489
  __slots__ = ("kwargs", "components")
482
490
 
@@ -510,17 +518,17 @@ def _iter_coverage_cases(
510
518
  generation_config: GenerationConfig,
511
519
  ) -> Generator[Case, None, None]:
512
520
  from schemathesis.specs.openapi._hypothesis import _build_custom_formats
513
- from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
514
- from schemathesis.specs.openapi.examples import find_in_responses, find_matching_in_responses
521
+ from schemathesis.specs.openapi.examples import find_matching_in_responses
522
+ from schemathesis.specs.openapi.media_types import MEDIA_TYPES
515
523
  from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
516
524
  from schemathesis.specs.openapi.serialization import get_serializers_for_operation
517
525
 
518
- generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
526
+ generators: dict[tuple[ParameterLocation, str], Generator[coverage.GeneratedValue, None, None]] = {}
519
527
  serializers = get_serializers_for_operation(operation)
520
528
  template = Template(serializers)
521
529
 
522
530
  instant = Instant()
523
- responses = find_in_responses(operation)
531
+ responses = list(operation.responses.iter_examples())
524
532
  # NOTE: The HEAD method is excluded
525
533
  unexpected_methods = unexpected_methods or {"get", "put", "post", "delete", "options", "patch", "trace"}
526
534
  custom_formats = _build_custom_formats(generation_config)
@@ -528,17 +536,23 @@ def _iter_coverage_cases(
528
536
  seen_negative = coverage.HashSet()
529
537
  seen_positive = coverage.HashSet()
530
538
  assert isinstance(operation.schema, BaseOpenAPISchema)
531
- validator_cls = operation.schema.validator_cls
539
+ validator_cls = operation.schema.adapter.jsonschema_validator_cls
532
540
 
533
541
  for parameter in operation.iter_parameters():
534
542
  location = parameter.location
535
543
  name = parameter.name
536
- schema = parameter.as_json_schema(operation, update_quantifiers=False)
544
+ schema = parameter.unoptimized_schema
545
+ examples = parameter.examples
546
+ if examples:
547
+ schema = dict(schema)
548
+ schema["examples"] = examples
537
549
  for value in find_matching_in_responses(responses, parameter.name):
538
550
  schema.setdefault("examples", []).append(value)
539
551
  gen = coverage.cover_schema_iter(
540
552
  coverage.CoverageContext(
553
+ root_schema=schema,
541
554
  location=location,
555
+ media_type=None,
542
556
  generation_modes=generation_modes,
543
557
  is_required=parameter.is_required,
544
558
  custom_formats=custom_formats,
@@ -548,6 +562,27 @@ def _iter_coverage_cases(
548
562
  )
549
563
  value = next(gen, NOT_SET)
550
564
  if isinstance(value, NotSet):
565
+ if location == ParameterLocation.PATH:
566
+ # Can't skip path parameters - they should be filled
567
+ schema = dict(schema)
568
+ schema.setdefault("type", "string")
569
+ schema.setdefault("minLength", 1)
570
+ gen = coverage.cover_schema_iter(
571
+ coverage.CoverageContext(
572
+ root_schema=schema,
573
+ location=location,
574
+ media_type=None,
575
+ generation_modes=[GenerationMode.POSITIVE],
576
+ is_required=parameter.is_required,
577
+ custom_formats=custom_formats,
578
+ validator_cls=validator_cls,
579
+ ),
580
+ schema,
581
+ )
582
+ value = next(gen, NOT_SET)
583
+ assert not isinstance(value, NotSet), f"It should always be possible: {schema!r}"
584
+ template.add_parameter(location, name, value)
585
+ continue
551
586
  continue
552
587
  template.add_parameter(location, name, value)
553
588
  generators[(location, name)] = gen
@@ -555,15 +590,24 @@ def _iter_coverage_cases(
555
590
  if operation.body:
556
591
  for body in operation.body:
557
592
  instant = Instant()
558
- schema = body.as_json_schema(operation, update_quantifiers=False)
559
- # Definition could be a list for Open API 2.0
560
- definition = body.definition if isinstance(body.definition, dict) else {}
561
- examples = [example["value"] for example in definition.get("examples", {}).values() if "value" in example]
593
+ schema = body.unoptimized_schema
594
+ examples = body.examples
562
595
  if examples:
563
- schema.setdefault("examples", []).extend(examples)
596
+ schema = dict(schema)
597
+ # User-registered media types should only handle text / binary data
598
+ if body.media_type in MEDIA_TYPES:
599
+ schema["examples"] = [example for example in examples if isinstance(example, (str, bytes))]
600
+ else:
601
+ schema["examples"] = examples
602
+ try:
603
+ media_type = media_types.parse(body.media_type)
604
+ except MalformedMediaType:
605
+ media_type = None
564
606
  gen = coverage.cover_schema_iter(
565
607
  coverage.CoverageContext(
566
- location="body",
608
+ root_schema=schema,
609
+ location=ParameterLocation.BODY,
610
+ media_type=media_type,
567
611
  generation_modes=generation_modes,
568
612
  is_required=body.is_required,
569
613
  custom_formats=custom_formats,
@@ -591,7 +635,7 @@ def _iter_coverage_cases(
591
635
  description=value.description,
592
636
  location=value.location,
593
637
  parameter=body.media_type,
594
- parameter_location="body",
638
+ parameter_location=ParameterLocation.BODY,
595
639
  ),
596
640
  ),
597
641
  )
@@ -613,7 +657,7 @@ def _iter_coverage_cases(
613
657
  description=next_value.description,
614
658
  location=next_value.location,
615
659
  parameter=body.media_type,
616
- parameter_location="body",
660
+ parameter_location=ParameterLocation.BODY,
617
661
  ),
618
662
  ),
619
663
  )
@@ -689,8 +733,8 @@ def _iter_coverage_cases(
689
733
  # I.e. contains just `default` value without any other keywords
690
734
  value = container.get(parameter.name, NOT_SET)
691
735
  if value is not NOT_SET:
692
- data = template.with_container(
693
- container_name="query",
736
+ data = template.with_location(
737
+ location=ParameterLocation.QUERY,
694
738
  value={**container, parameter.name: [value, value]},
695
739
  generation_mode=GenerationMode.NEGATIVE,
696
740
  )
@@ -702,20 +746,19 @@ def _iter_coverage_cases(
702
746
  phase=PhaseInfo.coverage(
703
747
  description=f"Duplicate `{parameter.name}` query parameter",
704
748
  parameter=parameter.name,
705
- parameter_location="query",
749
+ parameter_location=ParameterLocation.QUERY,
706
750
  ),
707
751
  ),
708
752
  )
709
753
  # Generate missing required parameters
710
754
  for parameter in operation.iter_parameters():
711
- if parameter.is_required and parameter.location != "path":
755
+ if parameter.is_required and parameter.location != ParameterLocation.PATH:
712
756
  instant = Instant()
713
757
  name = parameter.name
714
758
  location = parameter.location
715
- container_name = LOCATION_TO_CONTAINER[location]
716
- container = template.get(container_name, {})
717
- data = template.with_container(
718
- container_name=container_name,
759
+ container = template.get(location.container_name, {})
760
+ data = template.with_location(
761
+ location=location,
719
762
  value={k: v for k, v in container.items() if k != name},
720
763
  generation_mode=GenerationMode.NEGATIVE,
721
764
  )
@@ -727,7 +770,7 @@ def _iter_coverage_cases(
727
770
  generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
728
771
  components=data.components,
729
772
  phase=PhaseInfo.coverage(
730
- description=f"Missing `{name}` at {location}",
773
+ description=f"Missing `{name}` at {location.value}",
731
774
  parameter=name,
732
775
  parameter_location=location,
733
776
  ),
@@ -735,14 +778,14 @@ def _iter_coverage_cases(
735
778
  )
736
779
  # Generate combinations for each location
737
780
  for location, parameter_set in [
738
- ("query", operation.query),
739
- ("header", operation.headers),
740
- ("cookie", operation.cookies),
781
+ (ParameterLocation.QUERY, operation.query),
782
+ (ParameterLocation.HEADER, operation.headers),
783
+ (ParameterLocation.COOKIE, operation.cookies),
741
784
  ]:
742
785
  if not parameter_set:
743
786
  continue
744
787
 
745
- container_name = LOCATION_TO_CONTAINER[location]
788
+ container_name = location.container_name
746
789
  base_container = template.get(container_name, {})
747
790
 
748
791
  # Get required and optional parameters
@@ -754,15 +797,12 @@ def _iter_coverage_cases(
754
797
  def make_case(
755
798
  container_values: dict,
756
799
  description: str,
757
- _location: str,
758
- _container_name: str,
800
+ _location: ParameterLocation,
759
801
  _parameter: str | None,
760
802
  _generation_mode: GenerationMode,
761
803
  _instant: Instant,
762
804
  ) -> Case:
763
- data = template.with_container(
764
- container_name=_container_name, value=container_values, generation_mode=_generation_mode
765
- )
805
+ data = template.with_location(location=_location, value=container_values, generation_mode=_generation_mode)
766
806
  return operation.Case(
767
807
  **data.kwargs,
768
808
  _meta=CaseMetadata(
@@ -784,7 +824,7 @@ def _iter_coverage_cases(
784
824
  ) -> dict[str, Any]:
785
825
  return {
786
826
  "properties": {
787
- parameter.name: parameter.as_json_schema(operation)
827
+ parameter.name: parameter.optimized_schema
788
828
  for parameter in _parameter_set
789
829
  if parameter.name in combination
790
830
  },
@@ -793,12 +833,14 @@ def _iter_coverage_cases(
793
833
  }
794
834
 
795
835
  def _yield_negative(
796
- subschema: dict[str, Any], _location: str, _container_name: str, is_required: bool
836
+ subschema: dict[str, Any], _location: ParameterLocation, is_required: bool
797
837
  ) -> Generator[Case, None, None]:
798
838
  iterator = iter(
799
839
  coverage.cover_schema_iter(
800
840
  coverage.CoverageContext(
841
+ root_schema=subschema,
801
842
  location=_location,
843
+ media_type=None,
802
844
  generation_modes=[GenerationMode.NEGATIVE],
803
845
  is_required=is_required,
804
846
  custom_formats=custom_formats,
@@ -815,7 +857,6 @@ def _iter_coverage_cases(
815
857
  more.value,
816
858
  more.description,
817
859
  _location,
818
- _container_name,
819
860
  more.parameter,
820
861
  GenerationMode.NEGATIVE,
821
862
  instant,
@@ -831,14 +872,13 @@ def _iter_coverage_cases(
831
872
  only_required,
832
873
  "Only required properties",
833
874
  location,
834
- container_name,
835
875
  None,
836
876
  GenerationMode.POSITIVE,
837
877
  Instant(),
838
878
  )
839
879
  if GenerationMode.NEGATIVE in generation_modes:
840
880
  subschema = _combination_schema(only_required, required, parameter_set)
841
- for case in _yield_negative(subschema, location, container_name, is_required=bool(required)):
881
+ for case in _yield_negative(subschema, location, is_required=bool(required)):
842
882
  kwargs = _case_to_kwargs(case)
843
883
  if not seen_negative.insert(kwargs):
844
884
  continue
@@ -858,14 +898,13 @@ def _iter_coverage_cases(
858
898
  combo,
859
899
  f"All required properties and optional '{opt_param}'",
860
900
  location,
861
- container_name,
862
901
  None,
863
902
  GenerationMode.POSITIVE,
864
903
  Instant(),
865
904
  )
866
905
  if GenerationMode.NEGATIVE in generation_modes:
867
906
  subschema = _combination_schema(combo, required, parameter_set)
868
- for case in _yield_negative(subschema, location, container_name, is_required=bool(required)):
907
+ for case in _yield_negative(subschema, location, is_required=bool(required)):
869
908
  assert case.meta is not None
870
909
  assert isinstance(case.meta.phase.data, CoveragePhaseData)
871
910
  # Already generated in one of the blocks above
@@ -884,7 +923,6 @@ def _iter_coverage_cases(
884
923
  combo,
885
924
  f"All required and {size} optional properties",
886
925
  location,
887
- container_name,
888
926
  None,
889
927
  GenerationMode.POSITIVE,
890
928
  Instant(),
@@ -892,8 +930,6 @@ def _iter_coverage_cases(
892
930
 
893
931
 
894
932
  def _case_to_kwargs(case: Case) -> dict:
895
- from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
896
-
897
933
  kwargs = {}
898
934
  for container_name in LOCATION_TO_CONTAINER.values():
899
935
  value = getattr(case, container_name)
@@ -915,3 +951,5 @@ NonSerializableMark = Mark[SerializationNotPossible](attr_name="non_serializable
915
951
  InvalidRegexMark = Mark[SchemaError](attr_name="invalid_regex")
916
952
  InvalidHeadersExampleMark = Mark[dict[str, str]](attr_name="invalid_example_header")
917
953
  MissingPathParameters = Mark[InvalidSchema](attr_name="missing_path_parameters")
954
+ InfiniteRecursiveReferenceMark = Mark[InfiniteRecursiveReference](attr_name="infinite_recursive_reference")
955
+ UnresolvableReferenceMark = Mark[UnresolvableReference](attr_name="unresolvable_reference")
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from enum import Enum
5
5
 
6
+ from schemathesis.core.parameters import ParameterLocation
6
7
  from schemathesis.generation import GenerationMode
7
8
 
8
9
 
@@ -15,16 +16,6 @@ class TestPhase(str, Enum):
15
16
  STATEFUL = "stateful"
16
17
 
17
18
 
18
- class ComponentKind(str, Enum):
19
- """Components that can be generated."""
20
-
21
- QUERY = "query"
22
- PATH_PARAMETERS = "path_parameters"
23
- HEADERS = "headers"
24
- COOKIES = "cookies"
25
- BODY = "body"
26
-
27
-
28
19
  @dataclass
29
20
  class ComponentInfo:
30
21
  """Information about how a specific component was generated."""
@@ -62,7 +53,7 @@ class CoveragePhaseData:
62
53
  description: str
63
54
  location: str | None
64
55
  parameter: str | None
65
- parameter_location: str | None
56
+ parameter_location: ParameterLocation | None
66
57
 
67
58
  __slots__ = ("description", "location", "parameter", "parameter_location")
68
59
 
@@ -82,7 +73,7 @@ class PhaseInfo:
82
73
  description: str,
83
74
  location: str | None = None,
84
75
  parameter: str | None = None,
85
- parameter_location: str | None = None,
76
+ parameter_location: ParameterLocation | None = None,
86
77
  ) -> PhaseInfo:
87
78
  return cls(
88
79
  name=TestPhase.COVERAGE,
@@ -111,7 +102,7 @@ class CaseMetadata:
111
102
  """Complete metadata for generated cases."""
112
103
 
113
104
  generation: GenerationInfo
114
- components: dict[ComponentKind, ComponentInfo]
105
+ components: dict[ParameterLocation, ComponentInfo]
115
106
  phase: PhaseInfo
116
107
 
117
108
  __slots__ = ("generation", "components", "phase")
@@ -119,7 +110,7 @@ class CaseMetadata:
119
110
  def __init__(
120
111
  self,
121
112
  generation: GenerationInfo,
122
- components: dict[ComponentKind, ComponentInfo],
113
+ components: dict[ParameterLocation, ComponentInfo],
123
114
  phase: PhaseInfo,
124
115
  ) -> None:
125
116
  self.generation = generation
@@ -5,12 +5,12 @@ from dataclasses import dataclass
5
5
  from typing import TYPE_CHECKING, Any, Iterator
6
6
 
7
7
  from schemathesis.config import ProjectConfig
8
+ from schemathesis.core.parameters import ParameterLocation
8
9
  from schemathesis.core.transforms import diff
9
- from schemathesis.generation.meta import ComponentKind
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from schemathesis.generation.case import Case
13
- from schemathesis.schemas import APIOperation, Parameter
13
+ from schemathesis.schemas import APIOperation, OperationParameter
14
14
 
15
15
 
16
16
  @dataclass
@@ -24,21 +24,21 @@ class Override:
24
24
 
25
25
  __slots__ = ("query", "headers", "cookies", "path_parameters")
26
26
 
27
- def items(self) -> Iterator[tuple[str, dict[str, str]]]:
27
+ def items(self) -> Iterator[tuple[ParameterLocation, dict[str, str]]]:
28
28
  for key, value in (
29
- ("query", self.query),
30
- ("headers", self.headers),
31
- ("cookies", self.cookies),
32
- ("path_parameters", self.path_parameters),
29
+ (ParameterLocation.QUERY, self.query),
30
+ (ParameterLocation.HEADER, self.headers),
31
+ (ParameterLocation.COOKIE, self.cookies),
32
+ (ParameterLocation.PATH, self.path_parameters),
33
33
  ):
34
34
  if value:
35
35
  yield key, value
36
36
 
37
37
  @classmethod
38
- def from_components(cls, components: dict[ComponentKind, StoredValue], case: Case) -> Override:
38
+ def from_components(cls, components: dict[ParameterLocation, StoredValue], case: Case) -> Override:
39
39
  return Override(
40
40
  **{
41
- kind.value: get_component_diff(stored=stored, current=getattr(case, kind.value))
41
+ kind.container_name: get_component_diff(stored=stored, current=getattr(case, kind.container_name))
42
42
  for kind, stored in components.items()
43
43
  }
44
44
  )
@@ -69,9 +69,9 @@ def for_operation(config: ProjectConfig, *, operation: APIOperation) -> Override
69
69
  return output
70
70
 
71
71
 
72
- def _get_override_value(param: Parameter, parameters: dict[str, Any]) -> Any:
72
+ def _get_override_value(param: OperationParameter, parameters: dict[str, Any]) -> Any:
73
73
  key = param.name
74
- full_key = f"{param.location}.{param.name}"
74
+ full_key = f"{param.location.value}.{param.name}"
75
75
  if key in parameters:
76
76
  return parameters[key]
77
77
  elif full_key in parameters:
@@ -102,17 +102,17 @@ def get_component_diff(stored: StoredValue, current: dict[str, Any] | None) -> d
102
102
  return current
103
103
 
104
104
 
105
- def store_components(case: Case) -> dict[ComponentKind, StoredValue]:
105
+ def store_components(case: Case) -> dict[ParameterLocation, StoredValue]:
106
106
  """Store original component states for a test case."""
107
107
  return {
108
108
  kind: StoredValue(
109
- value=store_original_state(getattr(case, kind.value)),
109
+ value=store_original_state(getattr(case, kind.container_name)),
110
110
  is_generated=bool(case.meta and kind in case.meta.components),
111
111
  )
112
112
  for kind in [
113
- ComponentKind.QUERY,
114
- ComponentKind.HEADERS,
115
- ComponentKind.COOKIES,
116
- ComponentKind.PATH_PARAMETERS,
113
+ ParameterLocation.QUERY,
114
+ ParameterLocation.HEADER,
115
+ ParameterLocation.COOKIE,
116
+ ParameterLocation.PATH,
117
117
  ]
118
118
  }
@@ -187,7 +187,7 @@ class LazySchema:
187
187
  def as_strategy_kwargs(_operation: APIOperation) -> dict[str, Any]:
188
188
  override = overrides.for_operation(config=schema.config, operation=_operation)
189
189
 
190
- return {location: entry for location, entry in override.items() if entry}
190
+ return {location.container_name: entry for location, entry in override.items() if entry}
191
191
 
192
192
  tests = list(
193
193
  get_all_tests(
@@ -14,7 +14,6 @@ from jsonschema.exceptions import SchemaError
14
14
 
15
15
  from schemathesis.core.control import SkipTest
16
16
  from schemathesis.core.errors import (
17
- RECURSIVE_REFERENCE_ERROR_MESSAGE,
18
17
  SERIALIZERS_SUGGESTION_MESSAGE,
19
18
  IncorrectUsage,
20
19
  InvalidHeadersExample,
@@ -147,7 +146,7 @@ class SchemathesisCase(PyCollector):
147
146
  if override is not None:
148
147
  for location, entry in override.items():
149
148
  if entry:
150
- as_strategy_kwargs[location] = entry
149
+ as_strategy_kwargs[location.container_name] = entry
151
150
  modes = []
152
151
  phases = self.schema.config.phases_for(operation=operation)
153
152
  if phases.examples.enabled:
@@ -293,8 +292,6 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
293
292
 
294
293
  For example - kwargs validation is failed for some strategy.
295
294
  """
296
- from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
297
-
298
295
  from schemathesis.generation.hypothesis.builder import (
299
296
  InvalidHeadersExampleMark,
300
297
  InvalidRegexMark,
@@ -312,8 +309,6 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
312
309
  if "Inconsistent args" in str(exc) and "@example()" in str(exc):
313
310
  raise IncorrectUsage(GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE) from None
314
311
  raise InvalidSchema(exc.args[0]) from None
315
- except HypothesisRefResolutionError:
316
- pytest.skip(RECURSIVE_REFERENCE_ERROR_MESSAGE)
317
312
  except (SkipTest, unittest.SkipTest) as exc:
318
313
  if UnsatisfiableExampleMark.is_set(pyfuncitem.obj):
319
314
  raise Unsatisfiable("Failed to generate test cases from examples for this API operation") from None