schemathesis 3.25.5__py3-none-any.whl → 4.0.0a1__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 (221) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +102 -82
  3. schemathesis/checks.py +126 -46
  4. schemathesis/cli/__init__.py +11 -1766
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +37 -0
  7. schemathesis/cli/commands/run/__init__.py +662 -0
  8. schemathesis/cli/commands/run/checks.py +80 -0
  9. schemathesis/cli/commands/run/context.py +117 -0
  10. schemathesis/cli/commands/run/events.py +35 -0
  11. schemathesis/cli/commands/run/executor.py +138 -0
  12. schemathesis/cli/commands/run/filters.py +194 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +18 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  17. schemathesis/cli/commands/run/handlers/output.py +746 -0
  18. schemathesis/cli/commands/run/hypothesis.py +105 -0
  19. schemathesis/cli/commands/run/loaders.py +129 -0
  20. schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
  21. schemathesis/cli/constants.py +5 -52
  22. schemathesis/cli/core.py +17 -0
  23. schemathesis/cli/ext/fs.py +14 -0
  24. schemathesis/cli/ext/groups.py +55 -0
  25. schemathesis/cli/{options.py → ext/options.py} +39 -10
  26. schemathesis/cli/hooks.py +36 -0
  27. schemathesis/contrib/__init__.py +1 -3
  28. schemathesis/contrib/openapi/__init__.py +1 -3
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
  30. schemathesis/core/__init__.py +58 -0
  31. schemathesis/core/compat.py +25 -0
  32. schemathesis/core/control.py +2 -0
  33. schemathesis/core/curl.py +58 -0
  34. schemathesis/core/deserialization.py +65 -0
  35. schemathesis/core/errors.py +370 -0
  36. schemathesis/core/failures.py +285 -0
  37. schemathesis/core/fs.py +19 -0
  38. schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
  39. schemathesis/core/loaders.py +104 -0
  40. schemathesis/core/marks.py +66 -0
  41. schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
  42. schemathesis/core/output/__init__.py +69 -0
  43. schemathesis/core/output/sanitization.py +197 -0
  44. schemathesis/core/rate_limit.py +60 -0
  45. schemathesis/core/registries.py +31 -0
  46. schemathesis/{internal → core}/result.py +1 -1
  47. schemathesis/core/transforms.py +113 -0
  48. schemathesis/core/transport.py +108 -0
  49. schemathesis/core/validation.py +38 -0
  50. schemathesis/core/version.py +7 -0
  51. schemathesis/engine/__init__.py +30 -0
  52. schemathesis/engine/config.py +59 -0
  53. schemathesis/engine/context.py +119 -0
  54. schemathesis/engine/control.py +36 -0
  55. schemathesis/engine/core.py +157 -0
  56. schemathesis/engine/errors.py +394 -0
  57. schemathesis/engine/events.py +337 -0
  58. schemathesis/engine/phases/__init__.py +66 -0
  59. schemathesis/{cli → engine/phases}/probes.py +63 -70
  60. schemathesis/engine/phases/stateful/__init__.py +65 -0
  61. schemathesis/engine/phases/stateful/_executor.py +326 -0
  62. schemathesis/engine/phases/stateful/context.py +85 -0
  63. schemathesis/engine/phases/unit/__init__.py +174 -0
  64. schemathesis/engine/phases/unit/_executor.py +321 -0
  65. schemathesis/engine/phases/unit/_pool.py +74 -0
  66. schemathesis/engine/recorder.py +241 -0
  67. schemathesis/errors.py +31 -0
  68. schemathesis/experimental/__init__.py +18 -14
  69. schemathesis/filters.py +103 -14
  70. schemathesis/generation/__init__.py +21 -37
  71. schemathesis/generation/case.py +190 -0
  72. schemathesis/generation/coverage.py +931 -0
  73. schemathesis/generation/hypothesis/__init__.py +30 -0
  74. schemathesis/generation/hypothesis/builder.py +585 -0
  75. schemathesis/generation/hypothesis/examples.py +50 -0
  76. schemathesis/generation/hypothesis/given.py +66 -0
  77. schemathesis/generation/hypothesis/reporting.py +14 -0
  78. schemathesis/generation/hypothesis/strategies.py +16 -0
  79. schemathesis/generation/meta.py +115 -0
  80. schemathesis/generation/modes.py +28 -0
  81. schemathesis/generation/overrides.py +96 -0
  82. schemathesis/generation/stateful/__init__.py +20 -0
  83. schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
  84. schemathesis/generation/targets.py +69 -0
  85. schemathesis/graphql/__init__.py +15 -0
  86. schemathesis/graphql/checks.py +115 -0
  87. schemathesis/graphql/loaders.py +131 -0
  88. schemathesis/hooks.py +99 -67
  89. schemathesis/openapi/__init__.py +13 -0
  90. schemathesis/openapi/checks.py +412 -0
  91. schemathesis/openapi/generation/__init__.py +0 -0
  92. schemathesis/openapi/generation/filters.py +63 -0
  93. schemathesis/openapi/loaders.py +178 -0
  94. schemathesis/pytest/__init__.py +5 -0
  95. schemathesis/pytest/control_flow.py +7 -0
  96. schemathesis/pytest/lazy.py +273 -0
  97. schemathesis/pytest/loaders.py +12 -0
  98. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
  99. schemathesis/python/__init__.py +0 -0
  100. schemathesis/python/asgi.py +12 -0
  101. schemathesis/python/wsgi.py +12 -0
  102. schemathesis/schemas.py +537 -261
  103. schemathesis/specs/graphql/__init__.py +0 -1
  104. schemathesis/specs/graphql/_cache.py +25 -0
  105. schemathesis/specs/graphql/nodes.py +1 -0
  106. schemathesis/specs/graphql/scalars.py +7 -5
  107. schemathesis/specs/graphql/schemas.py +215 -187
  108. schemathesis/specs/graphql/validation.py +11 -18
  109. schemathesis/specs/openapi/__init__.py +7 -1
  110. schemathesis/specs/openapi/_cache.py +122 -0
  111. schemathesis/specs/openapi/_hypothesis.py +146 -165
  112. schemathesis/specs/openapi/checks.py +565 -67
  113. schemathesis/specs/openapi/converter.py +33 -6
  114. schemathesis/specs/openapi/definitions.py +11 -18
  115. schemathesis/specs/openapi/examples.py +153 -39
  116. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  117. schemathesis/specs/openapi/expressions/context.py +4 -6
  118. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  119. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  120. schemathesis/specs/openapi/expressions/nodes.py +38 -14
  121. schemathesis/specs/openapi/expressions/parser.py +26 -5
  122. schemathesis/specs/openapi/formats.py +45 -0
  123. schemathesis/specs/openapi/links.py +65 -165
  124. schemathesis/specs/openapi/media_types.py +32 -0
  125. schemathesis/specs/openapi/negative/__init__.py +7 -3
  126. schemathesis/specs/openapi/negative/mutations.py +24 -8
  127. schemathesis/specs/openapi/parameters.py +46 -30
  128. schemathesis/specs/openapi/patterns.py +137 -0
  129. schemathesis/specs/openapi/references.py +47 -57
  130. schemathesis/specs/openapi/schemas.py +483 -367
  131. schemathesis/specs/openapi/security.py +25 -7
  132. schemathesis/specs/openapi/serialization.py +11 -6
  133. schemathesis/specs/openapi/stateful/__init__.py +185 -73
  134. schemathesis/specs/openapi/utils.py +6 -1
  135. schemathesis/transport/__init__.py +104 -0
  136. schemathesis/transport/asgi.py +26 -0
  137. schemathesis/transport/prepare.py +99 -0
  138. schemathesis/transport/requests.py +221 -0
  139. schemathesis/{_xml.py → transport/serialization.py} +143 -28
  140. schemathesis/transport/wsgi.py +165 -0
  141. schemathesis-4.0.0a1.dist-info/METADATA +297 -0
  142. schemathesis-4.0.0a1.dist-info/RECORD +152 -0
  143. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
  145. schemathesis/_compat.py +0 -74
  146. schemathesis/_dependency_versions.py +0 -17
  147. schemathesis/_hypothesis.py +0 -246
  148. schemathesis/_override.py +0 -49
  149. schemathesis/cli/cassettes.py +0 -375
  150. schemathesis/cli/context.py +0 -55
  151. schemathesis/cli/debug.py +0 -26
  152. schemathesis/cli/handlers.py +0 -16
  153. schemathesis/cli/junitxml.py +0 -43
  154. schemathesis/cli/output/__init__.py +0 -1
  155. schemathesis/cli/output/default.py +0 -765
  156. schemathesis/cli/output/short.py +0 -40
  157. schemathesis/cli/sanitization.py +0 -20
  158. schemathesis/code_samples.py +0 -149
  159. schemathesis/constants.py +0 -55
  160. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  161. schemathesis/contrib/openapi/formats/uuid.py +0 -15
  162. schemathesis/contrib/unique_data.py +0 -41
  163. schemathesis/exceptions.py +0 -560
  164. schemathesis/extra/_aiohttp.py +0 -27
  165. schemathesis/extra/_flask.py +0 -10
  166. schemathesis/extra/_server.py +0 -17
  167. schemathesis/failures.py +0 -209
  168. schemathesis/fixups/__init__.py +0 -36
  169. schemathesis/fixups/fast_api.py +0 -41
  170. schemathesis/fixups/utf8_bom.py +0 -29
  171. schemathesis/graphql.py +0 -4
  172. schemathesis/internal/__init__.py +0 -7
  173. schemathesis/internal/copy.py +0 -13
  174. schemathesis/internal/datetime.py +0 -5
  175. schemathesis/internal/deprecation.py +0 -34
  176. schemathesis/internal/jsonschema.py +0 -35
  177. schemathesis/internal/transformation.py +0 -15
  178. schemathesis/internal/validation.py +0 -34
  179. schemathesis/lazy.py +0 -361
  180. schemathesis/loaders.py +0 -120
  181. schemathesis/models.py +0 -1231
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -555
  184. schemathesis/runner/events.py +0 -309
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -986
  187. schemathesis/runner/impl/solo.py +0 -90
  188. schemathesis/runner/impl/threadpool.py +0 -400
  189. schemathesis/runner/serialization.py +0 -411
  190. schemathesis/sanitization.py +0 -248
  191. schemathesis/serializers.py +0 -315
  192. schemathesis/service/__init__.py +0 -18
  193. schemathesis/service/auth.py +0 -11
  194. schemathesis/service/ci.py +0 -201
  195. schemathesis/service/client.py +0 -100
  196. schemathesis/service/constants.py +0 -38
  197. schemathesis/service/events.py +0 -57
  198. schemathesis/service/hosts.py +0 -107
  199. schemathesis/service/metadata.py +0 -46
  200. schemathesis/service/models.py +0 -49
  201. schemathesis/service/report.py +0 -255
  202. schemathesis/service/serialization.py +0 -184
  203. schemathesis/service/usage.py +0 -65
  204. schemathesis/specs/graphql/loaders.py +0 -344
  205. schemathesis/specs/openapi/filters.py +0 -49
  206. schemathesis/specs/openapi/loaders.py +0 -667
  207. schemathesis/specs/openapi/stateful/links.py +0 -92
  208. schemathesis/specs/openapi/validation.py +0 -25
  209. schemathesis/stateful/__init__.py +0 -133
  210. schemathesis/targets.py +0 -45
  211. schemathesis/throttling.py +0 -41
  212. schemathesis/transports/__init__.py +0 -5
  213. schemathesis/transports/auth.py +0 -15
  214. schemathesis/transports/headers.py +0 -35
  215. schemathesis/transports/responses.py +0 -52
  216. schemathesis/types.py +0 -35
  217. schemathesis/utils.py +0 -169
  218. schemathesis-3.25.5.dist-info/METADATA +0 -356
  219. schemathesis-3.25.5.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1 +0,0 @@
