schemathesis 4.0.0a11__py3-none-any.whl → 4.0.0b1__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 (73) hide show
  1. schemathesis/__init__.py +35 -27
  2. schemathesis/auths.py +85 -54
  3. schemathesis/checks.py +65 -36
  4. schemathesis/cli/commands/run/__init__.py +32 -27
  5. schemathesis/cli/commands/run/context.py +6 -1
  6. schemathesis/cli/commands/run/events.py +7 -1
  7. schemathesis/cli/commands/run/executor.py +12 -7
  8. schemathesis/cli/commands/run/handlers/output.py +188 -80
  9. schemathesis/cli/commands/run/validation.py +21 -6
  10. schemathesis/cli/constants.py +1 -1
  11. schemathesis/config/__init__.py +2 -1
  12. schemathesis/config/_generation.py +12 -13
  13. schemathesis/config/_operations.py +14 -0
  14. schemathesis/config/_phases.py +41 -5
  15. schemathesis/config/_projects.py +33 -1
  16. schemathesis/config/_report.py +6 -2
  17. schemathesis/config/_warnings.py +25 -0
  18. schemathesis/config/schema.json +49 -1
  19. schemathesis/core/errors.py +15 -19
  20. schemathesis/core/transport.py +117 -2
  21. schemathesis/engine/context.py +1 -0
  22. schemathesis/engine/errors.py +61 -2
  23. schemathesis/engine/events.py +10 -2
  24. schemathesis/engine/phases/probes.py +3 -0
  25. schemathesis/engine/phases/stateful/__init__.py +2 -1
  26. schemathesis/engine/phases/stateful/_executor.py +38 -5
  27. schemathesis/engine/phases/stateful/context.py +2 -2
  28. schemathesis/engine/phases/unit/_executor.py +36 -7
  29. schemathesis/generation/__init__.py +0 -3
  30. schemathesis/generation/case.py +153 -28
  31. schemathesis/generation/coverage.py +1 -1
  32. schemathesis/generation/hypothesis/builder.py +43 -19
  33. schemathesis/generation/metrics.py +93 -0
  34. schemathesis/generation/modes.py +0 -8
  35. schemathesis/generation/overrides.py +11 -27
  36. schemathesis/generation/stateful/__init__.py +17 -0
  37. schemathesis/generation/stateful/state_machine.py +32 -108
  38. schemathesis/graphql/loaders.py +152 -8
  39. schemathesis/hooks.py +63 -39
  40. schemathesis/openapi/checks.py +82 -20
  41. schemathesis/openapi/generation/filters.py +9 -2
  42. schemathesis/openapi/loaders.py +134 -8
  43. schemathesis/pytest/lazy.py +4 -31
  44. schemathesis/pytest/loaders.py +24 -0
  45. schemathesis/pytest/plugin.py +38 -6
  46. schemathesis/schemas.py +161 -94
  47. schemathesis/specs/graphql/scalars.py +37 -3
  48. schemathesis/specs/graphql/schemas.py +18 -9
  49. schemathesis/specs/openapi/_hypothesis.py +53 -34
  50. schemathesis/specs/openapi/checks.py +111 -47
  51. schemathesis/specs/openapi/expressions/nodes.py +1 -1
  52. schemathesis/specs/openapi/formats.py +30 -3
  53. schemathesis/specs/openapi/media_types.py +44 -1
  54. schemathesis/specs/openapi/negative/__init__.py +5 -3
  55. schemathesis/specs/openapi/negative/mutations.py +2 -2
  56. schemathesis/specs/openapi/parameters.py +0 -3
  57. schemathesis/specs/openapi/schemas.py +14 -93
  58. schemathesis/specs/openapi/stateful/__init__.py +2 -1
  59. schemathesis/specs/openapi/stateful/links.py +1 -63
  60. schemathesis/transport/__init__.py +54 -16
  61. schemathesis/transport/prepare.py +31 -7
  62. schemathesis/transport/requests.py +21 -9
  63. schemathesis/transport/serialization.py +0 -4
  64. schemathesis/transport/wsgi.py +15 -8
  65. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/METADATA +45 -87
  66. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/RECORD +69 -71
  67. schemathesis/contrib/__init__.py +0 -9
  68. schemathesis/contrib/openapi/__init__.py +0 -9
  69. schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
  70. schemathesis/generation/targets.py +0 -69
  71. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/WHEEL +0 -0
  72. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/entry_points.txt +0 -0
  73. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.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:
