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,5 +1,5 @@
1
+ from __future__ import annotations
1
2
  import asyncio
2
- from typing import Optional
3
3
 
4
4
  from aiohttp import web
5
5
 
@@ -22,6 +22,6 @@ def _run_server(app: web.Application, port: int) -> None:
22
22
  loop.run_forever()
23
23
 
24
24
 
25
- def run_server(app: web.Application, port: Optional[int] = None, timeout: float = 0.05) -> int:
25
+ def run_server(app: web.Application, port: int | None = None, timeout: float = 0.05) -> int:
26
26
  """Start a thread with the given aiohttp application."""
27
27
  return _server.run(_run_server, app=app, port=port, timeout=timeout)
@@ -1,10 +1,10 @@
1
- from typing import Optional
1
+ from __future__ import annotations
2
2
 
3
3
  from flask import Flask
4
4
 
5
5
  from . import _server
6
6
 
7
7
 
8
- def run_server(app: Flask, port: Optional[int] = None, timeout: float = 0.05) -> int:
8
+ def run_server(app: Flask, port: int | None = None, timeout: float = 0.05) -> int:
9
9
  """Start a thread with the given aiohttp application."""
10
10
  return _server.run(app.run, port=port, timeout=timeout)
@@ -1,11 +1,12 @@
1
+ from __future__ import annotations
1
2
  import threading
2
3
  from time import sleep
3
- from typing import Any, Callable, Optional
4
+ from typing import Any, Callable
4
5
 
5
6
  from aiohttp.test_utils import unused_port
6
7
 
7
8
 
8
- def run(target: Callable, port: Optional[int] = None, timeout: float = 0.05, **kwargs: Any) -> int:
9
+ def run(target: Callable, port: int | None = None, timeout: float = 0.05, **kwargs: Any) -> int:
9
10
  """Start a thread with the given aiohttp application."""
10
11
  if port is None:
11
12
  port = unused_port()
@@ -1,6 +1,7 @@
1
+ from __future__ import annotations
1
2
  from contextlib import contextmanager
2
3
  from functools import partial
3
- from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type, TypeVar, cast
4
+ from typing import Any, Callable, Generator, Type, TypeVar, cast
4
5
 
5
6
  import pytest
6
7
  from _pytest import fixtures, nodes
@@ -32,7 +33,7 @@ from ..utils import (
32
33
  T = TypeVar("T", bound=Node)
33
34
 
34
35
 
35
- def create(cls: Type[T], *args: Any, **kwargs: Any) -> T:
36
+ def create(cls: type[T], *args: Any, **kwargs: Any) -> T:
36
37
  if IS_PYTEST_ABOVE_54:
37
38
  return cls.from_parent(*args, **kwargs) # type: ignore
38
39
  return cls(*args, **kwargs)
@@ -43,7 +44,7 @@ class SchemathesisFunction(Function):
43
44
  self,
44
45
  *args: Any,
45
46
  test_func: Callable,
46
- test_name: Optional[str] = None,
47
+ test_name: str | None = None,
47
48
  **kwargs: Any,
48
49
  ) -> None:
49
50
  super().__init__(*args, **kwargs)
@@ -62,11 +63,11 @@ class SchemathesisFunction(Function):
62
63
 
63
64
  class SchemathesisCase(PyCollector):
64
65
  def __init__(self, test_function: Callable, *args: Any, **kwargs: Any) -> None:
65
- self.given_kwargs: Optional[Dict[str, Any]]
66
+ self.given_kwargs: dict[str, Any] | None
66
67
  given_args = get_given_args(test_function)
67
68
  given_kwargs = get_given_kwargs(test_function)
68
69
 
69
- def _init_with_valid_test(_test_function: Callable, _args: Tuple, _kwargs: Dict[str, Any]) -> None:
70
+ def _init_with_valid_test(_test_function: Callable, _args: tuple, _kwargs: dict[str, Any]) -> None:
70
71
  self.test_function = _test_function
71
72
  self.is_invalid_test = False
72
73
  self.given_kwargs = merge_given_args(test_function, _args, _kwargs)
@@ -108,6 +109,7 @@ class SchemathesisCase(PyCollector):
108
109
  test=self.test_function,