1
- from .loaders import from_asgi, from_dict, from_file, from_path, from_url, from_wsgi
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from ...schemas import APIOperation, APIOperationMap
8
+
9
+
10
+ @dataclass
11
+ class OperationCache:
12
+ _maps: dict[str, APIOperationMap] = field(default_factory=dict)
13
+ _operations: dict[str, APIOperation] = field(default_factory=dict)
14
+
15
+ def get_map(self, key: str) -> APIOperationMap | None:
16
+ return self._maps.get(key)
17
+
18
+ def insert_map(self, key: str, value: APIOperationMap) -> None:
19
+ self._maps[key] = value
20
+
21
+ def get_operation(self, key: str) -> APIOperation | None:
22
+ return self._operations.get(key)
23
+
24
+ def insert_operation(self, key: str, value: APIOperation) -> None:
25
+ self._operations[key] = value
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
3
4
 
4
5
  if TYPE_CHECKING:
@@ -3,8 +3,7 @@ from __future__ import annotations
3
3
  from functools import lru_cache
4
4
  from typing import TYPE_CHECKING
5
5
 
6
-
7
- from ...exceptions import UsageError
6
+ from schemathesis.core.errors import IncorrectUsage
8
7
 
9
8
  if TYPE_CHECKING:
10
9
  import graphql
