schemathesis 3.21.2__py3-none-any.whl → 3.22.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 (95) hide show
  1. schemathesis/__init__.py +1 -1
  2. schemathesis/_compat.py +2 -18
  3. schemathesis/_dependency_versions.py +1 -6
  4. schemathesis/_hypothesis.py +15 -12
  5. schemathesis/_lazy_import.py +3 -2
  6. schemathesis/_xml.py +12 -11
  7. schemathesis/auths.py +88 -81
  8. schemathesis/checks.py +4 -4
  9. schemathesis/cli/__init__.py +202 -171
  10. schemathesis/cli/callbacks.py +29 -32
  11. schemathesis/cli/cassettes.py +25 -25
  12. schemathesis/cli/context.py +18 -12
  13. schemathesis/cli/junitxml.py +2 -2
  14. schemathesis/cli/options.py +10 -11
  15. schemathesis/cli/output/default.py +64 -34
  16. schemathesis/code_samples.py +10 -10
  17. schemathesis/constants.py +1 -1
  18. schemathesis/contrib/unique_data.py +2 -2
  19. schemathesis/exceptions.py +55 -42
  20. schemathesis/extra/_aiohttp.py +2 -2
  21. schemathesis/extra/_flask.py +2 -2
  22. schemathesis/extra/_server.py +3 -2
  23. schemathesis/extra/pytest_plugin.py +10 -10
  24. schemathesis/failures.py +16 -16
  25. schemathesis/filters.py +40 -41
  26. schemathesis/fixups/__init__.py +4 -3
  27. schemathesis/fixups/fast_api.py +5 -4
  28. schemathesis/generation/__init__.py +16 -4
  29. schemathesis/hooks.py +25 -25
  30. schemathesis/internal/jsonschema.py +4 -3
  31. schemathesis/internal/transformation.py +3 -2
  32. schemathesis/lazy.py +39 -31
  33. schemathesis/loaders.py +8 -8
  34. schemathesis/models.py +128 -126
  35. schemathesis/parameters.py +6 -5
  36. schemathesis/runner/__init__.py +107 -81
  37. schemathesis/runner/events.py +37 -26
  38. schemathesis/runner/impl/core.py +86 -81
  39. schemathesis/runner/impl/solo.py +19 -15
  40. schemathesis/runner/impl/threadpool.py +40 -22
  41. schemathesis/runner/serialization.py +67 -40
  42. schemathesis/sanitization.py +18 -20
  43. schemathesis/schemas.py +83 -72
  44. schemathesis/serializers.py +39 -30
  45. schemathesis/service/ci.py +20 -21
  46. schemathesis/service/client.py +29 -9
  47. schemathesis/service/constants.py +1 -0
  48. schemathesis/service/events.py +2 -2
  49. schemathesis/service/hosts.py +8 -7
  50. schemathesis/service/metadata.py +5 -0
  51. schemathesis/service/models.py +22 -4
  52. schemathesis/service/report.py +15 -15
  53. schemathesis/service/serialization.py +23 -27
  54. schemathesis/service/usage.py +8 -7
  55. schemathesis/specs/graphql/loaders.py +31 -24
  56. schemathesis/specs/graphql/nodes.py +3 -2
  57. schemathesis/specs/graphql/scalars.py +26 -2
  58. schemathesis/specs/graphql/schemas.py +38 -34
  59. schemathesis/specs/openapi/_hypothesis.py +62 -44
  60. schemathesis/specs/openapi/checks.py +10 -10
  61. schemathesis/specs/openapi/converter.py +10 -9
  62. schemathesis/specs/openapi/definitions.py +2 -2
  63. schemathesis/specs/openapi/examples.py +22 -21
  64. schemathesis/specs/openapi/expressions/nodes.py +5 -4
  65. schemathesis/specs/openapi/expressions/parser.py +7 -6
  66. schemathesis/specs/openapi/filters.py +6 -6
  67. schemathesis/specs/openapi/formats.py +2 -2
  68. schemathesis/specs/openapi/links.py +19 -21
  69. schemathesis/specs/openapi/loaders.py +133 -78
  70. schemathesis/specs/openapi/negative/__init__.py +16 -11
  71. schemathesis/specs/openapi/negative/mutations.py +11 -10
  72. schemathesis/specs/openapi/parameters.py +20 -19
  73. schemathesis/specs/openapi/references.py +21 -20
  74. schemathesis/specs/openapi/schemas.py +97 -84
  75. schemathesis/specs/openapi/security.py +25 -24
  76. schemathesis/specs/openapi/serialization.py +20 -23
  77. schemathesis/specs/openapi/stateful/__init__.py +12 -11
  78. schemathesis/specs/openapi/stateful/links.py +7 -7
  79. schemathesis/specs/openapi/utils.py +4 -3
  80. schemathesis/specs/openapi/validation.py +3 -2
  81. schemathesis/stateful/__init__.py +15 -16
  82. schemathesis/stateful/state_machine.py +9 -9
  83. schemathesis/targets.py +3 -3
  84. schemathesis/throttling.py +2 -2
  85. schemathesis/transports/auth.py +2 -2
  86. schemathesis/transports/content_types.py +5 -0
  87. schemathesis/transports/headers.py +3 -2
  88. schemathesis/transports/responses.py +1 -1
  89. schemathesis/utils.py +7 -10
  90. {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/METADATA +12 -13
  91. schemathesis-3.22.1.dist-info/RECORD +130 -0
  92. schemathesis-3.21.2.dist-info/RECORD +0 -130
  93. {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/WHEEL +0 -0
  94. {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/entry_points.txt +0 -0
  95. {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,8 @@
1
+ from __future__ import annotations
1
2
  import hashlib
2
3
  import http
3
4
  from dataclasses import asdict
4
- from typing import Any, Optional, Union
5
+ from typing import Any
5
6
  from urllib.parse import urljoin
6
7
 
7
8
  import requests
@@ -11,7 +12,15 @@ from ..constants import USER_AGENT
11
12
  from .ci import CIProvider
12
13
  from .constants import CI_PROVIDER_HEADER, REPORT_CORRELATION_ID_HEADER, REQUEST_TIMEOUT, UPLOAD_SOURCE_HEADER
13
14
  from .metadata import Metadata
14
- from .models import ApiDetails, AuthResponse, FailedUploadResponse, UploadResponse, UploadSource
15
+ from .models import (
16
+ ProjectDetails,
17
+ AuthResponse,
18
+ FailedUploadResponse,
19
+ UploadResponse,
20
+ UploadSource,
21
+ ProjectEnvironment,
22
+ Specification,
23
+ )
15
24
 
16
25
 
17
26
  def response_hook(response: requests.Response, **_kwargs: Any) -> None:
@@ -22,7 +31,7 @@ def response_hook(response: requests.Response, **_kwargs: Any) -> None:
22
31
  class ServiceClient(requests.Session):
23
32
  """A more convenient session to send requests to Schemathesis.io."""
24
33
 
25
- def __init__(self, base_url: str, token: Optional[str], *, timeout: int = REQUEST_TIMEOUT, verify: bool = True):
34
+ def __init__(self, base_url: str, token: str | None, *, timeout: int = REQUEST_TIMEOUT, verify: bool = True):
26
35
  super().__init__()
27
36
  self.timeout = timeout
28
37
  self.verify = verify
@@ -43,11 +52,22 @@ class ServiceClient(requests.Session):
43
52
  url = urljoin(self.base_url, url)
44
53
  return super().request(method, url, *args, **kwargs)
45
54
 
46
- def get_api_details(self, name: str) -> ApiDetails:
55
+ def get_api_details(self, name: str) -> ProjectDetails:
47
56
  """Get information about an API."""
48
- response = self.get(f"/apis/{name}/")
57
+ response = self.get(f"/cli/projects/{name}/")
49
58
  data = response.json()
50
- return ApiDetails(location=data["location"], base_url=data["base_url"])
59
+ return ProjectDetails(
60
+ environments=[
61
+ ProjectEnvironment(
62
+ url=environment["url"],
63
+ name=environment["name"],
64
+ description=environment["description"],
65
+ is_default=environment["is_default"],
66
+ )
67
+ for environment in data["environments"]
68
+ ],
69
+ specification=Specification(schema=data["specification"]["schema"]),
70
+ )
51
71
 
52
72
  def login(self, metadata: Metadata) -> AuthResponse:
53
73
  """Send a login request."""
@@ -58,10 +78,10 @@ class ServiceClient(requests.Session):
58
78
  def upload_report(
59
79
  self,
60
80
  report: bytes,
61
- correlation_id: Optional[str] = None,
62
- ci_provider: Optional[CIProvider] = None,
81
+ correlation_id: str | None = None,
82
+ ci_provider: CIProvider | None = None,
63
83
  source: UploadSource = UploadSource.DEFAULT,
64
- ) -> Union[UploadResponse, FailedUploadResponse]:
84
+ ) -> UploadResponse | FailedUploadResponse:
65
85
  """Upload test run report to Schemathesis.io."""
66
86
  headers = {
67
87
  "Content-Type": "application/x-gtar",
@@ -35,3 +35,4 @@ HOSTS_PATH_ENV_VAR = "SCHEMATHESIS_HOSTS_PATH"
35
35
  URL_ENV_VAR = "SCHEMATHESIS_URL"
36
36
  REPORT_ENV_VAR = "SCHEMATHESIS_REPORT"
37
37
  TELEMETRY_ENV_VAR = "SCHEMATHESIS_TELEMETRY"
38
+ DOCKER_IMAGE_ENV_VAR = "SCHEMATHESIS_DOCKER_IMAGE"
@@ -1,5 +1,5 @@
1
+ from __future__ import annotations
1
2
  from dataclasses import dataclass
2
- from typing import Optional
3
3
 
4
4
  from . import ci
5
5
  from ..exceptions import format_exception
@@ -21,7 +21,7 @@ class Metadata(Event):
21
21
  """Meta-information about the report."""
22
22
 
23
23
  size: int
24
- ci_environment: Optional[ci.Environment]
24
+ ci_environment: ci.Environment | None
25
25
 
26
26
 
27
27
  @dataclass
@@ -1,9 +1,10 @@
1
1
  """Work with stored auth data."""
2
+ from __future__ import annotations
2
3
  import enum
3
4
  import tempfile
4
5
  from dataclasses import dataclass
5
6
  from pathlib import Path
6
- from typing import Any, Dict, Optional
7
+ from typing import Any
7
8
 
8
9
  import tomli
9
10
  import tomli_w
@@ -19,11 +20,11 @@ class HostData:
19
20
  hostname: str
20
21
  hosts_file: PathLike
21
22
 
22
- def load(self) -> Dict[str, Any]:
23
+ def load(self) -> dict[str, Any]:
23
24
  return load(self.hosts_file).get(self.hostname, {})
24
25
 
25
26
  @property
26
- def correlation_id(self) -> Optional[str]:
27
+ def correlation_id(self) -> str | None:
27
28
  return self.load().get("correlation_id")
28
29
 
29
30
  def store_correlation_id(self, correlation_id: str) -> None:
@@ -43,7 +44,7 @@ def store(token: str, hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = D
43
44
  _dump_hosts(hosts_file, hosts)
44
45
 
45
46
 
46
- def load(path: PathLike) -> Dict[str, Any]:
47
+ def load(path: PathLike) -> dict[str, Any]:
47
48
  """Load the given hosts file.
48
49
 
49
50
  Return an empty dict if it doesn't exist.
@@ -67,7 +68,7 @@ def _try_make_config_directory(path: PathLike) -> None:
67
68
  pass
68
69
 
69
70
 
70
- def load_for_host(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> Dict[str, Any]:
71
+ def load_for_host(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> dict[str, Any]:
71
72
  """Load all data associated with a hostname."""
72
73
  return load(hosts_file).get(hostname, {})
73
74
 
@@ -97,7 +98,7 @@ def remove(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOST
97
98
  return RemoveAuth.error
98
99
 
99
100
 
100
- def get_token(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> Optional[str]:
101
+ def get_token(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> str | None:
101
102
  """Load a token for a host."""
102
103
  return load_for_host(hostname, hosts_file).get("token")
103
104
 
@@ -107,7 +108,7 @@ def get_temporary_hosts_file() -> str:
107
108
  return str(temporary_dir / "schemathesis-hosts.toml")
108
109
 
109
110
 
110
- def _dump_hosts(path: PathLike, hosts: Dict[str, Any]) -> None:
111
+ def _dump_hosts(path: PathLike, hosts: dict[str, Any]) -> None:
111
112
  """Write hosts data to a file."""
112
113
  with open(path, "wb") as fd:
113
114
  tomli_w.dump(hosts, fd)
@@ -1,8 +1,11 @@
1
1
  """Useful info to collect from CLI usage."""
2
+ from __future__ import annotations
3
+ import os
2
4
  import platform
3
5
  from dataclasses import dataclass, field
4
6
 
5
7
  from ..constants import SCHEMATHESIS_VERSION
8
+ from .constants import DOCKER_IMAGE_ENV_VAR
6
9
 
7
10
 
8
11
  @dataclass
@@ -39,3 +42,5 @@ class Metadata:
39
42
  interpreter: InterpreterMetadata = field(default_factory=InterpreterMetadata)
40
43
  # CLI info itself.
41
44
  cli: CliMetadata = field(default_factory=CliMetadata)
45
+ # Used Docker image if any
46
+ docker_image: str | None = field(default_factory=lambda: os.getenv(DOCKER_IMAGE_ENV_VAR))
@@ -1,6 +1,7 @@
1
+ from __future__ import annotations
1
2
  from dataclasses import dataclass
2
3
  from enum import Enum
3
- from typing import Optional
4
+ from typing import Any
4
5
 
5
6
 
6
7
  class UploadSource(str, Enum):
@@ -9,9 +10,26 @@ class UploadSource(str, Enum):
9
10
 
10
11
 
11
12
  @dataclass
12
- class ApiDetails:
13
- location: str
14
- base_url: Optional[str]
13
+ class ProjectDetails:
14
+ environments: list[ProjectEnvironment]
15
+ specification: Specification
16
+
17
+ @property
18
+ def default_environment(self) -> ProjectEnvironment | None:
19
+ return next((env for env in self.environments if env.is_default), None)
20
+
21
+
22
+ @dataclass
23
+ class ProjectEnvironment:
24
+ url: str
25
+ name: str
26
+ description: str
27
+ is_default: bool
28
+
29
+
30
+ @dataclass
31
+ class Specification:
32
+ schema: dict[str, Any]
15
33
 
16
34
 
17
35
  @dataclass
@@ -9,7 +9,7 @@ from contextlib import suppress
9
9
  from dataclasses import asdict, dataclass, field
10
10
  from io import BytesIO
11
11
  from queue import Queue
12
- from typing import Any, Dict, Optional, TYPE_CHECKING
12
+ from typing import Any, TYPE_CHECKING
13
13
 
14
14
  import click
15
15
 
@@ -51,13 +51,13 @@ class ReportWriter:
51
51
  def add_metadata(
52
52
  self,
53
53
  *,
54
- api_name: Optional[str],
54
+ api_name: str | None,
55
55
  location: str,
56
- base_url: str,
56
+ base_url: str | None,
57
57
  started_at: str,
58
58
  metadata: Metadata,
59
- ci_environment: Optional[ci.Environment],
60
- usage_data: Optional[Dict[str, Any]],
59
+ ci_environment: ci.Environment | None,
60
+ usage_data: dict[str, Any] | None,
61
61
  ) -> None:
62
62
  data = {
63
63
  # API identifier on the Schemathesis.io side (optional)
@@ -105,9 +105,9 @@ class BaseReportHandler(EventHandler):
105
105
  class ServiceReportHandler(BaseReportHandler):
106
106
  client: ServiceClient
107
107
  host_data: HostData
108
- api_name: Optional[str]
108
+ api_name: str | None
109
109
  location: str
110
- base_url: Optional[str]
110
+ base_url: str | None
111
111
  started_at: str
112
112
  telemetry: bool
113
113
  out_queue: Queue
@@ -156,13 +156,13 @@ def consume_events(writer: ReportWriter, in_queue: Queue) -> ConsumeResult:
156
156
  def write_remote(
157
157
  client: ServiceClient,
158
158
  host_data: HostData,
159
- api_name: Optional[str],
159
+ api_name: str | None,
160
160
  location: str,
161
- base_url: str,
161
+ base_url: str | None,
162
162
  started_at: str,
163
163
  in_queue: Queue,
164
164
  out_queue: Queue,
165
- usage_data: Optional[Dict[str, Any]],
165
+ usage_data: dict[str, Any] | None,
166
166
  ) -> None:
167
167
  """Create a compressed ``tar.gz`` file during the run & upload it to Schemathesis.io when the run is finished."""
168
168
  payload = BytesIO()
@@ -199,9 +199,9 @@ def write_remote(
199
199
  @dataclass
200
200
  class FileReportHandler(BaseReportHandler):
201
201
  file_handle: click.utils.LazyFile
202
- api_name: Optional[str]
202
+ api_name: str | None
203
203
  location: str
204
- base_url: Optional[str]
204
+ base_url: str | None
205
205
  started_at: str
206
206
  telemetry: bool
207
207
  out_queue: Queue
@@ -227,13 +227,13 @@ class FileReportHandler(BaseReportHandler):
227
227
 
228
228
  def write_file(
229
229
  file_handle: click.utils.LazyFile,
230
- api_name: Optional[str],
230
+ api_name: str | None,
231
231
  location: str,
232
- base_url: str,
232
+ base_url: str | None,
233
233
  started_at: str,
234
234
  in_queue: Queue,
235
235
  out_queue: Queue,
236
- usage_data: Optional[Dict[str, Any]],
236
+ usage_data: dict[str, Any] | None,
237
237
  ) -> None:
238
238
  with file_handle.open() as fileobj, tarfile.open(mode="w:gz", fileobj=fileobj) as tar:
239
239
  writer = ReportWriter(tar)
@@ -1,5 +1,6 @@
1
+ from __future__ import annotations
1
2
  from dataclasses import asdict
2
- from typing import Any, Callable, Dict, List, Optional, TypeVar, cast
3
+ from typing import Any, Callable, Dict, Optional, TypeVar, cast
3
4
 
4
5
  from ..models import Response
5
6
  from ..runner import events
@@ -10,7 +11,7 @@ S = TypeVar("S", bound=events.ExecutionEvent)
10
11
  SerializeFunc = Callable[[S], Optional[Dict[str, Any]]]
11
12
 
12
13
 
13
- def serialize_initialized(event: events.Initialized) -> Optional[Dict[str, Any]]:
14
+ def serialize_initialized(event: events.Initialized) -> dict[str, Any] | None:
14
15
  return {
15
16
  "operations_count": event.operations_count,
16
17
  "location": event.location or "",
@@ -18,7 +19,7 @@ def serialize_initialized(event: events.Initialized) -> Optional[Dict[str, Any]]
18
19
  }
19
20
 
20
21
 
21
- def serialize_before_execution(event: events.BeforeExecution) -> Optional[Dict[str, Any]]:
22
+ def serialize_before_execution(event: events.BeforeExecution) -> dict[str, Any] | None:
22
23
  return {
23
24
  "correlation_id": event.correlation_id,
24
25
  "verbose_name": event.verbose_name,
@@ -26,7 +27,7 @@ def serialize_before_execution(event: events.BeforeExecution) -> Optional[Dict[s
26
27
  }
27
28
 
28
29
 
29
- def _serialize_case(case: SerializedCase) -> Dict[str, Any]:
30
+ def _serialize_case(case: SerializedCase) -> dict[str, Any]:
30
31
  return {
31
32
  "verbose_name": case.verbose_name,
32
33
  "path_template": case.path_template,
@@ -37,7 +38,7 @@ def _serialize_case(case: SerializedCase) -> Dict[str, Any]:
37
38
  }
38
39
 
39
40
 
40
- def _serialize_response(response: Response) -> Dict[str, Any]:
41
+ def _serialize_response(response: Response) -> dict[str, Any]:
41
42
  return {
42
43
  "status_code": response.status_code,
43
44
  "headers": response.headers,
@@ -47,7 +48,7 @@ def _serialize_response(response: Response) -> Dict[str, Any]:
47
48
  }
48
49
 
49
50
 
50
- def serialize_after_execution(event: events.AfterExecution) -> Optional[Dict[str, Any]]:
51
+ def serialize_after_execution(event: events.AfterExecution) -> dict[str, Any] | None:
51
52
  return {
52
53
  "correlation_id": event.correlation_id,
53
54
  "verbose_name": event.verbose_name,
@@ -76,22 +77,17 @@ def serialize_after_execution(event: events.AfterExecution) -> Optional[Dict[str
76
77
  }
77
78
  for check in event.result.checks
78
79
  ],
79
- "errors": [
80
- {
81
- "exception": error.exception,
82
- "exception_with_traceback": error.exception_with_traceback,
83
- }
84
- for error in event.result.errors
85
- ],
80
+ "errors": [asdict(error) for error in event.result.errors],
81
+ "skip_reason": event.result.skip_reason,
86
82
  },
87
83
  }
88
84
 
89
85
 
90
- def serialize_interrupted(_: events.Interrupted) -> Optional[Dict[str, Any]]:
86
+ def serialize_interrupted(_: events.Interrupted) -> dict[str, Any] | None:
91
87
  return None
92
88
 
93
89
 
94
- def serialize_internal_error(event: events.InternalError) -> Optional[Dict[str, Any]]:
90
+ def serialize_internal_error(event: events.InternalError) -> dict[str, Any] | None:
95
91
  return {
96
92
  "type": event.type.value,
97
93
  "subtype": event.subtype.value if event.subtype else event.subtype,
@@ -104,7 +100,7 @@ def serialize_internal_error(event: events.InternalError) -> Optional[Dict[str,
104
100
  }
105
101
 
106
102
 
107
- def serialize_finished(event: events.Finished) -> Optional[Dict[str, Any]]:
103
+ def serialize_finished(event: events.Finished) -> dict[str, Any] | None:
108
104
  return {
109
105
  "generic_errors": [
110
106
  {
@@ -131,14 +127,14 @@ SERIALIZER_MAP = {
131
127
  def serialize_event(
132
128
  event: events.ExecutionEvent,
133
129
  *,
134
- on_initialized: Optional[SerializeFunc] = None,
135
- on_before_execution: Optional[SerializeFunc] = None,
136
- on_after_execution: Optional[SerializeFunc] = None,
137
- on_interrupted: Optional[SerializeFunc] = None,
138
- on_internal_error: Optional[SerializeFunc] = None,
139
- on_finished: Optional[SerializeFunc] = None,
140
- extra: Optional[Dict[str, Any]] = None,
141
- ) -> Dict[str, Optional[Dict[str, Any]]]:
130
+ on_initialized: SerializeFunc | None = None,
131
+ on_before_execution: SerializeFunc | None = None,
132
+ on_after_execution: SerializeFunc | None = None,
133
+ on_interrupted: SerializeFunc | None = None,
134
+ on_internal_error: SerializeFunc | None = None,
135
+ on_finished: SerializeFunc | None = None,
136
+ extra: dict[str, Any] | None = None,
137
+ ) -> dict[str, dict[str, Any] | None]:
142
138
  """Turn an event into JSON-serializable structure."""
143
139
  # Use the explicitly provided serializer for this event and fallback to default one if it is not provided
144
140
  serializer = {
@@ -163,7 +159,7 @@ def serialize_event(
163
159
  return {event.__class__.__name__: data}
164
160
 
165
161
 
166
- def stringify_path_parameters(path_parameters: Optional[Dict[str, Any]]) -> Dict[str, str]:
162
+ def stringify_path_parameters(path_parameters: dict[str, Any] | None) -> dict[str, str]:
167
163
  """Cast all path parameter values to strings.
168
164
 
169
165
  Path parameter values may be of arbitrary type, but to display them properly they should be casted to strings.
@@ -171,14 +167,14 @@ def stringify_path_parameters(path_parameters: Optional[Dict[str, Any]]) -> Dict
171
167
  return {key: str(value) for key, value in (path_parameters or {}).items()}
172
168
 
173
169
 
174
- def prepare_query(query: Optional[Dict[str, Any]]) -> Dict[str, List[str]]:
170
+ def prepare_query(query: dict[str, Any] | None) -> dict[str, list[str]]:
175
171
  """Convert all query values to list of strings.
176
172
 
177
173
  Query parameters may be generated in different shapes, including integers, strings, list of strings, etc.
178
174
  It can also be an object, if the schema contains an object, but `style` and `explode` combo is not applicable.
179
175
  """
180
176
 
181
- def to_list_of_strings(value: Any) -> List[str]:
177
+ def to_list_of_strings(value: Any) -> list[str]:
182
178
  if isinstance(value, list):
183
179
  return list(map(str, value))
184
180
  if isinstance(value, str):
@@ -1,5 +1,6 @@
1
+ from __future__ import annotations
1
2
  import sys
2
- from typing import Any, Dict, List, Optional, Tuple
3
+ from typing import Any
3
4
 
4
5
  import click
5
6
  from click.types import StringParamType
@@ -7,14 +8,14 @@ from click.types import StringParamType
7
8
  from .. import cli, hooks
8
9
 
9
10
 
10
- def collect(args: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
11
+ def collect(args: list[str] | None = None) -> dict[str, Any] | None:
11
12
  """Collect anonymized CLI usage data."""
12
- context: Optional[click.Context] = click.get_current_context(silent=True)
13
+ context: click.Context | None = click.get_current_context(silent=True)
13
14
  if context is not None and not sys.argv[0].endswith("pytest"):
14
15
  args = args or sys.argv[2:]
15
16
  parameters, _, types = parse_cli_args(context, args)
16
- parameters_data: Dict[str, Dict[str, Any]] = {}
17
- used_headers: List[str] = []
17
+ parameters_data: dict[str, dict[str, Any]] = {}
18
+ used_headers: list[str] = []
18
19
  schema = parameters["schema"]
19
20
  app = parameters.get("app")
20
21
  if not schema:
@@ -43,7 +44,7 @@ def collect(args: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
43
44
  return None
44
45
 
45
46
 
46
- def _collect_option(option: str, option_type: click.Parameter, used_headers: List[str], value: Any) -> Dict[str, Any]:
47
+ def _collect_option(option: str, option_type: click.Parameter, used_headers: list[str], value: Any) -> dict[str, Any]:
47
48
  entry = {}
48
49
  if isinstance(option_type.type, (StringParamType, click.types.File)):
49
50
  if option == "headers" and value:
@@ -59,6 +60,6 @@ def _collect_option(option: str, option_type: click.Parameter, used_headers: Lis
59
60
  return entry
60
61
 
61
62
 
62
- def parse_cli_args(context: click.Context, args: List[str]) -> Tuple[Dict[str, Any], List, List[click.Parameter]]:
63
+ def parse_cli_args(context: click.Context, args: list[str]) -> tuple[dict[str, Any], list, list[click.Parameter]]:
63
64
  parser = cli.run.make_parser(context)
64
65
  return parser.parse_args(args=args)
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
  import pathlib
3
3
  from functools import lru_cache
4
4
  from json import JSONDecodeError
5
- from typing import IO, Any, Callable, Dict, Optional, Union, cast, TYPE_CHECKING
5
+ from typing import IO, Any, Callable, Dict, cast, TYPE_CHECKING
6
6
 
7
7
  from ...code_samples import CodeSampleStyle
8
8
  from ...generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod, DataGenerationMethodInput
@@ -22,14 +22,14 @@ if TYPE_CHECKING:
22
22
  from .schemas import GraphQLSchema
23
23
 
24
24
 
25
- @lru_cache()
25
+ @lru_cache
26
26
  def get_introspection_query() -> str:
27
27
  import graphql
28
28
 
29
29
  return graphql.get_introspection_query()
30
30
 
31
31
 
32
- @lru_cache()
32
+ @lru_cache
33
33
  def get_introspection_query_ast() -> DocumentNode:
34
34
  import graphql
35
35
 
@@ -41,10 +41,10 @@ def from_path(
41
41
  path: PathLike,
42
42
  *,
43
43
  app: Any = None,
44
- base_url: Optional[str] = None,
44
+ base_url: str | None = None,
45
45
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
46
46
  code_sample_style: str = CodeSampleStyle.default().name,
47
- rate_limit: Optional[str] = None,
47
+ rate_limit: str | None = None,
48
48
  encoding: str = "utf8",
49
49
  sanitize_output: bool = True,
50
50
  ) -> GraphQLSchema:
@@ -66,7 +66,7 @@ def from_path(
66
66
  )
67
67
 
68
68
 
69
- def extract_schema_from_response(response: GenericResponse) -> Dict[str, Any]:
69
+ def extract_schema_from_response(response: GenericResponse) -> dict[str, Any]:
70
70
  from requests import Response
71
71
 
72
72
  try:
@@ -86,12 +86,12 @@ def from_url(
86
86
  url: str,
87
87
  *,
88
88
  app: Any = None,
89
- base_url: Optional[str] = None,
90
- port: Optional[int] = None,
89
+ base_url: str | None = None,
90
+ port: int | None = None,
91
91
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
92
92
  code_sample_style: str = CodeSampleStyle.default().name,
93
- wait_for_schema: Optional[float] = None,
94
- rate_limit: Optional[str] = None,
93
+ wait_for_schema: float | None = None,
94
+ rate_limit: str | None = None,
95
95
  sanitize_output: bool = True,
96
96
  **kwargs: Any,
97
97
  ) -> GraphQLSchema:
@@ -145,14 +145,14 @@ def from_url(
145
145
 
146
146
 
147
147
  def from_file(
148
- file: Union[IO[str], str],
148
+ file: IO[str] | str,
149
149
  *,
150
150
  app: Any = None,
151
- base_url: Optional[str] = None,
151
+ base_url: str | None = None,
152
152
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
153
153
  code_sample_style: str = CodeSampleStyle.default().name,
154
- location: Optional[str] = None,
155
- rate_limit: Optional[str] = None,
154
+ location: str | None = None,
155
+ rate_limit: str | None = None,
156
156
  sanitize_output: bool = True,
157
157
  ) -> GraphQLSchema:
158
158
  """Load GraphQL schema from a file descriptor or a string.
@@ -165,7 +165,14 @@ def from_file(
165
165
  data = file
166
166
  else:
167
167
  data = file.read()
168
- document = graphql.build_schema(data)
168
+ try:
169
+ document = graphql.build_schema(data)
170
+ except Exception as exc:
171
+ raise SchemaError(
172
+ SchemaErrorType.GRAPHQL_INVALID_SCHEMA,
173
+ "The provided API schema does not appear to be a valid GraphQL schema",
174
+ extras=[entry for entry in str(exc).splitlines() if entry],
175
+ ) from exc
169
176
  result = graphql.execute(document, get_introspection_query_ast())
170
177
  # TYPES: We don't pass `is_awaitable` above, therefore `result` is of the `ExecutionResult` type
171
178
  result = cast(graphql.ExecutionResult, result)
@@ -187,14 +194,14 @@ def from_file(
187
194
 
188
195
 
189
196
  def from_dict(
190
- raw_schema: Dict[str, Any],
197
+ raw_schema: dict[str, Any],
191
198
  *,
192
199
  app: Any = None,
193
- base_url: Optional[str] = None,
194
- location: Optional[str] = None,
200
+ base_url: str | None = None,
201
+ location: str | None = None,
195
202
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
196
203
  code_sample_style: str = CodeSampleStyle.default().name,
197
- rate_limit: Optional[str] = None,
204
+ rate_limit: str | None = None,
198
205
  sanitize_output: bool = True,
199
206
  ) -> GraphQLSchema:
200
207
  """Load GraphQL schema from a Python dictionary.
@@ -212,7 +219,7 @@ def from_dict(
212
219
  if "data" in raw_schema:
213
220
  raw_schema = raw_schema["data"]
214
221
  dispatch("before_load_schema", hook_context, raw_schema)
215
- rate_limiter: Optional[Limiter] = None
222
+ rate_limiter: Limiter | None = None
216
223
  if rate_limit is not None:
217
224
  rate_limiter = build_limiter(rate_limit)
218
225
  instance = GraphQLSchema(
@@ -233,10 +240,10 @@ def from_wsgi(
233
240
  schema_path: str,
234
241
  app: Any,
235
242
  *,
236
- base_url: Optional[str] = None,
243
+ base_url: str | None = None,
237
244
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
238
245
  code_sample_style: str = CodeSampleStyle.default().name,
239
- rate_limit: Optional[str] = None,
246
+ rate_limit: str | None = None,
240
247
  sanitize_output: bool = True,
241
248
  **kwargs: Any,
242
249
  ) -> GraphQLSchema:
@@ -272,10 +279,10 @@ def from_asgi(
272
279
  schema_path: str,
273
280
  app: Any,
274
281
  *,
275
- base_url: Optional[str] = None,
282
+ base_url: str | None = None,
276
283
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
277
284
  code_sample_style: str = CodeSampleStyle.default().name,
278
- rate_limit: Optional[str] = None,
285
+ rate_limit: str | None = None,
279
286
  sanitize_output: bool = True,
280
287
  **kwargs: Any,
281
288
  ) -> GraphQLSchema:
@@ -1,4 +1,5 @@
1
- from typing import TYPE_CHECKING, Optional
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
2
3
 
3
4
  if TYPE_CHECKING:
4
5
  from graphql import ValueNode
@@ -17,7 +18,7 @@ __all__ = [ # noqa: F822
17
18
  ]
18
19
 
19
20
 
20
- def __getattr__(name: str) -> Optional["ValueNode"]:
21
+ def __getattr__(name: str) -> ValueNode | None:
21
22
  if name in __all__:
22
23
  import hypothesis_graphql.nodes
23
24