214
- return self.hooks.register(hook)
254
+ """Register a hook function for this schema only.
255
+
256
+ Args:
257
+ hook: Hook name string or hook function to register.
258
+
259
+ """
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,
@@ -368,13 +421,18 @@ class BaseSchema(Mapping):
368
421
  operation: APIOperation,
369
422
  hooks: HookDispatcher | None = None,
370
423
  auth_storage: AuthStorage | None = None,
371
- generation_mode: GenerationMode = GenerationMode.default(),
424
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
372
425
  **kwargs: Any,
373
426
  ) -> SearchStrategy:
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,35 +452,30 @@ 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
- generation_mode: GenerationMode = GenerationMode.default(),
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
477
+ def find_operation_by_label(self, label: str) -> APIOperation | None:
478
+ raise NotImplementedError
426
479
 
427
480
 
428
481
  @dataclass
@@ -441,20 +494,23 @@ class APIOperationMap(Mapping):
441
494
 
442
495
  def as_strategy(
443
496
  self,
444
- hooks: HookDispatcher | None = None,
445
- auth_storage: AuthStorage | None = None,
446
- generation_mode: GenerationMode = GenerationMode.default(),
497
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
447
498
  **kwargs: Any,
448
499
  ) -> SearchStrategy:
449
- """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
+ """
450
512
  _strategies = [
451
- operation.as_strategy(
452
- hooks=hooks,
453
- auth_storage=auth_storage,
454
- generation_mode=generation_mode,
455
- **kwargs,
456
- )
457
- for operation in self._data.values()
513
+ operation.as_strategy(generation_mode=generation_mode, **kwargs) for operation in self._data.values()
458
514
  ]
459
515
  return strategies.combine(_strategies)
460
516
 
@@ -560,16 +616,7 @@ class OperationDefinition(Generic[D]):
560
616
 
561
617
  @dataclass(eq=False)
562
618
  class APIOperation(Generic[P]):
563
- """A single operation defined in an API.
564
-
565
- You can get one via a ``schema`` instance.
566
-
567
- .. code-block:: python
568
-
569
- # Get the POST /items operation
570
- operation = schema["/items"]["POST"]
571
-
572
- """
619
+ """An API operation (e.g., `GET /users`)."""
573
620
 
574
621
  # `path` does not contain `basePath`
575
622
  # Example <scheme>://<host>/<basePath>/users - "/users" is path
@@ -607,7 +654,6 @@ class APIOperation(Generic[P]):
607
654
  return self.schema.get_tags(self)
608
655
 
609
656
  def iter_parameters(self) -> Iterator[P]:
610
- """Iterate over all operation's parameters."""
611
657
  return chain(self.path_parameters, self.headers, self.cookies, self.query)
612
658
 
613
659
  def _lookup_container(self, location: str) -> ParameterSet[P] | PayloadAlternatives[P] | None:
@@ -620,11 +666,6 @@ class APIOperation(Generic[P]):
620
666
  }.get(location)
621
667
 
622
668
  def add_parameter(self, parameter: P) -> None:
623
- """Add a new processed parameter to an API operation.
624
-
625
- :param parameter: A parameter that will be used with this operation.
626
- :rtype: None
627
- """
628
669
  # If the parameter has a typo, then by default, there will be an error from `jsonschema` earlier.
629
670
  # But if the user wants to skip schema validation, we choose to ignore a malformed parameter.
630
671
  # In this case, we still might generate some tests for an API operation, but without this parameter,
@@ -641,16 +682,22 @@ class APIOperation(Generic[P]):
641
682
 
642
683
  def as_strategy(
643
684
  self,
644
- hooks: HookDispatcher | None = None,
645
- auth_storage: AuthStorage | None = None,
646
- generation_mode: GenerationMode = GenerationMode.default(),
685
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
647
686
  **kwargs: Any,
648
687
  ) -> SearchStrategy[Case]:
649
- """Turn this API operation into a Hypothesis strategy."""
650
- 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)
651
698
 
652
699
  def _apply_hooks(dispatcher: HookDispatcher, _strategy: SearchStrategy[Case]) -> SearchStrategy[Case]:
653
- context = HookContext(self)
700
+ context = HookContext(operation=self)
654
701
  for hook in dispatcher.get_all_by_name("before_generate_case"):