@@ -22,18 +21,21 @@ def scalar(name: str, strategy: st.SearchStrategy[graphql.ValueNode]) -> None:
22
21
  from hypothesis.strategies import SearchStrategy
23
22
 
24
23
  if not isinstance(name, str):
25
- raise UsageError(f"Scalar name {name!r} must be a string")
24
+ raise IncorrectUsage(f"Scalar name {name!r} must be a string")
26
25
  if not isinstance(strategy, SearchStrategy):
27
- raise UsageError(f"{strategy!r} must be a Hypothesis strategy which generates AST nodes matching this scalar")
26
+ raise IncorrectUsage(
27
+ f"{strategy!r} must be a Hypothesis strategy which generates AST nodes matching this scalar"
28
+ )
28
29
  CUSTOM_SCALARS[name] = strategy
29
30
 
30
31
 
31
32
  @lru_cache
32
33
  def get_extra_scalar_strategies() -> dict[str, st.SearchStrategy]:
33
34
  """Get all extra GraphQL strategies."""
34
- from . import nodes
35
35
  from hypothesis import strategies as st
36
36
 
37
+ from . import nodes
38
+
37
39
  dates = st.dates().map(str)
38
40
  times = st.times().map("%sZ".__mod__)
39
41
 
@@ -1,54 +1,56 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import enum
4
+ import time
4
5
  from dataclasses import dataclass, field
