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,6 @@
1
+ from __future__ import annotations
1
2
  import json
2
- from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Union
3
+ from typing import Any, Callable, Dict, Generator, List
3
4
 
4
5
  from ...utils import compose
5
6
 
@@ -10,11 +11,11 @@ MapFunction = Callable[[Generated], Generated]
10
11
 
11
12
 
12
13
  def make_serializer(
13
- func: Callable[[DefinitionList], Generator[Optional[Callable], None, None]]
14
- ) -> Callable[[DefinitionList], Optional[Callable]]:
14
+ func: Callable[[DefinitionList], Generator[Callable | None, None, None]]
15
+ ) -> Callable[[DefinitionList], Callable | None]:
15
16
  """A maker function to avoid code duplication."""
16
17
 
17
- def _wrapper(definitions: DefinitionList) -> Optional[Callable]:
18
+ def _wrapper(definitions: DefinitionList) -> Callable | None:
18
19
  conversions = list(func(definitions))
19
20
  if conversions:
20
21
  return compose(*[conv for conv in conversions if conv is not None])
@@ -23,7 +24,7 @@ def make_serializer(
23
24
  return _wrapper
24
25
 
25
26
 
26
- def _serialize_openapi3(definitions: DefinitionList) -> Generator[Optional[Callable], None, None]:
27
+ def _serialize_openapi3(definitions: DefinitionList) -> Generator[Callable | None, None, None]:
27
28
  """Different collection styles for Open API 3.0."""
28
29
  for definition in definitions:
29
30
  name = definition["name"]
@@ -49,8 +50,8 @@ def _serialize_openapi3(definitions: DefinitionList) -> Generator[Optional[Calla
49
50
 
50
51
 
51
52
  def _serialize_path_openapi3(
52
- name: str, type_: str, style: Optional[str], explode: Optional[bool]
53
- ) -> Generator[Optional[Callable], None, None]:
53
+ name: str, type_: str, style: str | None, explode: bool | None
54
+ ) -> Generator[Callable | None, None, None]:
54
55
  if style == "simple":
55
56
  if type_ == "object":
56
57
  if explode is False:
@@ -76,8 +77,8 @@ def _serialize_path_openapi3(
76
77
 
77
78
 
78
79
  def _serialize_query_openapi3(
79
- name: str, type_: str, style: Optional[str], explode: Optional[bool]
80
- ) -> Generator[Optional[Callable], None, None]:
80
+ name: str, type_: str, style: str | None, explode: bool | None
81
+ ) -> Generator[Callable | None, None, None]:
81
82
  if type_ == "object":
82
83
  if style == "deepObject":
83
84
  yield deep_object(name)
@@ -95,9 +96,7 @@ def _serialize_query_openapi3(
95
96
  yield delimited(name, delimiter=",")
96
97
 
97
98
 
98
- def _serialize_header_openapi3(
99
- name: str, type_: str, explode: Optional[bool]
100
- ) -> Generator[Optional[Callable], None, None]:
99
+ def _serialize_header_openapi3(name: str, type_: str, explode: bool | None) -> Generator[Callable | None, None, None]:
101
100
  # Headers should be coerced to a string so we can check it for validity later
102
101
  yield to_string(name)
103
102
  # Header parameters always use the "simple" style, that is, comma-separated values
@@ -110,9 +109,7 @@ def _serialize_header_openapi3(
110
109
  yield delimited_object(name)
111
110
 
112
111
 
113
- def _serialize_cookie_openapi3(
114
- name: str, type_: str, explode: Optional[bool]
115
- ) -> Generator[Optional[Callable], None, None]:
112
+ def _serialize_cookie_openapi3(name: str, type_: str, explode: bool | None) -> Generator[Callable | None, None, None]:
116
113
  # Cookies should be coerced to a string so we can check it for validity later
117
114
  yield to_string(name)
118
115
  # Cookie parameters always use the "form" style
@@ -129,7 +126,7 @@ def _serialize_cookie_openapi3(
129
126
  yield comma_delimited_object(name)
130
127
 
131
128
 
132
- def _serialize_swagger2(definitions: DefinitionList) -> Generator[Optional[Callable], None, None]:
129
+ def _serialize_swagger2(definitions: DefinitionList) -> Generator[Callable | None, None, None]:
133
130
  """Different collection formats for Open API 2.0."""
134
131
  for definition in definitions:
135
132
  name = definition["name"]
@@ -165,11 +162,11 @@ def conversion(func: Callable[..., None]) -> Callable:
165
162
  return _wrapper
166
163
 
167
164
 
168
- def make_delimited(data: Optional[Dict[str, Any]], delimiter: str = ",") -> str:
165
+ def make_delimited(data: dict[str, Any] | None, delimiter: str = ",") -> str:
169
166
  return delimiter.join(f"{key}={value}" for key, value in force_dict(data or {}).items())
170
167
 
171
168
 
172
- def force_iterable(value: Any) -> Union[List, Tuple]:
169
+ def force_iterable(value: Any) -> list | tuple:
173
170
  """Converts the value to a list or a tuple.
174
171
 
175
172
  Only relevant for negative test scenarios where the original types might be changed.
@@ -179,7 +176,7 @@ def force_iterable(value: Any) -> Union[List, Tuple]:
179
176
  return [value]
180
177
 
181
178
 
182
- def force_dict(value: Any) -> Dict:
179
+ def force_dict(value: Any) -> dict:
183
180
  """Converts the value to a dictionary.
184
181
 
185
182
  Only relevant for negative test scenarios where the original types might be changed.
@@ -247,7 +244,7 @@ def label_primitive(item: Generated, name: str) -> None:
247
244
 
248
245
 
249
246
  @conversion
250
- def label_array(item: Generated, name: str, explode: Optional[bool]) -> None:
247
+ def label_array(item: Generated, name: str, explode: bool | None) -> None:
251
248
  """Serialize an array with the `label` style.
252
249
 
253
250
  Explode=True
@@ -270,7 +267,7 @@ def label_array(item: Generated, name: str, explode: Optional[bool]) -> None:
270
267
 
271
268
 
272
269
  @conversion
273
- def label_object(item: Generated, name: str, explode: Optional[bool]) -> None:
270
+ def label_object(item: Generated, name: str, explode: bool | None) -> None:
274
271
  """Serialize an object with the `label` style.
275
272
 
276
273
  Explode=True
@@ -306,7 +303,7 @@ def matrix_primitive(item: Generated, name: str) -> None:
306
303
 
307
304
 
308
305
  @conversion
309
- def matrix_array(item: Generated, name: str, explode: Optional[bool]) -> None:
306
+ def matrix_array(item: Generated, name: str, explode: bool | None) -> None:
310
307
  """Serialize an array with the `matrix` style.
311
308
 
312
309
  Explode=True
@@ -328,7 +325,7 @@ def matrix_array(item: Generated, name: str, explode: Optional[bool]) -> None:
328
325
 
329
326
 
330
327
  @conversion
331
- def matrix_object(item: Generated, name: str, explode: Optional[bool]) -> None:
328
+ def matrix_object(item: Generated, name: str, explode: bool | None) -> None:
332
329
  """Serialize an object with the `matrix` style.
333
330
 
334
331
  Explode=True
@@ -1,5 +1,6 @@
1
+ from __future__ import annotations
1
2
  from collections import defaultdict
2
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, cast
3
+ from typing import TYPE_CHECKING, Any, List, cast
3
4
 
4
5
  from hypothesis import strategies as st
5
6
  from hypothesis.stateful import Bundle, Rule, precondition, rule
@@ -18,13 +19,13 @@ if TYPE_CHECKING:
18
19
 
19
20
 
20
21
  class OpenAPIStateMachine(APIStateMachine):
21
- def transform(self, result: StepResult, direction: Direction, case: "Case") -> "Case":
22
+ def transform(self, result: StepResult, direction: Direction, case: Case) -> Case:
22
23
  context = expressions.ExpressionContext(case=result.case, response=result.response)
23
24
  direction.set_data(case, elapsed=result.elapsed, context=context)
24
25
  return case
25
26
 
26
27
 
27
- def create_state_machine(schema: "BaseOpenAPISchema") -> Type[APIStateMachine]:
28
+ def create_state_machine(schema: BaseOpenAPISchema) -> type[APIStateMachine]:
28
29
  """Create a state machine class.
29
30
 
30
31
  It aims to avoid making calls that are not likely to lead to a stateful call later. For example:
@@ -42,17 +43,17 @@ def create_state_machine(schema: "BaseOpenAPISchema") -> Type[APIStateMachine]:
42
43
 
43
44
  rules = make_all_rules(operations, bundles, connections)
44
45
 
45
- kwargs: Dict[str, Any] = {"bundles": bundles, "schema": schema}
46
+ kwargs: dict[str, Any] = {"bundles": bundles, "schema": schema}
46
47
  return type("APIWorkflow", (OpenAPIStateMachine,), {**kwargs, **rules})
47
48
 
48
49
 
49
- def init_bundles(schema: "BaseOpenAPISchema") -> Dict[str, CaseInsensitiveDict]:
50
+ def init_bundles(schema: BaseOpenAPISchema) -> dict[str, CaseInsensitiveDict]:
50
51
  """Create bundles for all operations in the given schema.
51
52
 
52
53
  Each API operation has a bundle that stores all responses from that operation.
53
54
  We need to create bundles first, so they can be referred when building connections between operations.
54
55
  """
55
- output: Dict[str, CaseInsensitiveDict] = {}
56
+ output: dict[str, CaseInsensitiveDict] = {}
56
57
  for result in schema.get_all_operations():
57
58
  if isinstance(result, Ok):
58
59
  operation = result.ok()
@@ -62,10 +63,10 @@ def init_bundles(schema: "BaseOpenAPISchema") -> Dict[str, CaseInsensitiveDict]:
62
63
 
63
64
 
64
65
  def make_all_rules(
65
- operations: List["APIOperation"],
66
- bundles: Dict[str, CaseInsensitiveDict],
66
+ operations: list[APIOperation],
67
+ bundles: dict[str, CaseInsensitiveDict],
67
68
  connections: APIOperationConnections,
68
- ) -> Dict[str, Rule]:
69
+ ) -> dict[str, Rule]:
69
70
  """Create rules for all API operations, based on the provided connections."""
70
71
  rules = {}
71
72
  for operation in operations:
@@ -76,10 +77,10 @@ def make_all_rules(
76
77
 
77
78
 
78
79
  def make_rule(
79
- operation: "APIOperation",
80
+ operation: APIOperation,
80
81
  bundle: Bundle,
81
82
  connections: APIOperationConnections,
82
- ) -> Optional[Rule]:
83
+ ) -> Rule | None:
83
84
  """Create a rule for an API operation."""
84
85
 
85
86
  def _make_rule(previous: st.SearchStrategy) -> Rule:
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
  from dataclasses import dataclass
3
- from typing import TYPE_CHECKING, Callable, Dict, List, Tuple
3
+ from typing import TYPE_CHECKING, Callable, Dict, List
4
4
 
5
5
  import hypothesis.strategies as st
6
6
  from requests.structures import CaseInsensitiveDict
@@ -18,15 +18,15 @@ FilterFunction = Callable[["StepResult"], bool]
18
18
  @dataclass
19
19
  class Connection:
20
20
  source: str
21
- strategy: st.SearchStrategy[Tuple[StepResult, OpenAPILink]]
21
+ strategy: st.SearchStrategy[tuple[StepResult, OpenAPILink]]
22
22
 
23
23
 
24
24
  APIOperationConnections = Dict[str, List[Connection]]
25
25
 
26
26
 
27
27
  def apply(
28
- operation: "APIOperation",
29
- bundles: Dict[str, CaseInsensitiveDict],
28
+ operation: APIOperation,
29
+ bundles: dict[str, CaseInsensitiveDict],
30
30
  connections: APIOperationConnections,
31
31
  ) -> None:
32
32
  """Gather all connections based on Open API links definitions."""
@@ -42,12 +42,12 @@ def apply(
42
42
 
43
43
  def _convert_strategy(
44
44
  strategy: st.SearchStrategy[StepResult], link: OpenAPILink
45
- ) -> st.SearchStrategy[Tuple[StepResult, OpenAPILink]]:
45
+ ) -> st.SearchStrategy[tuple[StepResult, OpenAPILink]]:
46
46
  # This function is required to capture values properly (it won't work properly when lambda is defined in a loop)
47
47
  return strategy.map(lambda out: (out, link))
48
48
 
49
49
 
50
- def make_response_filter(status_code: str, all_status_codes: List[str]) -> FilterFunction:
50
+ def make_response_filter(status_code: str, all_status_codes: list[str]) -> FilterFunction:
51
51
  """Create a filter for stored responses.
52
52
 
53
53
  This filter will decide whether some response is suitable to use as a source for requesting some API operation.
@@ -76,7 +76,7 @@ def match_status_code(status_code: str) -> FilterFunction:
76
76
  return compare
77
77
 
78
78
 
79
- def default_status_code(status_codes: List[str]) -> FilterFunction:
79
+ def default_status_code(status_codes: list[str]) -> FilterFunction:
80
80
  """Create a filter that matches all "default" responses.
81
81
 
82
82
  In Open API, the "default" response is the one that is used if no other options were matched.
@@ -1,9 +1,10 @@
1
+ from __future__ import annotations
1
2
  import string
2
3
  from itertools import product
3
- from typing import Any, Dict, Generator, List, Union
4
+ from typing import Any, Generator
4
5
 
5
6
 
6
- def expand_status_code(status_code: Union[str, int]) -> Generator[int, None, None]:
7
+ def expand_status_code(status_code: str | int) -> Generator[int, None, None]:
7
8
  chars = [list(string.digits) if digit == "X" else [digit] for digit in str(status_code).upper()]
8
9
  for expanded in product(*chars):
9
10
  yield int("".join(expanded))
@@ -14,7 +15,7 @@ def is_header_location(location: str) -> bool:
14
15
  return location in ("header", "cookie")
15
16
 
16
17
 
17
- def get_type(schema: Dict[str, Any]) -> List[str]:
18
+ def get_type(schema: dict[str, Any]) -> list[str]:
18
19
  type_ = schema.get("type", ["null", "boolean", "integer", "number", "string", "array", "object"])
19
20
  if isinstance(type_, str):
20
21
  return [type_]
@@ -1,4 +1,5 @@
1
- from typing import Any, List, Tuple, Union
1
+ from __future__ import annotations
2
+ from typing import Any
2
3
 
3
4
  from ...constants import HTTP_METHODS
4
5
 
@@ -9,7 +10,7 @@ def is_pattern_error(exception: TypeError) -> bool:
9
10
  return "expected string or bytes-like object" in str(exception)
10
11
 
11
12
 
12
- def find_numeric_http_status_codes(schema: Any) -> List[Tuple[int, List[Union[str, int]]]]:
13
+ def find_numeric_http_status_codes(schema: Any) -> list[tuple[int, list[str | int]]]:
13
14
  if not isinstance(schema, dict):
14
15
  return []
15
16
  found = []
@@ -2,9 +2,9 @@ from __future__ import annotations
2
2
  import enum
3
3
  import json
4
4
  from dataclasses import dataclass, field
5
- from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, List, Optional, Tuple, Type
5
+ from typing import TYPE_CHECKING, Any, Callable, Generator
6
6
 
7
- from .._dependency_versions import IS_HYPOTHESIS_ABOVE_6_68_1
7
+ from .. import GenerationConfig
8
8
  from ..exceptions import OperationSchemaError
9
9
  from ..models import APIOperation, Case
10
10
  from ..constants import NOT_SET
@@ -29,7 +29,7 @@ class ParsedData:
29
29
  It is used later to create a new version of an API operation that will reuse this data.
30
30
  """
31
31
 
32
- parameters: Dict[str, Any]
32
+ parameters: dict[str, Any]
33
33
  body: Any = NOT_SET
34
34
 
35
35
  def __hash__(self) -> int:
@@ -54,7 +54,7 @@ class StatefulTest:
54
54
  def parse(self, case: Case, response: GenericResponse) -> ParsedData:
55
55
  raise NotImplementedError
56
56
 
57
- def make_operation(self, collected: List[ParsedData]) -> APIOperation:
57
+ def make_operation(self, collected: list[ParsedData]) -> APIOperation:
58
58
  raise NotImplementedError
59
59
 
60
60
 
@@ -63,7 +63,7 @@ class StatefulData:
63
63
  """Storage for data that will be used in later tests."""
64
64
 
65
65
  stateful_test: StatefulTest
66
- container: List[ParsedData] = field(default_factory=list)
66
+ container: list[ParsedData] = field(default_factory=list)
67
67
 
68
68
  def make_operation(self) -> APIOperation:
69
69
  return self.stateful_test.make_operation(self.container)
@@ -81,9 +81,9 @@ class Feedback:
81
81
  Provides a way to control runner's behavior from tests.
82
82
  """
83
83
 
84
- stateful: Optional[Stateful]
84
+ stateful: Stateful | None
85
85
  operation: APIOperation = field(repr=False)
86
- stateful_tests: Dict[str, StatefulData] = field(default_factory=dict, repr=False)
86
+ stateful_tests: dict[str, StatefulData] = field(default_factory=dict, repr=False)
87
87
 
88
88
  def add_test_case(self, case: Case, response: GenericResponse) -> None:
89
89
  """Store test data to reuse it in the future additional tests."""
@@ -94,10 +94,11 @@ class Feedback:
94
94
  def get_stateful_tests(
95
95
  self,
96
96
  test: Callable,
97
- settings: Optional[hypothesis.settings],
98
- seed: Optional[int],
99
- as_strategy_kwargs: Optional[Dict[str, Any]],
100
- ) -> Generator[Result[Tuple[APIOperation, Callable], OperationSchemaError], None, None]:
97
+ settings: hypothesis.settings | None,
98
+ generation_config: GenerationConfig | None,
99
+ seed: int | None,
100
+ as_strategy_kwargs: dict[str, Any] | None,
101
+ ) -> Generator[Result[tuple[APIOperation, Callable], OperationSchemaError], None, None]:
101
102
  """Generate additional tests that use data from the previous ones."""
102
103
  from .._hypothesis import create_test
103
104
 
@@ -109,13 +110,14 @@ class Feedback:
109
110
  settings=settings,
110
111
  seed=seed,
111
112
  data_generation_methods=operation.schema.data_generation_methods,
113
+ generation_config=generation_config,
112
114
  as_strategy_kwargs=as_strategy_kwargs,
113
115
  )
114
116
  yield Ok((operation, test_function))
115
117
 
116
118
 
117
119
  def run_state_machine_as_test(
118
- state_machine_factory: Type[APIStateMachine], *, settings: Optional[hypothesis.settings] = None
120
+ state_machine_factory: type[APIStateMachine], *, settings: hypothesis.settings | None = None
119
121
  ) -> None:
120
122
  """Run a state machine as a test.
121
123
 
@@ -123,7 +125,4 @@ def run_state_machine_as_test(
123
125
  """
124
126
  from hypothesis.stateful import run_state_machine_as_test as _run_state_machine_as_test
125
127
 
126
- if IS_HYPOTHESIS_ABOVE_6_68_1:
127
- # Newer Hypothesis contains an argument to set the minimum number of steps for a state machine execution
128
- return _run_state_machine_as_test(state_machine_factory, settings=settings, _min_steps=2)
129
- return _run_state_machine_as_test(state_machine_factory, settings=settings)
128
+ return _run_state_machine_as_test(state_machine_factory, settings=settings, _min_steps=2)
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import time
4
4
  from dataclasses import dataclass
5
- from typing import TYPE_CHECKING, Any, Dict, Optional, Callable, Tuple, ClassVar
5
+ from typing import TYPE_CHECKING, Any, Callable, ClassVar
6
6
 
7
7
  from hypothesis.stateful import RuleBasedStateMachine
8
8
 
@@ -34,8 +34,8 @@ class APIStateMachine(RuleBasedStateMachine):
34
34
  # This is a convenience attribute, which happened to clash with `RuleBasedStateMachine` instance level attribute
35
35
  # They don't interfere, since it is properly overridden on the Hypothesis side, but it is likely that this
36
36
  # attribute will be renamed in the future
37
- bundles: ClassVar[Dict[str, CaseInsensitiveDict]] # type: ignore
38
- schema: "BaseSchema"
37
+ bundles: ClassVar[dict[str, CaseInsensitiveDict]] # type: ignore
38
+ schema: BaseSchema
39
39
 
40
40
  def __init__(self) -> None:
41
41
  super().__init__() # type: ignore
@@ -53,7 +53,7 @@ class APIStateMachine(RuleBasedStateMachine):
53
53
  return super()._pretty_print(value) # type: ignore
54
54
 
55
55
  @classmethod
56
- def run(cls, *, settings: Optional[hypothesis.settings] = None) -> None:
56
+ def run(cls, *, settings: hypothesis.settings | None = None) -> None:
57
57
  """Run state machine as a test."""
58
58
  from . import run_state_machine_as_test
59
59
 
@@ -74,14 +74,14 @@ class APIStateMachine(RuleBasedStateMachine):
74
74
  def transform(self, result: StepResult, direction: Direction, case: Case) -> Case:
75
75
  raise NotImplementedError
76
76
 
77
- def _step(self, case: Case, previous: Optional[Tuple[StepResult, Direction]] = None) -> StepResult:
77
+ def _step(self, case: Case, previous: tuple[StepResult, Direction] | None = None) -> StepResult:
78
78
  # This method is a proxy that is used under the hood during the state machine initialization.
79
79
  # The whole point of having it is to make it possible to override `step`; otherwise, custom "step" is ignored.
80
80
  # It happens because, at the point of initialization, the final class is not yet created.
81
81
  __tracebackhide__ = True
82
82
  return self.step(case, previous)
83
83
 
84
- def step(self, case: Case, previous: Optional[Tuple[StepResult, Direction]] = None) -> StepResult:
84
+ def step(self, case: Case, previous: tuple[StepResult, Direction] | None = None) -> StepResult:
85
85
  """A single state machine step.
86
86
 
87
87
  :param Case case: Generated test case data that should be sent in an API call to the tested API operation.
@@ -177,7 +177,7 @@ class APIStateMachine(RuleBasedStateMachine):
177
177
  method = self._get_call_method(case)
178
178
  return method(**kwargs)
179
179
 
180
- def get_call_kwargs(self, case: Case) -> Dict[str, Any]:
180
+ def get_call_kwargs(self, case: Case) -> dict[str, Any]:
181
181
  """Create custom keyword arguments that will be passed to the :meth:`Case.call` method.
182
182
 
183
183
  Mostly they are proxied to the :func:`requests.request` call.
@@ -204,7 +204,7 @@ class APIStateMachine(RuleBasedStateMachine):
204
204
  return case.call
205
205
 
206
206
  def validate_response(
207
- self, response: GenericResponse, case: Case, additional_checks: Tuple[CheckFunction, ...] = ()
207
+ self, response: GenericResponse, case: Case, additional_checks: tuple[CheckFunction, ...] = ()
208
208
  ) -> None:
209
209
  """Validate an API response.
210
210
 
@@ -242,7 +242,7 @@ class APIStateMachine(RuleBasedStateMachine):
242
242
  return StepResult(response, case, elapsed)
243
243
 
244
244
 
245
- def _print_case(case: Case, kwargs: Dict[str, Any]) -> str:
245
+ def _print_case(case: Case, kwargs: dict[str, Any]) -> str:
246
246
  from requests.structures import CaseInsensitiveDict
247
247
 
248
248
  operation = f"state.schema['{case.operation.path}']['{case.operation.method.upper()}']"
schemathesis/targets.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
  from dataclasses import dataclass
3
- from typing import TYPE_CHECKING, Callable, Tuple
3
+ from typing import TYPE_CHECKING, Callable
4
4
 
5
5
  if TYPE_CHECKING:
6
6
  from .models import Case
@@ -16,7 +16,7 @@ class TargetContext:
16
16
  :ivar float response_time: API response time.
17
17
  """
18
18
 
19
- case: "Case"
19
+ case: Case
20
20
  response: GenericResponse
21
21
  response_time: float
22
22
 
@@ -28,7 +28,7 @@ def response_time(context: TargetContext) -> float:
28
28
  Target = Callable[[TargetContext], float]
29
29
  DEFAULT_TARGETS = ()
30
30
  OPTIONAL_TARGETS = (response_time,)
31
- ALL_TARGETS: Tuple[Target, ...] = DEFAULT_TARGETS + OPTIONAL_TARGETS
31
+ ALL_TARGETS: tuple[Target, ...] = DEFAULT_TARGETS + OPTIONAL_TARGETS
32
32
 
33
33
 
34
34
  def register(target: Target) -> Target:
@@ -1,5 +1,5 @@
1
1
  from __future__ import annotations
2
- from typing import Tuple, TYPE_CHECKING
2
+ from typing import TYPE_CHECKING
3
3
 
4
4
  from .exceptions import UsageError
5
5
 
@@ -8,7 +8,7 @@ if TYPE_CHECKING:
8
8
  from pyrate_limiter import Limiter
9
9
 
10
10
 
11
- def parse_units(rate: str) -> Tuple[int, int]:
11
+ def parse_units(rate: str) -> tuple[int, int]:
12
12
  from pyrate_limiter import Duration
13
13
 
14
14
  try:
@@ -1,5 +1,5 @@
1
1
  from __future__ import annotations
2
- from typing import Optional, Union, TYPE_CHECKING
2
+ from typing import TYPE_CHECKING
3
3
 
4
4
  from ..types import RawAuth
5
5
 
@@ -7,7 +7,7 @@ if TYPE_CHECKING:
7
7
  from requests.auth import HTTPDigestAuth
8
8
 
9
9
 
10
- def get_requests_auth(auth: Optional[RawAuth], auth_type: Optional[str]) -> Optional[Union[HTTPDigestAuth, RawAuth]]:
10
+ def get_requests_auth(auth: RawAuth | None, auth_type: str | None) -> HTTPDigestAuth | RawAuth | None:
11
11
  from requests.auth import HTTPDigestAuth
12
12
 
13
13
  if auth and auth_type == "digest":
@@ -21,6 +21,11 @@ def is_json_media_type(value: str) -> bool:
21
21
  return main == "application" and (sub == "json" or sub.endswith("+json"))
22
22
 
23
23
 
24
+ def is_yaml_media_type(value: str) -> bool:
25
+ """Detect whether the content type is YAML-compatible."""
26
+ return value in ("text/yaml", "text/x-yaml", "application/x-yaml", "text/vnd.yaml")
27
+
28
+
24
29
  def is_plain_text_media_type(value: str) -> bool:
25
30
  """Detect variations of the ``text/plain`` media type."""
26
31
  return parse_content_type(value) == ("text", "plain")
@@ -1,10 +1,11 @@
1
+ from __future__ import annotations
1
2
  import re
2
- from typing import Dict, Any
3
+ from typing import Any
3
4
 
4
5
  from ..constants import USER_AGENT
5
6
 
6
7
 
7
- def setup_default_headers(kwargs: Dict[str, Any]) -> None:
8
+ def setup_default_headers(kwargs: dict[str, Any]) -> None:
8
9
  headers = kwargs.setdefault("headers", {})
9
10
  if "user-agent" not in {header.lower() for header in headers}:
10
11
  kwargs["headers"]["User-Agent"] = USER_AGENT
@@ -58,7 +58,7 @@ def copy_response(response: GenericResponse) -> GenericResponse:
58
58
 
59
59
  def get_reason(status_code: int) -> str:
60
60
  if sys.version_info < (3, 9) and status_code == 418:
61
- # Python 3.7 & 3.8 do not have 418 status in the `HTTPStatus` enum
61
+ # Python 3.8 does not have 418 status in the `HTTPStatus` enum
62
62
  return "I'm a Teapot"
63
63
 
64
64
  import http.client
schemathesis/utils.py CHANGED
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  import functools
2
3
  import operator
3
4
  from contextlib import contextmanager
@@ -5,12 +6,8 @@ from inspect import getfullargspec
5
6
  from typing import (
6
7
  Any,
7
8
  Callable,
8
- Dict,
9
9
  Generator,
10
- List,
11
10
  NoReturn,
12
- Optional,
13
- Tuple,
14
11
  Union,
15
12
  )
16
13
 
@@ -57,7 +54,7 @@ IGNORED_PATTERNS = (
57
54
 
58
55
 
59
56
  @contextmanager
60
- def capture_hypothesis_output() -> Generator[List[str], None, None]:
57
+ def capture_hypothesis_output() -> Generator[list[str], None, None]:
61
58
  """Capture all output of Hypothesis into a list of strings.
62
59
 
63
60
  It allows us to have more granular control over Schemathesis output.
@@ -91,11 +88,11 @@ GIVEN_ARGS_MARKER = "_schemathesis_given_args"
91
88
  GIVEN_KWARGS_MARKER = "_schemathesis_given_kwargs"
92
89
 
93
90
 
94
- def get_given_args(func: GenericTest) -> Tuple:
91
+ def get_given_args(func: GenericTest) -> tuple:
95
92
  return getattr(func, GIVEN_ARGS_MARKER, ())
96
93
 
97
94
 
98
- def get_given_kwargs(func: GenericTest) -> Dict[str, Any]:
95
+ def get_given_kwargs(func: GenericTest) -> dict[str, Any]:
99
96
  return getattr(func, GIVEN_KWARGS_MARKER, {})
100
97
 
101
98
 
@@ -124,7 +121,7 @@ def given_proxy(*args: GivenInput, **kwargs: GivenInput) -> Callable[[GenericTes
124
121
  return wrapper
125
122
 
126
123
 
127
- def merge_given_args(func: GenericTest, args: Tuple, kwargs: Dict[str, Any]) -> Dict[str, Any]:
124
+ def merge_given_args(func: GenericTest, args: tuple, kwargs: dict[str, Any]) -> dict[str, Any]:
128
125
  """Merge positional arguments to ``@schema.given`` into a dictionary with keyword arguments.
129
126
 
130
127
  Kwargs are modified inplace.
@@ -136,7 +133,7 @@ def merge_given_args(func: GenericTest, args: Tuple, kwargs: Dict[str, Any]) ->
136
133
  return kwargs
137
134
 
138
135
 
139
- def validate_given_args(func: GenericTest, args: Tuple, kwargs: Dict[str, Any]) -> Optional[Callable]:
136
+ def validate_given_args(func: GenericTest, args: tuple, kwargs: dict[str, Any]) -> Callable | None:
140
137
  signature = get_signature(func)
141
138
  return is_invalid_test(func, signature, args, kwargs) # type: ignore
142
139
 
@@ -150,7 +147,7 @@ def compose(*functions: Callable) -> Callable:
150
147
  return functools.reduce(lambda f, g: lambda x: f(g(x)), functions, noop)
151
148
 
152
149
 
153
- def combine_strategies(strategies: List[st.SearchStrategy]) -> st.SearchStrategy:
150
+ def combine_strategies(strategies: list[st.SearchStrategy]) -> st.SearchStrategy:
154
151
  """Combine a list of strategies into a single one.
155
152
 
156
153
  If the input is `[a, b, c]`, then the result is equivalent to `a | b | c`.