655
702
  _strategy = hook(context, _strategy)
656
703
  for hook in dispatcher.get_all_by_name("filter_case"):
@@ -666,6 +713,7 @@ class APIOperation(Generic[P]):
666
713
 
667
714
  strategy = _apply_hooks(GLOBAL_HOOK_DISPATCHER, strategy)
668
715
  strategy = _apply_hooks(self.schema.hooks, strategy)
716
+ hooks = kwargs.get("hooks")
669
717
  if hooks is not None:
670
718
  strategy = _apply_hooks(hooks, strategy)
671
719
  return strategy
@@ -674,15 +722,9 @@ class APIOperation(Generic[P]):
674
722
  return self.schema.get_security_requirements(self)
675
723
 
676
724
  def get_strategies_from_examples(self, **kwargs: Any) -> list[SearchStrategy[Case]]:
677
- """Get examples from the API operation."""
678
725
  return self.schema.get_strategies_from_examples(self, **kwargs)
679
726
 
680
727
  def get_parameter_serializer(self, location: str) -> Callable | None:
681
- """Get a function that serializes parameters for the given location.
682
-
683
- It handles serializing data into various `collectionFormat` options and similar.
684
- Note that payload is handled by this function - it is handled by serializers.
685
- """
686
728
  return self.schema.get_parameter_serializer(self, location)
687
729
 
688
730
  def prepare_multipart(self, form_data: dict[str, Any]) -> tuple[list | None, dict[str, Any] | None]:
@@ -709,27 +751,37 @@ class APIOperation(Generic[P]):
709
751
  *,
710
752
  method: str | None = None,
711
753
  path_parameters: dict[str, Any] | None = None,
712
- headers: dict[str, Any] | None = None,
754
+ headers: dict[str, Any] | CaseInsensitiveDict | None = None,
713
755
  cookies: dict[str, Any] | None = None,
714
756
  query: dict[str, Any] | None = None,
715
757
  body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
716
758
  media_type: str | None = None,
717
- meta: CaseMetadata | None = None,
759
+ _meta: CaseMetadata | None = None,
718
760
  ) -> Case:
719
- """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.
720
771
 
721
- The main use case is constructing Case instances completely manually, without data generation.
722
772
  """
773
+ from requests.structures import CaseInsensitiveDict
774
+
723
775
  return self.schema.make_case(
724
776
  operation=self,
725
777
  method=method,
726
- path_parameters=path_parameters,
727
- headers=headers,
728
- cookies=cookies,
729
- 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 {},
730
782
  body=body,
731
783
  media_type=media_type,
732
- meta=meta,
784
+ meta=_meta,
733
785
  )
734
786
 
735
787
  @property
@@ -737,15 +789,30 @@ class APIOperation(Generic[P]):
737
789
  path = self.path.replace("~", "~0").replace("/", "~1")
738
790
  return f"#/paths/{path}/{self.method}"
739
791
 
740
- def validate_response(self, response: Response) -> bool | None:
741
- """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.
742
801
 
743
- :raises FailureGroup: If the response does not conform to the API schema.
744
802
  """
745
- 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.
746
807
 
747
- def is_response_valid(self, response: Response) -> bool:
748
- """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
+ """
749
816
  try:
750
817
  self.validate_response(response)
751
818
  return True
@@ -13,10 +13,44 @@ CUSTOM_SCALARS: dict[str, st.SearchStrategy[graphql.ValueNode]] = {}
13
13
 
14
14
 
15
15
  def scalar(name: str, strategy: st.SearchStrategy[graphql.ValueNode]) -> None:
16
- """Register a new strategy for generating custom scalars.
16
+ r"""Register a custom Hypothesis strategy for generating GraphQL scalar values.
17
+
18
+ Args:
19
+ name: Scalar name that matches your GraphQL schema scalar definition
20
+ strategy: Hypothesis strategy that generates GraphQL AST ValueNode objects
21
+
22
+ Example:
23
+ ```python
24
+ import schemathesis
25
+ from hypothesis import strategies as st
26
+ from schemathesis.graphql import nodes
27
+
28
+ # Register email scalar
29
+ schemathesis.graphql.scalar("Email", st.emails().map(nodes.String))
30
+
31
+ # Register positive integer scalar
32
+ schemathesis.graphql.scalar(
33
+ "PositiveInt",
34
+ st.integers(min_value=1).map(nodes.Int)
35
+ )
36
+
37
+ # Register phone number scalar
38
+ schemathesis.graphql.scalar(
39
+ "Phone",
40
+ st.from_regex(r"\+1-\d{3}-\d{3}-\d{4}").map(nodes.String)
41
+ )
42
+ ```
43
+
44
+ Schema usage:
45
+ ```graphql
46
+ scalar Email
47
+ scalar PositiveInt
48
+
49
+ type Query {
50
+ getUser(email: Email!, rating: PositiveInt!): User
51
+ }
52
+ ```
17
53
 