5
6
  from difflib import get_close_matches
6
7
  from enum import unique
8
+ from types import SimpleNamespace
7
9
  from typing import (
10
+ TYPE_CHECKING,
8
11
  Any,
9
12
  Callable,
10
13
  Generator,
11
- Sequence,
12
- TypeVar,
13
- cast,
14
- TYPE_CHECKING,
15
- NoReturn,
16
- MutableMapping,
17
14
  Iterator,
15
+ Mapping,
16
+ NoReturn,
17
+ Union,
18
+ cast,
18
19
  )
19
- from urllib.parse import urlsplit, urlunsplit
20
+ from urllib.parse import urlsplit
20
21
 
21
22
  import graphql
22
- import requests
23
- from graphql import GraphQLNamedType
24
23
  from hypothesis import strategies as st
25
- from hypothesis.strategies import SearchStrategy
26
24
  from hypothesis_graphql import strategies as gql_st
27
25
  from requests.structures import CaseInsensitiveDict
28
26
 
29
- from ..openapi.constants import LOCATION_TO_CONTAINER
30
- from ... import auths
31
- from ...auths import AuthStorage
32
- from ...checks import not_a_server_error
33
- from ...constants import NOT_SET
34
- from ...exceptions import OperationSchemaError, OperationNotFound
35
- from ...generation import DataGenerationMethod, GenerationConfig
36
- from ...hooks import (
37
- GLOBAL_HOOK_DISPATCHER,
38
- HookContext,
39
- HookDispatcher,
40
- apply_to_all_dispatchers,
41
- should_skip_operation,
27
+ from schemathesis.core import NOT_SET, NotSet, Specification
28
+ from schemathesis.core.errors import InvalidSchema, OperationNotFound
29
+ from schemathesis.core.result import Ok, Result
30
+ from schemathesis.generation.case import Case
31
+ from schemathesis.generation.meta import (
32
+ CaseMetadata,
33
+ ComponentInfo,
34
+ ComponentKind,
35
+ ExplicitPhaseData,
36
+ GeneratePhaseData,
37
+ GenerationInfo,
38
+ PhaseInfo,
39
+ TestPhase,
42
40
  )
43
- from ...internal.result import Ok, Result
44
- from ...models import APIOperation, Case, CheckFunction, OperationDefinition
45
- from ...schemas import BaseSchema, APIOperationMap
46
- from ...stateful import Stateful, StatefulTest
47
- from ...types import Body, Cookies, Headers, NotSet, PathParameters, Query
41
+
42
+ from ... import auths
43
+ from ...generation import GenerationConfig, GenerationMode
44
+ from ...hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
45
+ from ...schemas import APIOperation, APIOperationMap, ApiOperationsCount, BaseSchema, OperationDefinition
46
+ from ..openapi.constants import LOCATION_TO_CONTAINER
47
+ from ._cache import OperationCache
48
48
  from .scalars import CUSTOM_SCALARS, get_extra_scalar_strategies
49
49
 
50
50
  if TYPE_CHECKING:
51
- from ...transports.responses import GenericResponse
51
+ from hypothesis.strategies import SearchStrategy
52
+
53
+ from schemathesis.auths import AuthStorage
52
54
 
53
55
 
54
56
  @unique
@@ -58,73 +60,13 @@ class RootType(enum.Enum):
58
60
 
59
61
 
60
62
  @dataclass(repr=False)
61
- class GraphQLCase(Case):
62
- def as_requests_kwargs(self, base_url: str | None = None, headers: dict[str, str] | None = None) -> dict[str, Any]:
63
- final_headers = self._get_headers(headers)
64
- base_url = self._get_base_url(base_url)
65
- # Replace the path, in case if the user provided any path parameters via hooks
66
- parts = list(urlsplit(base_url))
67
- parts[2] = self.formatted_path
68
- kwargs: dict[str, Any] = {
69
- "method": self.method,
70
- "url": urlunsplit(parts),
71
- "headers": final_headers,
72
- "cookies": self.cookies,
73
- "params": self.query,
74
- }
75
- # There is no direct way to have bytes here, but it is a useful pattern to support.
76
- # It also unifies GraphQLCase with its Open API counterpart where bytes may come from external examples
77
- if isinstance(self.body, bytes):
78
- kwargs["data"] = self.body
79
- # Assume that the payload is JSON, not raw GraphQL queries
80
- kwargs["headers"].setdefault("Content-Type", "application/json")
81
- else:
82
- kwargs["json"] = {"query": self.body}
83
- return kwargs
84
-
85
- def as_werkzeug_kwargs(self, headers: dict[str, str] | None = None) -> dict[str, Any]:
86
- final_headers = self._get_headers(headers)
87
- return {
88
- "method": self.method,
89
- "path": self.operation.schema.get_full_path(self.formatted_path),
90
- # Convert to a regular dictionary, as we use `CaseInsensitiveDict` which is not supported by Werkzeug
91
- "headers": dict(final_headers),
92
- "query_string": self.query,
93
- "json": {"query": self.body},
94
- }
95
-
96
- def validate_response(
97
- self,
98
- response: GenericResponse,
99
- checks: tuple[CheckFunction, ...] = (),
100
- additional_checks: tuple[CheckFunction, ...] = (),
101
- excluded_checks: tuple[CheckFunction, ...] = (),
102
- code_sample_style: str | None = None,
103
- ) -> None:
104
- checks = checks or (not_a_server_error,)
105
- checks += additional_checks
106
- checks = tuple(check for check in checks if check not in excluded_checks)
107
- return super().validate_response(response, checks, code_sample_style=code_sample_style)
108
-
109
- def call_asgi(
110
- self,
111
- app: Any = None,
112
- base_url: str | None = None,
113
- headers: dict[str, str] | None = None,
114
- **kwargs: Any,
115
- ) -> requests.Response:
116
- return super().call_asgi(app=app, base_url=base_url, headers=headers, **kwargs)
117
-
118
-
119
- C = TypeVar("C", bound=Case)
120
-
121
-
122
- @dataclass
123
63
  class GraphQLOperationDefinition(OperationDefinition):
124
64
  field_name: str
125
65
  type_: graphql.GraphQLType
126
66
  root_type: RootType
127
67
 
68
+ def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
69
+
128
70
  @property
129
71
  def is_query(self) -> bool:
130
72
  return self.root_type == RootType.QUERY
@@ -136,9 +78,37 @@ class GraphQLOperationDefinition(OperationDefinition):
136
78
 
137
79
  @dataclass
138
80
  class GraphQLSchema(BaseSchema):
81
+ _operation_cache: OperationCache = field(default_factory=OperationCache)
82
+
139
83
  def __repr__(self) -> str:
140
84
  return f"<{self.__class__.__name__}>"
141
85
 
86
+ def __iter__(self) -> Iterator[str]:
87
+ schema = self.client_schema
88
+ for operation_type in (
89
+ schema.query_type,
90
+ schema.mutation_type,
91
+ ):
92
+ if operation_type is not None:
93
+ yield operation_type.name
94
+
95
+ def _get_operation_map(self, key: str) -> APIOperationMap:
96
+ cache = self._operation_cache
97
+ map = cache.get_map(key)
98
+ if map is not None:
99
+ return map
100
+ schema = self.client_schema
101
+ for root_type, operation_type in (
102
+ (RootType.QUERY, schema.query_type),
103
+ (RootType.MUTATION, schema.mutation_type),
104
+ ):
105
+ if operation_type and operation_type.name == key:
106
+ map = APIOperationMap(self, {})
107
+ map._data = FieldMap(map, root_type, operation_type)
108
+ cache.insert_map(key, map)
109
+ return map
110
+ raise KeyError(key)
111
+
142
112
  def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
143
113
  raw_schema = self.raw_schema["__schema"]
144
114
  type_names = [type_def["name"] for type_def in raw_schema.get("types", [])]
@@ -148,25 +118,12 @@ class GraphQLSchema(BaseSchema):
148
118
  message += f". Did you mean `{matches[0]}`?"
149
119
  raise OperationNotFound(message=message, item=item) from exc
150
120
 
151
- def _store_operations(
152
- self, operations: Generator[Result[APIOperation, OperationSchemaError], None, None]
153
- ) -> dict[str, APIOperationMap]:
154
- output: dict[str, APIOperationMap] = {}
155
- for result in operations:
156
- if isinstance(result, Ok):
157
- operation = result.ok()
158
- definition = cast(GraphQLOperationDefinition, operation.definition)
159
- type_name = definition.type_.name if isinstance(definition.type_, GraphQLNamedType) else "Unknown"
160
- for_type = output.setdefault(type_name, APIOperationMap(FieldMap()))
161
- for_type[definition.field_name] = operation
162
- return output
163
-
164
121
  def get_full_path(self, path: str) -> str:
165
122
  return self.base_path
166
123
 
167
124
  @property
168
- def verbose_name(self) -> str:
169
- return "GraphQL"
125
+ def specification(self) -> Specification:
126
+ return Specification.graphql(version="")
170
127
 
171
128
  @property
172
129
  def client_schema(self) -> graphql.GraphQLSchema:
@@ -183,18 +140,30 @@ class GraphQLSchema(BaseSchema):
183
140
  def _get_base_path(self) -> str:
184
141
  return cast(str, urlsplit(self.location).path)
185
142
 
186
- @property
187
- def operations_count(self) -> int:
143
+ def _do_count_operations(self) -> ApiOperationsCount:
144
+ counter = ApiOperationsCount()
188
145
  raw_schema = self.raw_schema["__schema"]
189
- total = 0
146
+ dummy_operation = APIOperation(
147
+ base_url=self.get_base_url(),
148
+ path=self.base_path,
149
+ label="",
150
+ method="POST",
151
+ schema=self,
152
+ definition=None, # type: ignore
153
+ )
154
+
190
155
  for type_name in ("queryType", "mutationType"):
191
156
  type_def = raw_schema.get(type_name)
192
157
  if type_def is not None:
193
158
  query_type_name = type_def["name"]
194
159
  for type_def in raw_schema.get("types", []):
195
160
  if type_def["name"] == query_type_name:
196
- total += len(type_def["fields"])
197
- return total
161
+ for field in type_def["fields"]:
162
+ counter.total += 1
163
+ dummy_operation.label = f"{query_type_name}.{field['name']}"
164
+ if not self._should_skip(dummy_operation):
165
+ counter.selected += 1
166
+ return counter
198
167
 
199
168
  @property
200
169
  def links_count(self) -> int:
@@ -202,8 +171,8 @@ class GraphQLSchema(BaseSchema):
202
171
  return 0
203
172
 
204
173
  def get_all_operations(
205
- self, hooks: HookDispatcher | None = None
206
- ) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
174
+ self, generation_config: GenerationConfig | None = None
175
+ ) -> Generator[Result[APIOperation, InvalidSchema], None, None]:
207
176
  schema = self.client_schema