109
110
  _given_kwargs=self.given_kwargs,
110
111
  data_generation_methods=self.schemathesis_case.data_generation_methods,
112
+ generation_config=self.schemathesis_case.generation_config,
111
113
  )
112
114
  name = self._get_test_name(operation)
113
115
  else:
@@ -158,13 +160,11 @@ class SchemathesisCase(PyCollector):
158
160
  test_func=self.test_function,
159
161
  )
160
162
 
161
- def _get_class_parent(self) -> Optional[Type]:
163
+ def _get_class_parent(self) -> type | None:
162
164
  clscol = self.getparent(Class)
163
165
  return clscol.obj if clscol else None
164
166
 
165
- def _parametrize(
166
- self, cls: Optional[Type], definition: FunctionDefinition, fixtureinfo: FuncFixtureInfo
167
- ) -> Metafunc:
167
+ def _parametrize(self, cls: type | None, definition: FunctionDefinition, fixtureinfo: FuncFixtureInfo) -> Metafunc:
168
168
  parent = self.getparent(Module)
169
169
  module = parent.obj if parent is not None else parent
170
170
  kwargs = {"cls": cls, "module": module}
@@ -181,7 +181,7 @@ class SchemathesisCase(PyCollector):
181
181
  self.ihook.pytest_generate_tests.call_extra(methods, {"metafunc": metafunc})
182
182
  return metafunc
183
183
 
184
- def collect(self) -> List[Function]: # type: ignore
184
+ def collect(self) -> list[Function]: # type: ignore
185
185
  """Generate different test items for all API operations available in the given schema."""
186
186
  try:
187
187
  items = [
schemathesis/failures.py CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
  import textwrap
3
3
  from dataclasses import dataclass
4
4
  from json import JSONDecodeError
5
- from typing import Any, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
5
+ from typing import Any, TYPE_CHECKING
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from jsonschema import ValidationError
@@ -17,7 +17,7 @@ class FailureContext:
17
17
  message: str
18
18
  type: str
19
19
 
20
- def unique_by_key(self, check_message: Optional[str]) -> Tuple[str, ...]:
20
+ def unique_by_key(self, check_message: str | None) -> tuple[str, ...]:
21
21
  """A key to distinguish different failure contexts."""
22
22
  return (check_message or self.message,)
23
23
 
@@ -27,20 +27,20 @@ class ValidationErrorContext(FailureContext):
27
27
  """Additional information about JSON Schema validation errors."""
28
28
 
29
29
  validation_message: str
30
- schema_path: List[Union[str, int]]
31
- schema: Union[Dict[str, Any], bool]
32
- instance_path: List[Union[str, int]]
33
- instance: Union[None, bool, float, str, list, Dict[str, Any]]
30
+ schema_path: list[str | int]
31
+ schema: dict[str, Any] | bool
32
+ instance_path: list[str | int]
33
+ instance: None | bool | float | str | list | dict[str, Any]
34
34
  message: str
35
35
  title: str = "Response violates schema"
36
36
  type: str = "json_schema"
37
37
 
38
- def unique_by_key(self, check_message: Optional[str]) -> Tuple[str, ...]:
38
+ def unique_by_key(self, check_message: str | None) -> tuple[str, ...]:
39
39
  # Deduplicate by JSON Schema path. All errors that happened on this sub-schema will be deduplicated
40
40
  return ("/".join(map(str, self.schema_path)),)
41
41
 
42
42
  @classmethod
43
- def from_exception(cls, exc: "ValidationError") -> "ValidationErrorContext":
43
+ def from_exception(cls, exc: ValidationError) -> ValidationErrorContext:
44
44
  from .exceptions import truncated_json
45
45
 
46
46
  schema = textwrap.indent(truncated_json(exc.schema, max_lines=20), prefix=" ")
@@ -69,14 +69,14 @@ class JSONDecodeErrorContext(FailureContext):
69
69
  title: str = "JSON deserialization error"
70
70
  type: str = "json_decode"
71
71
 
72
- def unique_by_key(self, check_message: Optional[str]) -> Tuple[str, ...]:
72
+ def unique_by_key(self, check_message: str | None) -> tuple[str, ...]:
73
73
  # Treat different JSON decoding failures as the same issue
74
74
  # Payloads often contain dynamic data and distinguishing it by the error location still would not be sufficient
75
75
  # as it may be different on different dynamic payloads
76
76
  return (self.title,)
77
77
 
78
78
  @classmethod
79
- def from_exception(cls, exc: JSONDecodeError) -> "JSONDecodeErrorContext":
79
+ def from_exception(cls, exc: JSONDecodeError) -> JSONDecodeErrorContext:
80
80
  return cls(
81
81
  message=str(exc),
82
82
  validation_message=exc.msg,
@@ -99,7 +99,7 @@ class ServerError(FailureContext):
99
99
  class MissingContentType(FailureContext):
100
100
  """Content type header is missing."""
101
101
 
102
- media_types: List[str]
102
+ media_types: list[str]
103
103
  message: str
104
104
  title: str = "Missing Content-Type header"
105
105
  type: str = "missing_content_type"
@@ -110,7 +110,7 @@ class UndefinedContentType(FailureContext):
110
110
  """Response has Content-Type that is not documented in the schema."""
111
111
 
112
112
  content_type: str
113
- defined_content_types: List[str]
113
+ defined_content_types: list[str]
114
114
  message: str
115
115
  title: str = "Undocumented Content-Type"
116
116
  type: str = "undefined_content_type"
@@ -123,9 +123,9 @@ class UndefinedStatusCode(FailureContext):
123
123
  # Response's status code
124
124
  status_code: int
125
125
  # Status codes as defined in schema
126
- defined_status_codes: List[str]
126
+ defined_status_codes: list[str]
127
127
  # Defined status code with expanded wildcards
128
- allowed_status_codes: List[int]
128
+ allowed_status_codes: list[int]
129
129
  message: str
130
130
  title: str = "Undocumented HTTP status code"
131
131
  type: str = "undefined_status_code"
@@ -135,7 +135,7 @@ class UndefinedStatusCode(FailureContext):
135
135
  class MissingHeaders(FailureContext):
136
136
  """Some required headers are missing."""
137
137
 
138
- missing_headers: List[str]
138
+ missing_headers: list[str]
139
139
  message: str
140
140
  title: str = "Missing required headers"
141
141
  type: str = "missing_headers"
@@ -165,7 +165,7 @@ class ResponseTimeExceeded(FailureContext):
165
165
  title: str = "Response time limit exceeded"
166
166
  type: str = "response_time_exceeded"
167
167
 
168
- def unique_by_key(self, check_message: Optional[str]) -> Tuple[str, ...]:
168
+ def unique_by_key(self, check_message: str | None) -> tuple[str, ...]:
169
169
  return (self.title,)
170
170
 
171
171
 
schemathesis/filters.py CHANGED
@@ -1,11 +1,10 @@
1
1
  """Filtering system that allows users to filter API operations based on certain criteria."""
2
+ from __future__ import annotations
2
3
  import re
3
4
  from dataclasses import dataclass, field
4
5
  from functools import partial
5
6
  from types import SimpleNamespace
6
- from typing import TYPE_CHECKING, Callable, List, Optional, Set, Tuple, Union
7
-
8
- from typing_extensions import Protocol
7
+ from typing import TYPE_CHECKING, Callable, List, Union, Protocol
9
8
 
10
9
  from .exceptions import UsageError
11
10
 
@@ -14,7 +13,7 @@ if TYPE_CHECKING:
14
13
 
15
14
 
16
15
  class HasAPIOperation(Protocol):
17
- operation: "APIOperation"
16
+ operation: APIOperation
18
17
 
19
18
 
20
19
  MatcherFunc = Callable[[HasAPIOperation], bool]
@@ -39,12 +38,12 @@ class Matcher:
39
38
  return f"<{self.__class__.__name__}: {self.label}>"
40
39
 
41
40
  @classmethod
42
- def for_function(cls, func: MatcherFunc) -> "Matcher":
41
+ def for_function(cls, func: MatcherFunc) -> Matcher:
43
42
  """Matcher that uses the given function for matching operations."""
44
43
  return cls(func, label=func.__name__, _hash=hash(func))
45
44
 
46
45
  @classmethod
47
- def for_value(cls, attribute: str, expected: FilterValue) -> "Matcher":
46
+ def for_value(cls, attribute: str, expected: FilterValue) -> Matcher:
48
47
  """Matcher that checks whether the specified attribute has the expected value."""
49
48
  if isinstance(expected, list):
50
49
  func = partial(by_value_list, attribute=attribute, expected=expected)
@@ -54,7 +53,7 @@ class Matcher:
54
53
  return cls(func, label=label, _hash=hash(label))
55
54
 
56
55
  @classmethod
57
- def for_regex(cls, attribute: str, regex: RegexValue) -> "Matcher":
56
+ def for_regex(cls, attribute: str, regex: RegexValue) -> Matcher:
58
57
  """Matcher that checks whether the specified attribute has the provided regex."""
59
58
  if isinstance(regex, str):
60
59
  regex = re.compile(regex)
@@ -67,7 +66,7 @@ class Matcher:
67
66
  return self.func(ctx)
68
67
 
69
68
 
70
- def get_operation_attribute(operation: "APIOperation", attribute: str) -> str:
69
+ def get_operation_attribute(operation: APIOperation, attribute: str) -> str:
71
70
  # Just uppercase `method`
72
71
  value = getattr(operation, attribute)
73
72
  if attribute == "method":
@@ -79,7 +78,7 @@ def by_value(ctx: HasAPIOperation, attribute: str, expected: str) -> bool:
79
78
  return get_operation_attribute(ctx.operation, attribute) == expected
80
79
 
81
80
 
82
- def by_value_list(ctx: HasAPIOperation, attribute: str, expected: List[str]) -> bool:
81
+ def by_value_list(ctx: HasAPIOperation, attribute: str, expected: list[str]) -> bool:
83
82
  return get_operation_attribute(ctx.operation, attribute) in expected
84
83
 
85
84
 
@@ -92,7 +91,7 @@ def by_regex(ctx: HasAPIOperation, attribute: str, regex: re.Pattern) -> bool:
92
91
  class Filter:
93
92
  """Match API operations against a list of matchers."""
94
93
 
95
- matchers: Tuple[Matcher, ...]
94
+ matchers: tuple[Matcher, ...]
96
95
 
97
96
  def __repr__(self) -> str:
98
97
  inner = " && ".join(matcher.label for matcher in self.matchers)
@@ -110,10 +109,10 @@ class Filter:
110
109
  class FilterSet:
111
110
  """Combines multiple filters to apply inclusion and exclusion rules on API operations."""
112
111
 
113
- _includes: Set[Filter] = field(default_factory=set)
114
- _excludes: Set[Filter] = field(default_factory=set)
112
+ _includes: set[Filter] = field(default_factory=set)
113
+ _excludes: set[Filter] = field(default_factory=set)
115
114
 
116
- def apply_to(self, operations: List["APIOperation"]) -> List["APIOperation"]:
115
+ def apply_to(self, operations: list[APIOperation]) -> list[APIOperation]:
117
116
  """Get a filtered list of the given operations that match the filters."""
118
117
  return [operation for operation in operations if self.match(SimpleNamespace(operation=operation))]
119
118
 
@@ -141,14 +140,14 @@ class FilterSet:
141
140
 
142
141
  def include(
143
142
  self,
144
- func: Optional[MatcherFunc] = None,
143
+ func: MatcherFunc | None = None,
145
144
  *,
146
- name: Optional[FilterValue] = None,
147
- name_regex: Optional[RegexValue] = None,
148
- method: Optional[FilterValue] = None,
149
- method_regex: Optional[RegexValue] = None,
150
- path: Optional[FilterValue] = None,
151
- path_regex: Optional[RegexValue] = None,
145
+ name: FilterValue | None = None,
146
+ name_regex: RegexValue | None = None,
147
+ method: FilterValue | None = None,
148
+ method_regex: RegexValue | None = None,
149
+ path: FilterValue | None = None,
150
+ path_regex: RegexValue | None = None,
152
151
  ) -> None:
153
152
  """Add a new INCLUDE filter."""
154
153
  self._add_filter(
@@ -164,14 +163,14 @@ class FilterSet:
164
163
 
165
164
  def exclude(
166
165
  self,
167
- func: Optional[MatcherFunc] = None,
166
+ func: MatcherFunc | None = None,
168
167
  *,
169
- name: Optional[FilterValue] = None,
170
- name_regex: Optional[RegexValue] = None,
171
- method: Optional[FilterValue] = None,
172
- method_regex: Optional[RegexValue] = None,
173
- path: Optional[FilterValue] = None,
174
- path_regex: Optional[RegexValue] = None,
168
+ name: FilterValue | None = None,
169
+ name_regex: RegexValue | None = None,
170
+ method: FilterValue | None = None,
171
+ method_regex: RegexValue | None = None,
172
+ path: FilterValue | None = None,
173
+ path_regex: RegexValue | None = None,
175
174
  ) -> None:
176
175
  """Add a new EXCLUDE filter."""
177
176
  self._add_filter(
@@ -189,13 +188,13 @@ class FilterSet:
189
188
  self,
190
189
  include: bool,
191
190
  *,
192
- func: Optional[MatcherFunc] = None,
193
- name: Optional[FilterValue] = None,
194
- name_regex: Optional[RegexValue] = None,
195
- method: Optional[FilterValue] = None,
196
- method_regex: Optional[RegexValue] = None,
197
- path: Optional[FilterValue] = None,
198
- path_regex: Optional[RegexValue] = None,
191
+ func: MatcherFunc | None = None,
192
+ name: FilterValue | None = None,
193
+ name_regex: RegexValue | None = None,
194
+ method: FilterValue | None = None,
195
+ method_regex: RegexValue | None = None,
196
+ path: FilterValue | None = None,
197
+ path_regex: RegexValue | None = None,
199
198
  ) -> None:
200
199
  matchers = []
201
200
  if func is not None:
@@ -242,14 +241,14 @@ def attach_filter_chain(
242
241
  """
243
242
 
244
243
  def proxy(
245
- func: Optional[MatcherFunc] = None,
244
+ func: MatcherFunc | None = None,
246
245
  *,
247
- name: Optional[FilterValue] = None,
248
- name_regex: Optional[str] = None,
249
- method: Optional[FilterValue] = None,
250
- method_regex: Optional[str] = None,
251
- path: Optional[FilterValue] = None,
252
- path_regex: Optional[str] = None,
246
+ name: FilterValue | None = None,
247
+ name_regex: str | None = None,
248
+ method: FilterValue | None = None,
249
+ method_regex: str | None = None,
250
+ path: FilterValue | None = None,
251
+ path_regex: str | None = None,
253
252
  ) -> Callable:
254
253
  __tracebackhide__ = True
255
254
  filter_func(
@@ -1,4 +1,5 @@
1
- from typing import Iterable, Optional
1
+ from __future__ import annotations
2
+ from typing import Iterable
2
3
 
3
4
  from . import fast_api, utf8_bom
4
5
 
@@ -6,7 +7,7 @@ ALL_FIXUPS = {"fast_api": fast_api, "utf8_bom": utf8_bom}
6
7
  ALL_FIXUP_NAMES = list(ALL_FIXUPS.keys())
7
8
 
8
9
 
9
- def install(fixups: Optional[Iterable[str]] = None) -> None:
10
+ def install(fixups: Iterable[str] | None = None) -> None:
10
11
  """Install fixups.
11
12
 
12
13
  Without the first argument installs all available fixups.
@@ -18,7 +19,7 @@ def install(fixups: Optional[Iterable[str]] = None) -> None:
18
19
  ALL_FIXUPS[name].install() # type: ignore
19
20
 
20
21
 
21
- def uninstall(fixups: Optional[Iterable[str]] = None) -> None:
22
+ def uninstall(fixups: Iterable[str] | None = None) -> None:
22
23
  """Uninstall fixups.
23
24
 
24
25
  Without the first argument uninstalls all available fixups.
@@ -1,4 +1,5 @@
1
- from typing import Any, Dict
1
+ from __future__ import annotations
2
+ from typing import Any
2
3
 
3
4
  from ..hooks import HookContext
4
5
  from ..hooks import is_installed as global_is_installed
@@ -18,15 +19,15 @@ def is_installed() -> bool:
18
19
  return global_is_installed("before_load_schema", before_load_schema)
19
20
 
20
21
 
21
- def before_load_schema(context: HookContext, schema: Dict[str, Any]) -> None:
22
+ def before_load_schema(context: HookContext, schema: dict[str, Any]) -> None:
22
23
  adjust_schema(schema)
23
24
 
24
25
 
25
- def adjust_schema(schema: Dict[str, Any]) -> None:
26
+ def adjust_schema(schema: dict[str, Any]) -> None:
26
27
  traverse_schema(schema, _handle_boundaries)
27
28
 
28
29
 
29
- def _handle_boundaries(schema: Dict[str, Any]) -> Dict[str, Any]:
30
+ def _handle_boundaries(schema: dict[str, Any]) -> dict[str, Any]:
30
31
  """Convert Draft 7 keywords to Draft 4 compatible versions.
31
32
 
32
33
  FastAPI uses ``pydantic``, which generates Draft 7 compatible schemas.
@@ -1,6 +1,8 @@
1
+ from __future__ import annotations
1
2
  import random
3
+ from dataclasses import dataclass
2
4
  from enum import Enum
3
- from typing import List, Union, Iterable
5
+ from typing import Union, Iterable
4
6
 
5
7
 
6
8
  class DataGenerationMethod(str, Enum):
@@ -12,11 +14,11 @@ class DataGenerationMethod(str, Enum):
12
14
  negative = "negative"
13
15
 
14
16
  @classmethod
15
- def default(cls) -> "DataGenerationMethod":
17
+ def default(cls) -> DataGenerationMethod:
16
18
  return cls.positive
17
19
 
18
20
  @classmethod
19
- def all(cls) -> List["DataGenerationMethod"]:
21
+ def all(cls) -> list[DataGenerationMethod]:
20
22
  return list(DataGenerationMethod)
21
23
 
22
24
  def as_short_name(self) -> str:
@@ -30,7 +32,7 @@ class DataGenerationMethod(str, Enum):
30
32
  return self == DataGenerationMethod.negative
31
33
 
32
34
  @classmethod
33
- def ensure_list(cls, value: "DataGenerationMethodInput") -> List["DataGenerationMethod"]:
35
+ def ensure_list(cls, value: DataGenerationMethodInput) -> list[DataGenerationMethod]:
34
36
  if isinstance(value, DataGenerationMethod):
35
37
  return [value]
36
38
  return list(value)
@@ -54,3 +56,13 @@ def generate_random_case_id(length: int = 6) -> str:
54
56
  number, rem = divmod(number, BASE)
55
57
  output += CASE_ID_ALPHABET[rem]
56
58
  return output
59
+
60
+
61
+ @dataclass
62
+ class GenerationConfig:
63
+ """Holds various configuration options relevant for data generation."""
64
+
65
+ # Allow generating `\x00` bytes in strings
66
+ allow_x00: bool = True
67
+ # Generate strings using the given codec
68
+ codec: str | None = "utf-8"
schemathesis/hooks.py CHANGED
@@ -5,7 +5,7 @@ from copy import deepcopy
5
5
  from dataclasses import dataclass, field
6
6
  from enum import Enum, unique
7
7
  from functools import partial
8
- from typing import TYPE_CHECKING, Any, Callable, ClassVar, DefaultDict, Dict, List, Optional, Union, cast
8
+ from typing import TYPE_CHECKING, Any, Callable, ClassVar, DefaultDict, cast
9
9
 
10
10
  from .types import GenericTest
11
11
  from .internal.deprecation import deprecated_property
@@ -27,7 +27,7 @@ class HookScope(Enum):
27
27
  @dataclass
28
28
  class RegisteredHook:
29
29
  signature: inspect.Signature
30
- scopes: List[HookScope]
30
+ scopes: list[HookScope]
31
31
 
32
32
 
33
33
  @dataclass
@@ -38,10 +38,10 @@ class HookContext:
38
38
  Might be absent in some cases.
39
39
  """
40
40
 
41
- operation: Optional["APIOperation"] = None
41
+ operation: APIOperation | None = None
42
42
 
43
43
  @deprecated_property(removed_in="4.0", replacement="operation")
44
- def endpoint(self) -> Optional["APIOperation"]:
44
+ def endpoint(self) -> APIOperation | None:
45
45
  return self.operation
46
46
 
47
47
 
@@ -53,10 +53,10 @@ class HookDispatcher:
53
53
  """
54
54
 
55
55
  scope: HookScope
56
- _hooks: DefaultDict[str, List[Callable]] = field(default_factory=lambda: defaultdict(list))
57
- _specs: ClassVar[Dict[str, RegisteredHook]] = {}
56
+ _hooks: DefaultDict[str, list[Callable]] = field(default_factory=lambda: defaultdict(list))
57
+ _specs: ClassVar[dict[str, RegisteredHook]] = {}
58
58
 
59
- def register(self, hook: Union[str, Callable]) -> Callable:
59
+ def register(self, hook: str | Callable) -> Callable:
60
60
  """Register a new hook.
61
61
 
62
62
  :param hook: Either a hook function or a string.
@@ -87,7 +87,7 @@ class HookDispatcher:
87
87
  return decorator
88
88
  return self.register_hook_with_name(hook, hook.__name__)
89
89
 
90
- def merge(self, other: "HookDispatcher") -> "HookDispatcher":
90
+ def merge(self, other: HookDispatcher) -> HookDispatcher:
91
91
  """Merge two dispatches together.
92
92
 
93
93
  The resulting dispatcher will call the `self` hooks first.
@@ -99,7 +99,7 @@ class HookDispatcher:
99
99
  instance._hooks = all_hooks
100
100
  return instance
101
101
 
102
- def apply(self, hook: Callable, *, name: Optional[str] = None) -> Callable[[Callable], Callable]:
102
+ def apply(self, hook: Callable, *, name: str | None = None) -> Callable[[Callable], Callable]:
103
103
  """Register hook to run only on one test function.
104
104
 
105
105
  :param hook: A hook function.
@@ -130,7 +130,7 @@ class HookDispatcher:
130
130
  return decorator
131
131
 
132
132
  @classmethod
133
- def add_dispatcher(cls, func: GenericTest) -> "HookDispatcher":
133
+ def add_dispatcher(cls, func: GenericTest) -> HookDispatcher:
134
134
  """Attach a new dispatcher instance to the test if it is not already present."""
135
135
  if not hasattr(func, "_schemathesis_hooks"):
136
136
  func._schemathesis_hooks = cls(scope=HookScope.TEST) # type: ignore
@@ -143,7 +143,7 @@ class HookDispatcher:
143
143
  return hook
144
144
 
145
145
  @classmethod
146
- def register_spec(cls, scopes: List[HookScope]) -> Callable:
146
+ def register_spec(cls, scopes: list[HookScope]) -> Callable:
147
147
  """Register hook specification.
148
148
 
149
149
  All hooks, registered with `register` should comply with corresponding registered specs.
@@ -173,10 +173,10 @@ class HookDispatcher:
173
173
  f"Hook '{name}' takes {len(spec.signature.parameters)} arguments but {len(signature.parameters)} is defined"
174
174
  )
175
175
 
176
- def collect_statistic(self) -> Dict[str, int]:
176
+ def collect_statistic(self) -> dict[str, int]:
177
177
  return {name: len(hooks) for name, hooks in self._hooks.items()}
178
178
 
179
- def get_all_by_name(self, name: str) -> List[Callable]:
179
+ def get_all_by_name(self, name: str) -> list[Callable]:
180
180
  """Get a list of hooks registered for a name."""
181
181
  return self._hooks.get(name, [])
182
182
 
@@ -225,9 +225,9 @@ class HookDispatcher:
225
225
 
226
226
 
227
227
  def apply_to_all_dispatchers(
228
- operation: "APIOperation",
228
+ operation: APIOperation,
229
229
  context: HookContext,
230
- hooks: Optional[HookDispatcher],
230
+ hooks: HookDispatcher | None,
231
231
  strategy: st.SearchStrategy,
232
232
  container: str,
233
233
  ) -> st.SearchStrategy:
@@ -287,32 +287,32 @@ def before_generate_body(context: HookContext, strategy: st.SearchStrategy) -> s
287
287
 
288
288
 
289
289
  @all_scopes
290
- def before_generate_case(context: HookContext, strategy: st.SearchStrategy["Case"]) -> st.SearchStrategy["Case"]:
290
+ def before_generate_case(context: HookContext, strategy: st.SearchStrategy[Case]) -> st.SearchStrategy[Case]:
291
291
  """Called on a strategy that generates ``Case`` instances."""
292
292
 
293
293
 
294
294
  @all_scopes
295
- def before_process_path(context: HookContext, path: str, methods: Dict[str, Any]) -> None:
295
+ def before_process_path(context: HookContext, path: str, methods: dict[str, Any]) -> None:
296
296
  """Called before API path is processed."""
297
297
 
298
298
 
299
299
  @all_scopes
300
- def filter_operations(context: HookContext) -> Optional[bool]:
300
+ def filter_operations(context: HookContext) -> bool | None:
301
301
  """Decide whether testing of this particular API operation should be skipped or not."""
302
302
 
303
303
 
304
304
  @HookDispatcher.register_spec([HookScope.GLOBAL])
305
- def before_load_schema(context: HookContext, raw_schema: Dict[str, Any]) -> None:
305
+ def before_load_schema(context: HookContext, raw_schema: dict[str, Any]) -> None:
306
306
  """Called before schema instance is created."""
307
307
 
308
308
 
309
309
  @HookDispatcher.register_spec([HookScope.GLOBAL])
310
- def after_load_schema(context: HookContext, schema: "BaseSchema") -> None:
310
+ def after_load_schema(context: HookContext, schema: BaseSchema) -> None:
311
311
  """Called after schema instance is created."""
312
312
 
313
313
 
314
314
  @all_scopes
315
- def before_add_examples(context: HookContext, examples: List["Case"]) -> None:
315
+ def before_add_examples(context: HookContext, examples: list[Case]) -> None:
316
316
  """Called before explicit examples are added to a test via `@example` decorator.
317
317
 
318
318
  `examples` is a list that could be extended with examples provided by the user.
@@ -320,12 +320,12 @@ def before_add_examples(context: HookContext, examples: List["Case"]) -> None:
320
320
 
321
321
 
322
322
  @all_scopes
323
- def before_init_operation(context: HookContext, operation: "APIOperation") -> None:
323
+ def before_init_operation(context: HookContext, operation: APIOperation) -> None:
324
324
  """Allows you to customize a newly created API operation."""
325
325
 
326
326
 
327
327
  @HookDispatcher.register_spec([HookScope.GLOBAL])
328
- def add_case(context: HookContext, case: "Case", response: GenericResponse) -> Optional["Case"]:
328
+ def add_case(context: HookContext, case: Case, response: GenericResponse) -> Case | None:
329
329
  """Creates an additional test per API operation. If this hook returns None, no additional test created.
330
330
 
331
331
  Called with a copy of the original case object and the server's response to the original case.
@@ -333,7 +333,7 @@ def add_case(context: HookContext, case: "Case", response: GenericResponse) -> O
333
333
 
334
334
 
335
335
  @HookDispatcher.register_spec([HookScope.GLOBAL])
336
- def before_call(context: HookContext, case: "Case") -> None:
336
+ def before_call(context: HookContext, case: Case) -> None:
337
337
  """Called before every network call in CLI tests.
338
338
 
339
339
  Use cases:
@@ -343,7 +343,7 @@ def before_call(context: HookContext, case: "Case") -> None:
343
343
 
344
344
 
345
345
  @HookDispatcher.register_spec([HookScope.GLOBAL])
346
- def after_call(context: HookContext, case: "Case", response: GenericResponse) -> None:
346
+ def after_call(context: HookContext, case: Case, response: GenericResponse) -> None:
347
347
  """Called after every network call in CLI tests.
348
348
 
349
349
  Note that you need to modify the response in-place.