18
- :param str name: Scalar name. It should correspond the one used in the schema.
19
- :param strategy: Hypothesis strategy you'd like to use to generate values for this scalar.
20
54
  """
21
55
  from hypothesis.strategies import SearchStrategy
22
56
 
@@ -115,6 +115,15 @@ class GraphQLSchema(BaseSchema):
115
115
  return map
116
116
  raise KeyError(key)
117
117
 
118
+ def find_operation_by_label(self, label: str) -> APIOperation | None:
119
+ if label.startswith(("Query.", "Mutation.")):
120
+ ty, field = label.split(".", maxsplit=1)
121
+ try:
122
+ return self[ty][field]
123
+ except KeyError:
124
+ return None
125
+ return None
126
+
118
127
  def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
119
128
  raw_schema = self.raw_schema["__schema"]
120
129
  type_names = [type_def["name"] for type_def in raw_schema.get("types", [])]
@@ -223,7 +232,7 @@ class GraphQLSchema(BaseSchema):
223
232
  operation: APIOperation,
224
233
  hooks: HookDispatcher | None = None,
225
234
  auth_storage: AuthStorage | None = None,
226
- generation_mode: GenerationMode = GenerationMode.default(),
235
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
227
236
  **kwargs: Any,
228
237
  ) -> SearchStrategy:
229
238
  return graphql_cases(
@@ -244,7 +253,7 @@ class GraphQLSchema(BaseSchema):
244
253
  method: str | None = None,
245
254
  path: str | None = None,
246
255
  path_parameters: dict[str, Any] | None = None,
247
- headers: dict[str, Any] | None = None,
256
+ headers: dict[str, Any] | CaseInsensitiveDict | None = None,
248
257
  cookies: dict[str, Any] | None = None,
249
258
  query: dict[str, Any] | None = None,
250
259
  body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
@@ -255,10 +264,10 @@ class GraphQLSchema(BaseSchema):
255
264
  operation=operation,
256
265
  method=method or operation.method.upper(),
257
266
  path=path or operation.path,
258
- path_parameters=path_parameters,
259
- headers=CaseInsensitiveDict(headers) if headers is not None else headers,
260
- cookies=cookies,
261
- 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 {},
262
271
  body=body,
263
272
  media_type=media_type or "application/json",
264
273
  meta=meta,
@@ -321,7 +330,7 @@ def graphql_cases(
321
330
  operation: APIOperation,
322
331
  hooks: HookDispatcher | None = None,
323
332
  auth_storage: auths.AuthStorage | None = None,
324
- generation_mode: GenerationMode = GenerationMode.default(),
333
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
325
334
  path_parameters: NotSet | dict[str, Any] = NOT_SET,
326
335
  headers: NotSet | dict[str, Any] = NOT_SET,
327
336
  cookies: NotSet | dict[str, Any] = NOT_SET,
@@ -336,7 +345,7 @@ def graphql_cases(
336
345
  RootType.QUERY: gql_st.queries,
337
346
  RootType.MUTATION: gql_st.mutations,
338
347
  }[definition.root_type]
339
- hook_context = HookContext(operation)
348
+ hook_context = HookContext(operation=operation)
340
349
  custom_scalars = {**get_extra_scalar_strategies(), **CUSTOM_SCALARS}
341
350
  generation = operation.schema.config.generation_for(operation=operation, phase="fuzzing")
342
351
  strategy = strategy_factory(
@@ -367,7 +376,7 @@ def graphql_cases(
367
376
  cookies=cookies_,
368
377
  query=query_,
369
378
  body=body,
370
- meta=CaseMetadata(
379
+ _meta=CaseMetadata(
371
380
  generation=GenerationInfo(
372
381
  time=time.monotonic() - start,
373
382
  mode=generation_mode,