208
177
  for root_type, operation_type in (
209
178
  (RootType.QUERY, schema.query_type),
@@ -211,114 +180,136 @@ class GraphQLSchema(BaseSchema):
211
180
  ):
212
181
  if operation_type is None:
213
182
  continue
214
- for field_name, definition in operation_type.fields.items():
215
- operation: APIOperation = APIOperation(
216
- base_url=self.get_base_url(),
217
- path=self.base_path,
218
- verbose_name=f"{operation_type.name}.{field_name}",
219
- method="POST",
220
- app=self.app,
221
- schema=self,
222
- # Parameters are not yet supported
223
- definition=GraphQLOperationDefinition(
224
- raw=definition,
225
- resolved=definition,
226
- scope="",
227
- parameters=[],
228
- type_=operation_type,
229
- field_name=field_name,
230
- root_type=root_type,
231
- ),
232
- case_cls=GraphQLCase,
233
- )
234
- context = HookContext(operation=operation)
235
- if (
236
- should_skip_operation(GLOBAL_HOOK_DISPATCHER, context)
237
- or should_skip_operation(self.hooks, context)
238
- or (hooks and should_skip_operation(hooks, context))
239
- ):
183
+ for field_name, field_ in operation_type.fields.items():
184
+ operation = self._build_operation(root_type, operation_type, field_name, field_)
185
+ if self._should_skip(operation):
240
186
  continue
