schemathesis 4.0.0a12__py3-none-any.whl → 4.0.1__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 (41) hide show
  1. schemathesis/__init__.py +9 -4
  2. schemathesis/auths.py +20 -30
  3. schemathesis/checks.py +5 -0
  4. schemathesis/cli/commands/run/__init__.py +9 -6
  5. schemathesis/cli/commands/run/handlers/output.py +13 -0
  6. schemathesis/cli/constants.py +1 -1
  7. schemathesis/config/_operations.py +16 -21
  8. schemathesis/config/_projects.py +5 -1
  9. schemathesis/core/errors.py +10 -17
  10. schemathesis/core/transport.py +81 -1
  11. schemathesis/engine/errors.py +1 -1
  12. schemathesis/generation/case.py +152 -28
  13. schemathesis/generation/hypothesis/builder.py +12 -12
  14. schemathesis/generation/overrides.py +11 -27
  15. schemathesis/generation/stateful/__init__.py +13 -0
  16. schemathesis/generation/stateful/state_machine.py +31 -108
  17. schemathesis/graphql/loaders.py +14 -4
  18. schemathesis/hooks.py +1 -4
  19. schemathesis/openapi/checks.py +82 -20
  20. schemathesis/openapi/generation/filters.py +9 -2
  21. schemathesis/openapi/loaders.py +14 -4
  22. schemathesis/pytest/lazy.py +4 -31
  23. schemathesis/pytest/plugin.py +21 -11
  24. schemathesis/schemas.py +153 -89
  25. schemathesis/specs/graphql/schemas.py +6 -6
  26. schemathesis/specs/openapi/_hypothesis.py +39 -14
  27. schemathesis/specs/openapi/checks.py +95 -34
  28. schemathesis/specs/openapi/expressions/nodes.py +1 -1
  29. schemathesis/specs/openapi/negative/__init__.py +5 -3
  30. schemathesis/specs/openapi/negative/mutations.py +2 -2
  31. schemathesis/specs/openapi/parameters.py +0 -3
  32. schemathesis/specs/openapi/schemas.py +6 -91
  33. schemathesis/specs/openapi/stateful/links.py +1 -63
  34. schemathesis/transport/requests.py +12 -1
  35. schemathesis/transport/serialization.py +0 -4
  36. schemathesis/transport/wsgi.py +7 -0
  37. {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/METADATA +8 -10
  38. {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/RECORD +41 -41
  39. {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/WHEEL +0 -0
  40. {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/entry_points.txt +0 -0
  41. {schemathesis-4.0.0a12.dist-info → schemathesis-4.0.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/schemas.py CHANGED
@@ -40,8 +40,11 @@ from .filters import (
40
40
  from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope, dispatch, to_filterable_hook
41
41
 
42
42
  if TYPE_CHECKING:
43
+ import httpx
44
+ import requests
43
45
  from hypothesis.strategies import SearchStrategy
44
- from typing_extensions import Self
46
+ from requests.structures import CaseInsensitiveDict
47
+ from werkzeug.test import TestResponse
45
48
 
46
49
  from schemathesis.core import Specification
47
50
  from schemathesis.generation.stateful.state_machine import APIStateMachine
@@ -136,7 +139,25 @@ class BaseSchema(Mapping):
136
139
  operation_id: FilterValue | None = None,
137
140
  operation_id_regex: RegexValue | None = None,
138
141
  ) -> BaseSchema:
139
- """Include only operations that match the given filters."""
142
+ """Return a new schema containing only operations matching the specified criteria.
143
+
144
+ Args:
145
+ func: Custom filter function that accepts operation context.
146
+ name: Operation name(s) to include.
147
+ name_regex: Regex pattern for operation names.
148
+ method: HTTP method(s) to include.
149
+ method_regex: Regex pattern for HTTP methods.
150
+ path: API path(s) to include.
151
+ path_regex: Regex pattern for API paths.
152
+ tag: OpenAPI tag(s) to include.
153
+ tag_regex: Regex pattern for OpenAPI tags.
154
+ operation_id: Operation ID(s) to include.
155
+ operation_id_regex: Regex pattern for operation IDs.
156
+
157
+ Returns:
158
+ New schema instance with applied include filters.
159
+
160
+ """
140
161
  filter_set = self.filter_set.clone()
141
162
  filter_set.include(
142
163
  func,
@@ -169,7 +190,26 @@ class BaseSchema(Mapping):
169
190
  operation_id_regex: RegexValue | None = None,
170
191
  deprecated: bool = False,
171
192
  ) -> BaseSchema:
172
- """Include only operations that match the given filters."""
193
+ """Return a new schema excluding operations matching the specified criteria.
194
+
195
+ Args:
196
+ func: Custom filter function that accepts operation context.
197
+ name: Operation name(s) to exclude.
198
+ name_regex: Regex pattern for operation names.
199
+ method: HTTP method(s) to exclude.
200
+ method_regex: Regex pattern for HTTP methods.
201
+ path: API path(s) to exclude.
202
+ path_regex: Regex pattern for API paths.
203
+ tag: OpenAPI tag(s) to exclude.
204
+ tag_regex: Regex pattern for OpenAPI tags.
205
+ operation_id: Operation ID(s) to exclude.
206
+ operation_id_regex: Regex pattern for operation IDs.
207
+ deprecated: Whether to exclude deprecated operations.
208
+
209
+ Returns:
210
+ New schema instance with applied exclude filters.
211
+
212
+ """
173
213
  filter_set = self.filter_set.clone()
174
214
  if deprecated:
175
215
  if func is None:
@@ -211,10 +251,15 @@ class BaseSchema(Mapping):
211
251
  return self.statistic.operations.total
212
252
 
213
253
  def hook(self, hook: str | Callable) -> Callable:
254
+ """Register a hook function for this schema only.
255
+
256
+ Args:
257
+ hook: Hook name string or hook function to register.
258
+
259
+ """
214
260
  return self.hooks.hook(hook)
215
261
 
216
262
  def get_full_path(self, path: str) -> str:
217
- """Compute full path for the given path."""
218
263
  return get_full_path(self.base_path, path)
219
264
 
220
265
  @property
@@ -258,19 +303,27 @@ class BaseSchema(Mapping):
258
303
  raise NotImplementedError
259
304
 
260
305
  def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
261
- """Get examples from the API operation."""
262
306
  raise NotImplementedError
263
307
 
264
308
  def get_security_requirements(self, operation: APIOperation) -> list[str]:
265
- """Get applied security requirements for the given API operation."""
266
309
  raise NotImplementedError
267
310
 
268
311
  def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
269
- """Get a function that serializes parameters for the given location."""
270
312
  raise NotImplementedError
271
313
 
272
314
  def parametrize(self) -> Callable:
273
- """Mark a test function as a parametrized one."""
315
+ """Return a decorator that marks a test function for `pytest` parametrization.
316
+
317
+ The decorated test function will be parametrized with test cases generated
318
+ from the schema's API operations.
319
+
320
+ Returns:
321
+ Decorator function for test parametrization.
322
+
323
+ Raises:
324
+ IncorrectUsage: If applied to the same function multiple times.
325
+
326
+ """
274
327
 
275
328
  def wrapper(func: Callable) -> Callable:
276
329
  from schemathesis.pytest.plugin import SchemaHandleMark
@@ -293,7 +346,13 @@ class BaseSchema(Mapping):
293
346
  return wrapper
294
347
 
295
348
  def given(self, *args: GivenInput, **kwargs: GivenInput) -> Callable:
296
- """Proxy Hypothesis strategies to ``hypothesis.given``."""
349
+ """Proxy to Hypothesis's `given` decorator for adding custom strategies.
350
+
351
+ Args:
352
+ *args: Positional arguments passed to `hypothesis.given`.
353
+ **kwargs: Keyword arguments passed to `hypothesis.given`.
354
+
355
+ """
297
356
  return given_proxy(*args, **kwargs)
298
357
 
299
358
  def clone(
@@ -320,7 +379,6 @@ class BaseSchema(Mapping):
320
379
  )
321
380
 
322
381
  def get_local_hook_dispatcher(self) -> HookDispatcher | None:
323
- """Get a HookDispatcher instance bound to the test if present."""
324
382
  # It might be not present when it is used without pytest via `APIOperation.as_strategy()`
325
383
  if self.test_function is not None:
326
384
  # Might be missing it in case of `LazySchema` usage
@@ -328,7 +386,6 @@ class BaseSchema(Mapping):
328
386
  return None
329
387
 
330
388
  def dispatch_hook(self, name: str, context: HookContext, *args: Any, **kwargs: Any) -> None:
331
- """Dispatch a hook via all available dispatchers."""
332
389
  dispatch(name, context, *args, **kwargs)
333
390
  self.hooks.dispatch(name, context, *args, **kwargs)
334
391
  local_dispatcher = self.get_local_hook_dispatcher()
@@ -338,10 +395,6 @@ class BaseSchema(Mapping):
338
395
  def prepare_multipart(
339
396
  self, form_data: dict[str, Any], operation: APIOperation
340
397
  ) -> tuple[list | None, dict[str, Any] | None]:
341
- """Split content of `form_data` into files & data.
342
-
343
- Forms may contain file fields, that we should send via `files` argument in `requests`.
344
- """
345
398
  raise NotImplementedError
346
399
 
347
400
  def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
@@ -354,7 +407,7 @@ class BaseSchema(Mapping):
354
407
  method: str | None = None,
355
408
  path: str | None = None,
356
409
  path_parameters: dict[str, Any] | None = None,
357
- headers: dict[str, Any] | None = None,
410
+ headers: dict[str, Any] | CaseInsensitiveDict | None = None,
358
411
  cookies: dict[str, Any] | None = None,
359
412
  query: dict[str, Any] | None = None,
360
413
  body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
@@ -374,7 +427,12 @@ class BaseSchema(Mapping):
374
427
  raise NotImplementedError
375
428
 
376
429
  def as_state_machine(self) -> type[APIStateMachine]:
377
- """Create a state machine class."""
430
+ """Create a state machine class for stateful testing of linked API operations.
431
+
432
+ Returns:
433
+ APIStateMachine subclass configured for this schema.
434
+
435
+ """
378
436
  raise NotImplementedError
379
437
 
380
438
  def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
@@ -394,36 +452,28 @@ class BaseSchema(Mapping):
394
452
 
395
453
  def as_strategy(
396
454
  self,
397
- hooks: HookDispatcher | None = None,
398
- auth_storage: AuthStorage | None = None,
399
455
  generation_mode: GenerationMode = GenerationMode.POSITIVE,
400
456
  **kwargs: Any,
401
457
  ) -> SearchStrategy:
402
- """Build a strategy for generating test cases for all defined API operations."""
458
+ """Create a Hypothesis strategy that generates test cases for all schema operations.
459
+
460
+ Use with `@given` in non-Schemathesis tests.
461
+
462
+ Args:
463
+ generation_mode: Whether to generate positive or negative test data.
464
+ **kwargs: Additional keywords for each strategy.
465
+
466
+ Returns:
467
+ Combined Hypothesis strategy for all valid operations in the schema.
468
+
469
+ """
403
470
  _strategies = [
404
- operation.ok().as_strategy(
405
- hooks=hooks,
406
- auth_storage=auth_storage,
407
- generation_mode=generation_mode,
408
- **kwargs,
409
- )
471
+ operation.ok().as_strategy(generation_mode=generation_mode, **kwargs)
410
472
  for operation in self.get_all_operations()
411
473
  if isinstance(operation, Ok)
412
474
  ]
413
475
  return strategies.combine(_strategies)
414
476
 
415
- def configure(
416
- self,
417
- *,
418
- location: str | None | NotSet = NOT_SET,
419
- app: Any | NotSet = NOT_SET,
420
- ) -> Self:
421
- if not isinstance(location, NotSet):
422
- self.location = location
423
- if not isinstance(app, NotSet):
424
- self.app = app
425
- return self
426
-
427
477
  def find_operation_by_label(self, label: str) -> APIOperation | None:
428
478
  raise NotImplementedError
429
479
 
@@ -444,20 +494,23 @@ class APIOperationMap(Mapping):
444
494
 
445
495
  def as_strategy(
446
496
  self,
447
- hooks: HookDispatcher | None = None,
448
- auth_storage: AuthStorage | None = None,
449
497
  generation_mode: GenerationMode = GenerationMode.POSITIVE,
450
498
  **kwargs: Any,
451
499
  ) -> SearchStrategy:
452
- """Build a strategy for generating test cases for all API operations defined in this subset."""
500
+ """Create a Hypothesis strategy that generates test cases for all schema operations in this subset.
501
+
502
+ Use with `@given` in non-Schemathesis tests.
503
+
504
+ Args:
505
+ generation_mode: Whether to generate positive or negative test data.
506
+ **kwargs: Additional keywords for each strategy.
507
+
508
+ Returns:
509
+ Combined Hypothesis strategy for all valid operations in the schema.
510
+
511
+ """
453
512
  _strategies = [
454
- operation.as_strategy(
455
- hooks=hooks,
456
- auth_storage=auth_storage,
457
- generation_mode=generation_mode,
458
- **kwargs,
459
- )
460
- for operation in self._data.values()
513
+ operation.as_strategy(generation_mode=generation_mode, **kwargs) for operation in self._data.values()
461
514
  ]
462
515
  return strategies.combine(_strategies)
463
516
 
@@ -563,16 +616,7 @@ class OperationDefinition(Generic[D]):
563
616
 
564
617
  @dataclass(eq=False)
565
618
  class APIOperation(Generic[P]):
566
- """A single operation defined in an API.
567
-
568
- You can get one via a ``schema`` instance.
569
-
570
- .. code-block:: python
571
-
572
- # Get the POST /items operation
573
- operation = schema["/items"]["POST"]
574
-
575
- """
619
+ """An API operation (e.g., `GET /users`)."""
576
620
 
577
621
  # `path` does not contain `basePath`
578
622
  # Example <scheme>://<host>/<basePath>/users - "/users" is path
@@ -610,7 +654,6 @@ class APIOperation(Generic[P]):
610
654
  return self.schema.get_tags(self)
611
655
 
612
656
  def iter_parameters(self) -> Iterator[P]:
613
- """Iterate over all operation's parameters."""
614
657
  return chain(self.path_parameters, self.headers, self.cookies, self.query)
615
658
 
616
659
  def _lookup_container(self, location: str) -> ParameterSet[P] | PayloadAlternatives[P] | None:
@@ -623,11 +666,6 @@ class APIOperation(Generic[P]):
623
666
  }.get(location)
624
667
 
625
668
  def add_parameter(self, parameter: P) -> None:
626
- """Add a new processed parameter to an API operation.
627
-
628
- :param parameter: A parameter that will be used with this operation.
629
- :rtype: None
630
- """
631
669
  # If the parameter has a typo, then by default, there will be an error from `jsonschema` earlier.
632
670
  # But if the user wants to skip schema validation, we choose to ignore a malformed parameter.
633
671
  # In this case, we still might generate some tests for an API operation, but without this parameter,
@@ -644,13 +682,19 @@ class APIOperation(Generic[P]):
644
682
 
645
683
  def as_strategy(
646
684
  self,
647
- hooks: HookDispatcher | None = None,
648
- auth_storage: AuthStorage | None = None,
649
685
  generation_mode: GenerationMode = GenerationMode.POSITIVE,
650
686
  **kwargs: Any,
651
687
  ) -> SearchStrategy[Case]:
652
- """Turn this API operation into a Hypothesis strategy."""
653
- strategy = self.schema.get_case_strategy(self, hooks, auth_storage, generation_mode, **kwargs)
688
+ """Create a Hypothesis strategy that generates test cases for this API operation.
689
+
690
+ Use with `@given` in non-Schemathesis tests.
691
+
692
+ Args:
693
+ generation_mode: Whether to generate positive or negative test data.
694
+ **kwargs: Extra arguments to the underlying strategy function.
695
+
696
+ """
697
+ strategy = self.schema.get_case_strategy(self, generation_mode=generation_mode, **kwargs)
654
698
 
655
699
  def _apply_hooks(dispatcher: HookDispatcher, _strategy: SearchStrategy[Case]) -> SearchStrategy[Case]:
656
700
  context = HookContext(operation=self)
@@ -669,6 +713,7 @@ class APIOperation(Generic[P]):
669
713
 
670
714
  strategy = _apply_hooks(GLOBAL_HOOK_DISPATCHER, strategy)
671
715
  strategy = _apply_hooks(self.schema.hooks, strategy)
716
+ hooks = kwargs.get("hooks")
672
717
  if hooks is not None:
673
718
  strategy = _apply_hooks(hooks, strategy)
674
719
  return strategy
@@ -677,15 +722,9 @@ class APIOperation(Generic[P]):
677
722
  return self.schema.get_security_requirements(self)
678
723
 
679
724
  def get_strategies_from_examples(self, **kwargs: Any) -> list[SearchStrategy[Case]]:
680
- """Get examples from the API operation."""
681
725
  return self.schema.get_strategies_from_examples(self, **kwargs)
682
726
 
683
727
  def get_parameter_serializer(self, location: str) -> Callable | None:
684
- """Get a function that serializes parameters for the given location.
685
-
686
- It handles serializing data into various `collectionFormat` options and similar.
687
- Note that payload is handled by this function - it is handled by serializers.
688
- """
689
728
  return self.schema.get_parameter_serializer(self, location)
690
729
 
691
730
  def prepare_multipart(self, form_data: dict[str, Any]) -> tuple[list | None, dict[str, Any] | None]:
@@ -712,27 +751,37 @@ class APIOperation(Generic[P]):
712
751
  *,
713
752
  method: str | None = None,
714
753
  path_parameters: dict[str, Any] | None = None,
715
- headers: dict[str, Any] | None = None,
754
+ headers: dict[str, Any] | CaseInsensitiveDict | None = None,
716
755
  cookies: dict[str, Any] | None = None,
717
756
  query: dict[str, Any] | None = None,
718
757
  body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
719
758
  media_type: str | None = None,
720
- meta: CaseMetadata | None = None,
759
+ _meta: CaseMetadata | None = None,
721
760
  ) -> Case:
722
- """Create a new example for this API operation.
761
+ """Create a test case with specific data instead of generated values.
762
+
763
+ Args:
764
+ method: Override HTTP method.
765
+ path_parameters: Override path variables.
766
+ headers: Override HTTP headers.
767
+ cookies: Override cookies.
768
+ query: Override query parameters.
769
+ body: Override request body.
770
+ media_type: Override media type.
723
771
 
724
- The main use case is constructing Case instances completely manually, without data generation.
725
772
  """
773
+ from requests.structures import CaseInsensitiveDict
774
+
726
775
  return self.schema.make_case(
727
776
  operation=self,
728
777
  method=method,
729
- path_parameters=path_parameters,
730
- headers=headers,
731
- cookies=cookies,
732
- query=query,
778
+ path_parameters=path_parameters or {},
779
+ headers=CaseInsensitiveDict() if headers is None else CaseInsensitiveDict(headers),
780
+ cookies=cookies or {},
781
+ query=query or {},
733
782
  body=body,
734
783
  media_type=media_type,
735
- meta=meta,
784
+ meta=_meta,
736
785
  )
737
786
 
738
787
  @property
@@ -740,15 +789,30 @@ class APIOperation(Generic[P]):
740
789
  path = self.path.replace("~", "~0").replace("/", "~1")
741
790
  return f"#/paths/{path}/{self.method}"
742
791
 
743
- def validate_response(self, response: Response) -> bool | None:
744
- """Validate API response for conformance.
792
+ def validate_response(self, response: Response | httpx.Response | requests.Response | TestResponse) -> bool | None:
793
+ """Validate a response against the API schema.
794
+
795
+ Args:
796
+ response: The HTTP response to validate. Can be a `requests.Response`,
797
+ `httpx.Response`, `werkzeug.test.TestResponse`, or `schemathesis.Response`.
798
+
799
+ Raises:
800
+ FailureGroup: If the response does not conform to the schema.
745
801
 
746
- :raises FailureGroup: If the response does not conform to the API schema.
747
802
  """
748
- return self.schema.validate_response(self, response)
803
+ return self.schema.validate_response(self, Response.from_any(response))
804
+
805
+ def is_valid_response(self, response: Response | httpx.Response | requests.Response | TestResponse) -> bool:
806
+ """Check if the provided response is valid against the API schema.
749
807
 
750
- def is_response_valid(self, response: Response) -> bool:
751
- """Validate API response for conformance."""
808
+ Args:
809
+ response: The HTTP response to validate. Can be a `requests.Response`,
810
+ `httpx.Response`, `werkzeug.test.TestResponse`, or `schemathesis.Response`.
811
+
812
+ Returns:
813
+ `True` if response is valid, `False` otherwise.
814
+
815
+ """
752
816
  try:
753
817
  self.validate_response(response)
754
818
  return True
@@ -253,7 +253,7 @@ class GraphQLSchema(BaseSchema):
253
253
  method: str | None = None,
254
254
  path: str | None = None,
255
255
  path_parameters: dict[str, Any] | None = None,
256
- headers: dict[str, Any] | None = None,
256
+ headers: dict[str, Any] | CaseInsensitiveDict | None = None,
257
257
  cookies: dict[str, Any] | None = None,
258
258
  query: dict[str, Any] | None = None,
259
259
  body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
@@ -264,10 +264,10 @@ class GraphQLSchema(BaseSchema):
264
264
  operation=operation,
265
265
  method=method or operation.method.upper(),
266
266
  path=path or operation.path,
267
- path_parameters=path_parameters,
268
- headers=CaseInsensitiveDict(headers) if headers is not None else headers,
269
- cookies=cookies,
270
- query=query,
267
+ path_parameters=path_parameters or {},
268
+ headers=CaseInsensitiveDict() if headers is None else CaseInsensitiveDict(headers),
269
+ cookies=cookies or {},
270
+ query=query or {},
271
271
  body=body,
272
272
  media_type=media_type or "application/json",
273
273
  meta=meta,
@@ -376,7 +376,7 @@ def graphql_cases(
376
376
  cookies=cookies_,
377
377
  query=query_,
378
378
  body=body,
379
- meta=CaseMetadata(
379
+ _meta=CaseMetadata(
380
380
  generation=GenerationInfo(
381
381
  time=time.monotonic() - start,
382
382
  mode=generation_mode,
@@ -7,9 +7,11 @@ from typing import Any, Callable, Dict, Iterable, Optional, Union, cast
7
7
  from urllib.parse import quote_plus
8
8
  from weakref import WeakKeyDictionary
9
9
 
10
+ import jsonschema.protocols
10
11
  from hypothesis import event, note, reject
11
12
  from hypothesis import strategies as st
12
13
  from hypothesis_jsonschema import from_schema
14
+ from requests.structures import CaseInsensitiveDict
13
15
 
14
16
  from schemathesis.config import GenerationConfig
15
17
  from schemathesis.core import NOT_SET, NotSet, media_types
@@ -42,7 +44,9 @@ from .parameters import OpenAPIBody, OpenAPIParameter, parameters_to_json_schema
42
44
  from .utils import is_header_location
43
45
 
44
46
  SLASH = "/"
45
- StrategyFactory = Callable[[Dict[str, Any], str, str, Optional[str], GenerationConfig], st.SearchStrategy]
47
+ StrategyFactory = Callable[
48
+ [Dict[str, Any], str, str, Optional[str], GenerationConfig, type[jsonschema.protocols.Validator]], st.SearchStrategy
49
+ ]
46
50
 
47
51
 
48
52
  @st.composite # type: ignore
@@ -154,12 +158,12 @@ def openapi_cases(
154
158
 
155
159
  instance = operation.Case(
156
160
  media_type=media_type,
157
- path_parameters=path_parameters_.value,
158
- headers=headers_.value,
159
- cookies=cookies_.value,
160
- query=query_.value,
161
+ path_parameters=path_parameters_.value or {},
162
+ headers=headers_.value or CaseInsensitiveDict(),
163
+ cookies=cookies_.value or {},
164
+ query=query_.value or {},
161
165
  body=body_.value,
162
- meta=CaseMetadata(
166
+ _meta=CaseMetadata(
163
167
  generation=GenerationInfo(
164
168
  time=time.monotonic() - start,
165
169
  mode=generation_mode,
@@ -195,6 +199,8 @@ def _get_body_strategy(
195
199
  operation: APIOperation,
196
200
  generation_config: GenerationConfig,
197
201
  ) -> st.SearchStrategy:
202
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
203
+
198
204
  if parameter.media_type in MEDIA_TYPES:
199
205
  return MEDIA_TYPES[parameter.media_type]
200
206
  # The cache key relies on object ids, which means that the parameter should not be mutated
@@ -203,7 +209,10 @@ def _get_body_strategy(
203
209
  return _BODY_STRATEGIES_CACHE[parameter][strategy_factory]
204
210
  schema = parameter.as_json_schema(operation)
205
211
  schema = operation.schema.prepare_schema(schema)
206
- strategy = strategy_factory(schema, operation.label, "body", parameter.media_type, generation_config)
212
+ assert isinstance(operation.schema, BaseOpenAPISchema)
213
+ strategy = strategy_factory(
214
+ schema, operation.label, "body", parameter.media_type, generation_config, operation.schema.validator_cls
215
+ )
207
216
  if not parameter.is_required:
208
217
  strategy |= st.just(NOT_SET)
209
218
  _BODY_STRATEGIES_CACHE.setdefault(parameter, {})[strategy_factory] = strategy
@@ -337,6 +346,8 @@ def get_parameters_strategy(
337
346
  exclude: Iterable[str] = (),
338
347
  ) -> st.SearchStrategy:
339
348
  """Create a new strategy for the case's component from the API operation parameters."""
349
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
350
+
340
351
  parameters = getattr(operation, LOCATION_TO_CONTAINER[location])
341
352
  if parameters:
342
353
  # The cache key relies on object ids, which means that the parameter should not be mutated
@@ -344,17 +355,28 @@ def get_parameters_strategy(
344
355
  if operation in _PARAMETER_STRATEGIES_CACHE and nested_cache_key in _PARAMETER_STRATEGIES_CACHE[operation]:
345
356
  return _PARAMETER_STRATEGIES_CACHE[operation][nested_cache_key]
346
357
  schema = get_schema_for_location(operation, location, parameters)
347
- for name in exclude:
348
- # Values from `exclude` are not necessarily valid for the schema - they come from user-defined examples
349
- # that may be invalid
350
- schema["properties"].pop(name, None)
351
- with suppress(ValueError):
352
- schema["required"].remove(name)
358
+ if location == "header" and exclude:
359
+ # Remove excluded headers case-insensitively
360
+ exclude_lower = {name.lower() for name in exclude}
361
+ schema["properties"] = {
362
+ key: value for key, value in schema["properties"].items() if key.lower() not in exclude_lower
363
+ }
364
+ if "required" in schema:
365
+ schema["required"] = [key for key in schema["required"] if key.lower() not in exclude_lower]
366
+ elif exclude:
367
+ # Non-header locations: remove by exact name
368
+ for name in exclude:
369
+ schema["properties"].pop(name, None)
370
+ with suppress(ValueError):
371
+ schema["required"].remove(name)
353
372
  if not schema["properties"] and strategy_factory is make_negative_strategy:
354
373
  # Nothing to negate - all properties were excluded
355
374
  strategy = st.none()
356
375
  else:
357
- strategy = strategy_factory(schema, operation.label, location, None, generation_config)
376
+ assert isinstance(operation.schema, BaseOpenAPISchema)
377
+ strategy = strategy_factory(
378
+ schema, operation.label, location, None, generation_config, operation.schema.validator_cls
379
+ )
358
380
  serialize = operation.get_parameter_serializer(location)
359
381
  if serialize is not None:
360
382
  strategy = strategy.map(serialize)
@@ -418,6 +440,7 @@ def make_positive_strategy(
418
440
  location: str,
419
441
  media_type: str | None,
420
442
  generation_config: GenerationConfig,
443
+ validator_cls: type[jsonschema.protocols.Validator],
421
444
  custom_formats: dict[str, st.SearchStrategy] | None = None,
422
445
  ) -> st.SearchStrategy:
423
446
  """Strategy for generating values that fit the schema."""
@@ -448,6 +471,7 @@ def make_negative_strategy(
448
471
  location: str,
449
472
  media_type: str | None,
450
473
  generation_config: GenerationConfig,
474
+ validator_cls: type[jsonschema.protocols.Validator],
451
475
  custom_formats: dict[str, st.SearchStrategy] | None = None,
452
476
  ) -> st.SearchStrategy:
453
477
  custom_formats = _build_custom_formats(custom_formats, generation_config)
@@ -458,6 +482,7 @@ def make_negative_strategy(
458
482
  media_type=media_type,
459
483
  custom_formats=custom_formats,
460
484
  generation_config=generation_config,
485
+ validator_cls=validator_cls,
461
486
  )
462
487
 
463
488