241
187
  yield Ok(operation)
242
188
 
189
+ def _should_skip(
190
+ self,
191
+ operation: APIOperation,
192
+ _ctx_cache: SimpleNamespace = SimpleNamespace(operation=None),
193
+ ) -> bool:
194
+ _ctx_cache.operation = operation
195
+ return not self.filter_set.match(_ctx_cache)
196
+
197
+ def _build_operation(
198
+ self,
199
+ root_type: RootType,
200
+ operation_type: graphql.GraphQLObjectType,
201
+ field_name: str,
202
+ field: graphql.GraphQlField,
203
+ ) -> APIOperation:
204
+ return APIOperation(
205
+ base_url=self.get_base_url(),
206
+ path=self.base_path,
207
+ label=f"{operation_type.name}.{field_name}",
208
+ method="POST",
209
+ app=self.app,
210
+ schema=self,
211
+ # Parameters are not yet supported
212
+ definition=GraphQLOperationDefinition(
213
+ raw=field,
214
+ resolved=field,
215
+ scope="",
216
+ type_=operation_type,
217
+ field_name=field_name,
218
+ root_type=root_type,
219
+ ),
220
+ )
221
+
243
222
  def get_case_strategy(
244
223
  self,
245
224
  operation: APIOperation,
246
225
  hooks: HookDispatcher | None = None,
247
226
  auth_storage: AuthStorage | None = None,
248
- data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
227
+ generation_mode: GenerationMode = GenerationMode.default(),
249
228
  generation_config: GenerationConfig | None = None,
250
229
  **kwargs: Any,
251
230
  ) -> SearchStrategy:
252
- return get_case_strategy(
231
+ return graphql_cases(
253
232
  operation=operation,
254
- client_schema=self.client_schema,
255
233
  hooks=hooks,
256
234
  auth_storage=auth_storage,
257
- data_generation_method=data_generation_method,
235
+ generation_mode=generation_mode,
258
236
  generation_config=generation_config or self.generation_config,
259
237
  **kwargs,
260
238
  )
261
239
 
262
- def get_strategies_from_examples(self, operation: APIOperation) -> list[SearchStrategy[Case]]:
263
- return []
264
-
265
- def get_stateful_tests(
266
- self, response: GenericResponse, operation: APIOperation, stateful: Stateful | None
267
- ) -> Sequence[StatefulTest]:
240
+ def get_strategies_from_examples(self, operation: APIOperation, **kwargs: Any) -> list[SearchStrategy[Case]]:
268
241
  return []
269
242
 
270
243
  def make_case(
271
244
  self,
272
245
  *,
273
- case_cls: type[C],
274
246
  operation: APIOperation,
275
- path_parameters: PathParameters | None = None,
276
- headers: Headers | None = None,
277
- cookies: Cookies | None = None,
278
- query: Query | None = None,
279
- body: Body | NotSet = NOT_SET,
247
+ method: str | None = None,
248
+ path: str | None = None,
249
+ path_parameters: dict[str, Any] | None = None,
250
+ headers: dict[str, Any] | None = None,
251
+ cookies: dict[str, Any] | None = None,
252
+ query: dict[str, Any] | None = None,
253
+ body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET,
280
254
  media_type: str | None = None,
281
- ) -> C:
282
- return case_cls(
255
+ meta: CaseMetadata | None = None,
256
+ ) -> Case:
257
+ return Case(
283
258
  operation=operation,
259
+ method=method or operation.method.upper(),
260
+ path=path or operation.path,
284
261
  path_parameters=path_parameters,
285
262
  headers=CaseInsensitiveDict(headers) if headers is not None else headers,
286
263
  cookies=cookies,
287
264
  query=query,
288
265
  body=body,
289
- media_type=media_type,
266
+ media_type=media_type or "application/json",
267
+ meta=meta,
290
268
  )
291
269
 
292
270
  def get_tags(self, operation: APIOperation) -> list[str] | None:
293
271
  return None
294
272
 
273
+ def validate(self) -> None:
274
+ return None
275
+
295
276
 
296
277
  @dataclass
297
- class FieldMap(MutableMapping):
278
+ class FieldMap(Mapping):
298
279
  """Container for accessing API operations.
299
280
 
300
281
  Provides a more specific error message if API operation is not found.
301
282
  """
302
283
 
303
- data: dict[str, APIOperation] = field(default_factory=dict)
304
-
305
- def __setitem__(self, key: str, value: APIOperation) -> None:
306
- self.data[key] = value
284
+ _parent: APIOperationMap
285
+ _root_type: RootType
286
+ _operation_type: graphql.GraphQLObjectType
307
287
 
308
- def __delitem__(self, key: str) -> None:
309
- del self.data[key]
288
+ __slots__ = ("_parent", "_root_type", "_operation_type")
310
289
 
311
290
  def __len__(self) -> int:
312
- return len(self.data)
291
+ return len(self._operation_type.fields)
313
292
 
314
293
  def __iter__(self) -> Iterator[str]:
315
- return iter(self.data)
294
+ return iter(self._operation_type.fields)
295
+
296
+ def _init_operation(self, field_name: str) -> APIOperation:
297
+ schema = cast(GraphQLSchema, self._parent._schema)
298
+ cache = schema._operation_cache
299
+ operation = cache.get_operation(field_name)
300
+ if operation is not None:
301
+ return operation
302
+ operation_type = self._operation_type
303
+ field_ = operation_type.fields[field_name]
304
+ operation = schema._build_operation(self._root_type, operation_type, field_name, field_)
305
+ cache.insert_operation(field_name, operation)
306
+ return operation
316
307
 
317
308
  def __getitem__(self, item: str) -> APIOperation:
318
309
  try:
319
- return self.data[item]
310
+ return self._init_operation(item)
320
311
  except KeyError as exc:
321
- field_names = [operation.definition.field_name for operation in self.data.values()] # type: ignore[attr-defined]
312
+ field_names = list(self._operation_type.fields)
322
313
  matches = get_close_matches(item, field_names)
323
314
  message = f"`{item}` field not found"
324
315
  if matches:
@@ -327,48 +318,77 @@ class FieldMap(MutableMapping):
327
318
 
328
319
 
329
320
  @st.composite # type: ignore
330
- def get_case_strategy(
321
+ def graphql_cases(
331
322
  draw: Callable,
323
+ *,
332
324
  operation: APIOperation,
333
- client_schema: graphql.GraphQLSchema,
334
325
  hooks: HookDispatcher | None = None,
335
- auth_storage: AuthStorage | None = None,
336
- data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
337
- generation_config: GenerationConfig | None = None,
338
- **kwargs: Any,
326
+ auth_storage: auths.AuthStorage | None = None,
327
+ generation_mode: GenerationMode = GenerationMode.default(),
328
+ generation_config: GenerationConfig,
329
+ path_parameters: NotSet | dict[str, Any] = NOT_SET,
330
+ headers: NotSet | dict[str, Any] = NOT_SET,
331
+ cookies: NotSet | dict[str, Any] = NOT_SET,
332
+ query: NotSet | dict[str, Any] = NOT_SET,
333
+ body: Any = NOT_SET,
334
+ media_type: str | None = None,
335
+ phase: TestPhase = TestPhase.GENERATE,
339
336
  ) -> Any:
337
+ start = time.monotonic()
340
338
  definition = cast(GraphQLOperationDefinition, operation.definition)
341
339
  strategy_factory = {
342
340
  RootType.QUERY: gql_st.queries,
343
341
  RootType.MUTATION: gql_st.mutations,
344
342
  }[definition.root_type]
345
343
  hook_context = HookContext(operation)
346
- generation_config = generation_config or GenerationConfig()
347
344
  custom_scalars = {**get_extra_scalar_strategies(), **CUSTOM_SCALARS}
348
345
  strategy = strategy_factory(
349
- client_schema,
346
+ operation.schema.client_schema, # type: ignore[attr-defined]
350
347
  fields=[definition.field_name],
351
348
  custom_scalars=custom_scalars,
352
349
  print_ast=_noop, # type: ignore
353
350
  allow_x00=generation_config.allow_x00,
351
+ allow_null=generation_config.graphql_allow_null,
354
352
  codec=generation_config.codec,
355
353
  )
356
354
  strategy = apply_to_all_dispatchers(operation, hook_context, hooks, strategy, "body").map(graphql.print_ast)
357
355
  body = draw(strategy)
358
356
 
359
- path_parameters_ = _generate_parameter("path", draw, operation, hook_context, hooks)
360
- headers_ = _generate_parameter("header", draw, operation, hook_context, hooks)
361
- cookies_ = _generate_parameter("cookie", draw, operation, hook_context, hooks)
362
- query_ = _generate_parameter("query", draw, operation, hook_context, hooks)
363
-
364
- instance = GraphQLCase(
357
+ path_parameters_ = _generate_parameter("path", path_parameters, draw, operation, hook_context, hooks)
358
+ headers_ = _generate_parameter("header", headers, draw, operation, hook_context, hooks)
359
+ cookies_ = _generate_parameter("cookie", cookies, draw, operation, hook_context, hooks)
360
+ query_ = _generate_parameter("query", query, draw, operation, hook_context, hooks)
361
+
362
+ _phase_data = {
363
+ TestPhase.EXPLICIT: ExplicitPhaseData(),
364
+ TestPhase.GENERATE: GeneratePhaseData(),
365
+ }[phase]
366
+ phase_data = cast(Union[ExplicitPhaseData, GeneratePhaseData], _phase_data)
367
+ instance = operation.Case(
365
368
  path_parameters=path_parameters_,
366
369
  headers=headers_,
367
370
  cookies=cookies_,
368
371
  query=query_,
369
372
  body=body,
370
- operation=operation,
371
- data_generation_method=data_generation_method,
373
+ meta=CaseMetadata(
374
+ generation=GenerationInfo(
375
+ time=time.monotonic() - start,
376
+ mode=generation_mode,
377
+ ),
378
+ phase=PhaseInfo(name=phase, data=phase_data),
379
+ components={
380
+ kind: ComponentInfo(mode=generation_mode)
381
+ for kind, value in [
382
+ (ComponentKind.QUERY, query_),
383
+ (ComponentKind.PATH_PARAMETERS, path_parameters_),
384
+ (ComponentKind.HEADERS, headers_),
385
+ (ComponentKind.COOKIES, cookies_),
386
+ (ComponentKind.BODY, body),
387
+ ]
388
+ if value is not NOT_SET
389
+ },
390
+ ),
391
+ media_type=media_type or "application/json",
372
392
  ) # type: ignore
373
393
  context = auths.AuthContext(
374
394
  operation=operation,
@@ -379,11 +399,19 @@ def get_case_strategy(
379
399
 
380
400
 
381
401
  def _generate_parameter(
382
- location: str, draw: Callable, operation: APIOperation, context: HookContext, hooks: HookDispatcher | None
402
+ location: str,
403
+ explicit: NotSet | dict[str, Any],
404
+ draw: Callable,
405
+ operation: APIOperation,
406
+ context: HookContext,
407
+ hooks: HookDispatcher | None,
383
408
  ) -> Any:
384
409
  # Schemathesis does not generate anything but `body` for GraphQL, hence use `None`
385
410
  container = LOCATION_TO_CONTAINER[location]
386
- strategy = apply_to_all_dispatchers(operation, context, hooks, st.none(), container)
411
+ if isinstance(explicit, NotSet):
412
+ strategy = apply_to_all_dispatchers(operation, context, hooks, st.none(), container)
413
+ else:
414
+ strategy = apply_to_all_dispatchers(operation, context, hooks, st.just(explicit), container)
387
415
  return draw(strategy)
388
416
 
389
417