schemathesis 3.35.3__py3-none-any.whl → 3.35.5__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 (81) hide show
  1. schemathesis/__init__.py +5 -5
  2. schemathesis/_hypothesis.py +12 -6
  3. schemathesis/_override.py +4 -4
  4. schemathesis/auths.py +1 -1
  5. schemathesis/cli/__init__.py +19 -13
  6. schemathesis/cli/callbacks.py +6 -4
  7. schemathesis/cli/cassettes.py +67 -41
  8. schemathesis/cli/context.py +7 -6
  9. schemathesis/cli/junitxml.py +1 -1
  10. schemathesis/cli/options.py +7 -4
  11. schemathesis/cli/output/default.py +5 -5
  12. schemathesis/cli/reporting.py +4 -2
  13. schemathesis/code_samples.py +4 -3
  14. schemathesis/exceptions.py +4 -3
  15. schemathesis/extra/_flask.py +4 -1
  16. schemathesis/extra/pytest_plugin.py +6 -3
  17. schemathesis/failures.py +2 -1
  18. schemathesis/filters.py +2 -2
  19. schemathesis/generation/__init__.py +2 -2
  20. schemathesis/generation/_hypothesis.py +1 -1
  21. schemathesis/generation/coverage.py +5 -5
  22. schemathesis/graphql.py +0 -1
  23. schemathesis/hooks.py +3 -3
  24. schemathesis/lazy.py +10 -7
  25. schemathesis/loaders.py +3 -3
  26. schemathesis/models.py +39 -15
  27. schemathesis/runner/__init__.py +5 -5
  28. schemathesis/runner/events.py +1 -1
  29. schemathesis/runner/impl/context.py +58 -0
  30. schemathesis/runner/impl/core.py +54 -61
  31. schemathesis/runner/impl/solo.py +17 -20
  32. schemathesis/runner/impl/threadpool.py +65 -71
  33. schemathesis/runner/serialization.py +4 -3
  34. schemathesis/sanitization.py +2 -1
  35. schemathesis/schemas.py +18 -20
  36. schemathesis/serializers.py +2 -0
  37. schemathesis/service/client.py +1 -1
  38. schemathesis/service/events.py +4 -1
  39. schemathesis/service/extensions.py +2 -2
  40. schemathesis/service/hosts.py +4 -2
  41. schemathesis/service/models.py +3 -3
  42. schemathesis/service/report.py +3 -3
  43. schemathesis/service/serialization.py +4 -2
  44. schemathesis/specs/graphql/loaders.py +4 -3
  45. schemathesis/specs/graphql/schemas.py +4 -3
  46. schemathesis/specs/openapi/definitions.py +1 -5
  47. schemathesis/specs/openapi/examples.py +92 -2
  48. schemathesis/specs/openapi/expressions/__init__.py +7 -0
  49. schemathesis/specs/openapi/expressions/extractors.py +4 -1
  50. schemathesis/specs/openapi/expressions/nodes.py +5 -3
  51. schemathesis/specs/openapi/links.py +4 -4
  52. schemathesis/specs/openapi/loaders.py +5 -4
  53. schemathesis/specs/openapi/negative/__init__.py +5 -3
  54. schemathesis/specs/openapi/negative/mutations.py +5 -4
  55. schemathesis/specs/openapi/parameters.py +11 -8
  56. schemathesis/specs/openapi/schemas.py +9 -10
  57. schemathesis/specs/openapi/security.py +6 -4
  58. schemathesis/specs/openapi/stateful/__init__.py +2 -2
  59. schemathesis/specs/openapi/stateful/statistic.py +3 -3
  60. schemathesis/specs/openapi/stateful/types.py +3 -2
  61. schemathesis/stateful/__init__.py +3 -3
  62. schemathesis/stateful/config.py +1 -1
  63. schemathesis/stateful/context.py +3 -3
  64. schemathesis/stateful/events.py +3 -3
  65. schemathesis/stateful/runner.py +5 -4
  66. schemathesis/stateful/sink.py +1 -1
  67. schemathesis/stateful/state_machine.py +5 -5
  68. schemathesis/stateful/statistic.py +3 -1
  69. schemathesis/stateful/validation.py +1 -1
  70. schemathesis/transports/__init__.py +2 -2
  71. schemathesis/transports/asgi.py +7 -0
  72. schemathesis/transports/auth.py +2 -1
  73. schemathesis/transports/content_types.py +1 -1
  74. schemathesis/transports/responses.py +2 -1
  75. schemathesis/utils.py +4 -2
  76. {schemathesis-3.35.3.dist-info → schemathesis-3.35.5.dist-info}/METADATA +1 -1
  77. schemathesis-3.35.5.dist-info/RECORD +156 -0
  78. schemathesis-3.35.3.dist-info/RECORD +0 -154
  79. {schemathesis-3.35.3.dist-info → schemathesis-3.35.5.dist-info}/WHEEL +0 -0
  80. {schemathesis-3.35.3.dist-info → schemathesis-3.35.5.dist-info}/entry_points.txt +0 -0
  81. {schemathesis-3.35.3.dist-info → schemathesis-3.35.5.dist-info}/licenses/LICENSE +0 -0
@@ -4,10 +4,9 @@ from contextlib import suppress
4
4
  from dataclasses import dataclass
5
5
  from functools import lru_cache
6
6
  from itertools import chain, cycle, islice
7
- from typing import TYPE_CHECKING, Any, Generator, Union, cast
7
+ from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast
8
8
 
9
9
  import requests
10
- from hypothesis.strategies import SearchStrategy
11
10
  from hypothesis_jsonschema import from_schema
12
11
 
13
12
  from ...constants import DEFAULT_RESPONSE_TIMEOUT
@@ -20,6 +19,8 @@ from .formats import STRING_FORMATS
20
19
  from .parameters import OpenAPIBody, OpenAPIParameter
21
20
 
22
21
  if TYPE_CHECKING:
22
+ from hypothesis.strategies import SearchStrategy
23
+
23
24
  from ...generation import GenerationConfig
24
25
 
25
26
 
@@ -77,6 +78,7 @@ def get_strategies_from_examples(
77
78
 
78
79
  def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Generator[Example, None, None]:
79
80
  """Extract top-level parameter examples from `examples` & `example` fields."""
81
+ responses = find_in_responses(operation)
80
82
  for parameter in operation.iter_parameters():
81
83
  if "schema" in parameter.definition:
82
84
  definitions = [parameter.definition, *_expand_subschemas(parameter.definition["schema"])]
@@ -106,6 +108,10 @@ def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Genera
106
108
  yield ParameterExample(
107
109
  container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
108
110
  )
111
+ for value in find_matching_in_responses(responses, parameter.name):
112
+ yield ParameterExample(
113
+ container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
114
+ )
109
115
  for alternative in operation.body:
110
116
  alternative = cast(OpenAPIBody, alternative)
111
117
  if "schema" in alternative.definition:
@@ -349,3 +355,87 @@ def _produce_parameter_combinations(parameters: dict[str, dict[str, list]]) -> G
349
355
  }
350
356
  for container, variants in parameters.items()
351
357
  }
358
+
359
+
360
+ def find_in_responses(operation: APIOperation) -> dict[str, list[dict[str, Any]]]:
361
+ """Find schema examples in responses."""
362
+ output: dict[str, list[dict[str, Any]]] = {}
363
+ for status_code, response in operation.definition.raw.get("responses", {}).items():
364
+ if not str(status_code).startswith("2"):
365
+ # Check only 2xx responses
366
+ continue
367
+ if isinstance(response, dict) and "$ref" in response:
368
+ _, response = operation.schema.resolver.resolve_in_scope(response, operation.definition.scope) # type:ignore[attr-defined]
369
+ for media_type, definition in response.get("content", {}).items():
370
+ schema_ref = definition.get("schema", {}).get("$ref")
371
+ if schema_ref:
372
+ name = schema_ref.split("/")[-1]
373
+ else:
374
+ name = f"{status_code}/{media_type}"
375
+ for examples_field, example_field in (
376
+ ("examples", "example"),
377
+ ("x-examples", "x-example"),
378
+ ):
379
+ examples = definition.get(examples_field, {})
380
+ for example in examples.values():
381
+ if "value" in example:
382
+ output.setdefault(name, []).append(example["value"])
383
+ if example_field in definition:
384
+ output.setdefault(name, []).append(definition[example_field])
385
+ return output
386
+
387
+
388
+ NOT_FOUND = object()
389
+
390
+
391
+ def find_matching_in_responses(examples: dict[str, list], param: str) -> Iterator[Any]:
392
+ """Find matching parameter examples."""
393
+ normalized = param.lower()
394
+ is_id_param = normalized.endswith("id")
395
+ # Extract values from response examples that match input parameters.
396
+ # E.g., for `GET /orders/{id}/`, use "id" or "orderId" from `Order` response
397
+ # as examples for the "id" path parameter.
398
+ for schema_name, schema_examples in examples.items():
399
+ for example in schema_examples:
400
+ if not isinstance(example, dict):
401
+ continue
402
+ # Unwrapping example from `{"item": [{...}]}`
403
+ if isinstance(example, dict) and len(example) == 1 and list(example)[0].lower() == schema_name.lower():
404
+ inner = list(example.values())[0]
405
+ if isinstance(inner, list):
406
+ for sub_example in inner:
407
+ found = _find_matching_in_responses(sub_example, schema_name, param, normalized, is_id_param)
408
+ if found is not NOT_FOUND:
409
+ yield found
410
+ continue
411
+ example = inner
412
+ found = _find_matching_in_responses(example, schema_name, param, normalized, is_id_param)
413
+ if found is not NOT_FOUND:
414
+ yield found
415
+
416
+
417
+ def _find_matching_in_responses(
418
+ example: dict[str, Any], schema_name: str, param: str, normalized: str, is_id_param: bool
419
+ ) -> Any:
420
+ # Check for exact match
421
+ if param in example:
422
+ return example[param]
423
+
424
+ # Check for case-insensitive match
425
+ for key in example:
426
+ if key.lower() == normalized:
427
+ return example[key]
428
+ else:
429
+ # If no match found and it's an ID parameter, try additional checks
430
+ if is_id_param:
431
+ # Check for 'id' if parameter is '{something}Id'
432
+ if "id" in example:
433
+ return example["id"]
434
+ # Check for '{schemaName}Id' or '{schemaName}_id'
435
+ if normalized == "id" or normalized.startswith(schema_name.lower()):
436
+ for key in (schema_name, schema_name.lower()):
437
+ for suffix in ("_id", "Id"):
438
+ with_suffix = f"{key}{suffix}"
439
+ if with_suffix in example:
440
+ return example[with_suffix]
441
+ return NOT_FOUND
@@ -11,6 +11,13 @@ from typing import Any
11
11
  from . import lexer, nodes, parser
12
12
  from .context import ExpressionContext
13
13
 
14
+ __all__ = [
15
+ "lexer",
16
+ "nodes",
17
+ "parser",
18
+ "ExpressionContext",
19
+ ]
20
+
14
21
 
15
22
  def evaluate(expr: Any, context: ExpressionContext, evaluate_nested: bool = False) -> Any:
16
23
  """Evaluate runtime expression in context."""
@@ -1,7 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
- import re
4
3
  from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ import re
5
8
 
6
9
 
7
10
  @dataclass
@@ -4,13 +4,15 @@ from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass
6
6
  from enum import Enum, unique
7
- from typing import Any
7
+ from typing import TYPE_CHECKING, Any
8
8
 
9
9
  from requests.structures import CaseInsensitiveDict
10
10
 
11
11
  from .. import references
12
- from .context import ExpressionContext
13
- from .extractors import Extractor
12
+
13
+ if TYPE_CHECKING:
14
+ from .context import ExpressionContext
15
+ from .extractors import Extractor
14
16
 
15
17
 
16
18
  @dataclass
@@ -10,22 +10,22 @@ from difflib import get_close_matches
10
10
  from types import SimpleNamespace
11
11
  from typing import TYPE_CHECKING, Any, Generator, Literal, NoReturn, Sequence, TypedDict, Union, cast
12
12
 
13
- from jsonschema import RefResolver
14
-
15
13
  from ...constants import NOT_SET
16
14
  from ...internal.copy import fast_deepcopy
17
15
  from ...models import APIOperation, Case, TransitionId
18
- from ...parameters import ParameterSet
19
16
  from ...stateful import ParsedData, StatefulTest, UnresolvableLink
20
17
  from ...stateful.state_machine import Direction
21
- from ...types import NotSet
22
18
  from . import expressions
23
19
  from .constants import LOCATION_TO_CONTAINER
24
20
  from .parameters import OpenAPI20Body, OpenAPI30Body, OpenAPIParameter
25
21
  from .references import RECURSION_DEPTH_LIMIT, Unresolvable
26
22
 
27
23
  if TYPE_CHECKING:
24
+ from jsonschema import RefResolver
25
+
26
+ from ...parameters import ParameterSet
28
27
  from ...transports.responses import GenericResponse
28
+ from ...types import NotSet
29
29
 
30
30
 
31
31
  @dataclass(repr=False)
@@ -9,7 +9,7 @@ from urllib.parse import urljoin
9
9
 
10
10
  from ... import experimental, fixups
11
11
  from ...code_samples import CodeSampleStyle
12
- from ...constants import NOT_SET, WAIT_FOR_SCHEMA_INTERVAL
12
+ from ...constants import DEFAULT_RESPONSE_TIMEOUT, NOT_SET, WAIT_FOR_SCHEMA_INTERVAL
13
13
  from ...exceptions import SchemaError, SchemaErrorType
14
14
  from ...filters import filter_set_from_components
15
15
  from ...generation import (
@@ -163,6 +163,7 @@ def from_uri(
163
163
  interval=WAIT_FOR_SCHEMA_INTERVAL,
164
164
  )
165
165
  def _load_schema(_uri: str, **_kwargs: Any) -> requests.Response:
166
+ _kwargs.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
166
167
  return requests.get(_uri, **kwargs)
167
168
 
168
169
  else:
@@ -441,7 +442,7 @@ def _format_status_codes(status_codes: list[tuple[int, list[str | int]]]) -> str
441
442
  for status_code, path in status_codes:
442
443
  buffer.write(f" - {status_code} at schema['paths']")
443
444
  for chunk in path:
444
- buffer.write(f"[{repr(chunk)}]")
445
+ buffer.write(f"[{chunk!r}]")
445
446
  buffer.write("['responses']\n")
446
447
  return buffer.getvalue().rstrip()
447
448
 
@@ -595,9 +596,9 @@ def from_wsgi(
595
596
 
596
597
 
597
598
  def get_loader_for_app(app: Any) -> Callable:
598
- from starlette.applications import Starlette
599
+ from ...transports.asgi import is_asgi_app
599
600
 
600
- if isinstance(app, Starlette):
601
+ if is_asgi_app(app):
601
602
  return from_asgi
602
603
  if app.__class__.__module__.startswith("aiohttp."):
603
604
  return from_aiohttp
@@ -2,17 +2,19 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from functools import lru_cache
5
- from typing import Any
5
+ from typing import TYPE_CHECKING, Any
6
6
  from urllib.parse import urlencode
7
7
 
8
8
  import jsonschema
9
9
  from hypothesis import strategies as st
10
10
  from hypothesis_jsonschema import from_schema
11
11
 
12
- from ....generation import GenerationConfig
13
12
  from ..constants import ALL_KEYWORDS
14
13
  from .mutations import MutationContext
15
- from .types import Draw, Schema
14
+
15
+ if TYPE_CHECKING:
16
+ from ....generation import GenerationConfig
17
+ from .types import Draw, Schema
16
18
 
17
19
 
18
20
  @dataclass
@@ -179,7 +179,7 @@ def remove_required_property(context: MutationContext, draw: Draw, schema: Schem
179
179
  else:
180
180
  candidate = draw(st.sampled_from(sorted(required)))
181
181
  enabled_properties = draw(st.shared(FeatureStrategy(), key="properties")) # type: ignore
182
- candidates = [candidate] + sorted([prop for prop in required if enabled_properties.is_enabled(prop)])
182
+ candidates = [candidate, *sorted([prop for prop in required if enabled_properties.is_enabled(prop)])]
183
183
  property_name = draw(st.sampled_from(candidates))
184
184
  required.remove(property_name)
185
185
  if not required:
@@ -226,9 +226,10 @@ def change_type(context: MutationContext, draw: Draw, schema: Schema) -> Mutatio
226
226
  candidate = draw(st.sampled_from(sorted(candidates)))
227
227
  candidates.remove(candidate)
228
228
  enabled_types = draw(st.shared(FeatureStrategy(), key="types")) # type: ignore
229
- remaining_candidates = [candidate] + sorted(
230
- [candidate for candidate in candidates if enabled_types.is_enabled(candidate)]
231
- )
229
+ remaining_candidates = [
230
+ candidate,
231
+ *sorted([candidate for candidate in candidates if enabled_types.is_enabled(candidate)]),
232
+ ]
232
233
  new_type = draw(st.sampled_from(remaining_candidates))
233
234
  schema["type"] = new_type
234
235
  prevent_unsatisfiable_schema(schema, new_type)
@@ -2,13 +2,15 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  from dataclasses import dataclass
5
- from typing import Any, ClassVar, Iterable
5
+ from typing import TYPE_CHECKING, Any, ClassVar, Iterable
6
6
 
7
7
  from ...exceptions import OperationSchemaError
8
- from ...models import APIOperation
9
8
  from ...parameters import Parameter
10
9
  from .converter import to_json_schema_recursive
11
10
 
11
+ if TYPE_CHECKING:
12
+ from ...models import APIOperation
13
+
12
14
 
13
15
  @dataclass(eq=False)
14
16
  class OpenAPIParameter(Parameter):
@@ -53,14 +55,15 @@ class OpenAPIParameter(Parameter):
53
55
  def as_json_schema(self, operation: APIOperation) -> dict[str, Any]:
54
56
  """Convert parameter's definition to JSON Schema."""
55
57
  # JSON Schema allows `examples` as an array
58
+ examples = []
56
59
  if self.examples_field in self.definition:
57
- examples = [
58
- example["value"] for example in self.definition[self.examples_field].values() if "value" in example
59
- ]
60
- else:
61
- examples = None
60
+ examples.extend(
61
+ [example["value"] for example in self.definition[self.examples_field].values() if "value" in example]
62
+ )
63
+ if self.example_field in self.definition:
64
+ examples.append(self.definition[self.example_field])
62
65
  schema = self.from_open_api_to_json_schema(operation, self.definition)
63
- if examples is not None:
66
+ if examples:
64
67
  schema["examples"] = examples
65
68
  return self.transform_keywords(schema)
66
69
 
@@ -20,21 +20,18 @@ from typing import (
20
20
  Mapping,
21
21
  NoReturn,
22
22
  Sequence,
23
- Type,
24
23
  TypeVar,
25
24
  cast,
26
25
  )
27
26
  from urllib.parse import urlsplit
28
27
 
29
28
  import jsonschema
30
- from hypothesis.strategies import SearchStrategy
31
29
  from packaging import version
32
30
  from requests.structures import CaseInsensitiveDict
33
31
 
34
32
  from ... import experimental, failures
35
33
  from ..._compat import MultipleFailures
36
34
  from ..._override import CaseOverride, check_no_override_mark, set_override_mark
37
- from ...auths import AuthStorage
38
35
  from ...constants import HTTP_METHODS, NOT_SET
39
36
  from ...exceptions import (
40
37
  InternalError,
@@ -54,10 +51,8 @@ from ...internal.result import Err, Ok, Result
54
51
  from ...models import APIOperation, Case, OperationDefinition
55
52
  from ...schemas import APIOperationMap, BaseSchema
56
53
  from ...stateful import Stateful, StatefulTest
57
- from ...stateful.state_machine import APIStateMachine
58
54
  from ...transports.content_types import is_json_media_type, parse_content_type
59
55
  from ...transports.responses import get_json
60
- from ...types import Body, Cookies, FormData, GenericTest, Headers, NotSet, PathParameters, Query
61
56
  from . import links, serialization
62
57
  from ._cache import OperationCache
63
58
  from ._hypothesis import get_case_strategy
@@ -83,7 +78,12 @@ from .security import BaseSecurityProcessor, OpenAPISecurityProcessor, SwaggerSe
83
78
  from .stateful import create_state_machine
84
79
 
85
80
  if TYPE_CHECKING:
81
+ from hypothesis.strategies import SearchStrategy
82
+
83
+ from ...auths import AuthStorage
84
+ from ...stateful.state_machine import APIStateMachine
86
85
  from ...transports.responses import GenericResponse
86
+ from ...types import Body, Cookies, FormData, GenericTest, Headers, NotSet, PathParameters, Query
87
87
 
88
88
  SCHEMA_ERROR_MESSAGE = "Ensure that the definition complies with the OpenAPI specification"
89
89
  SCHEMA_PARSING_ERRORS = (KeyError, AttributeError, jsonschema.exceptions.RefResolutionError)
@@ -626,7 +626,7 @@ class BaseOpenAPISchema(BaseSchema):
626
626
  return operation.definition.raw.get("tags")
627
627
 
628
628
  @property
629
- def validator_cls(self) -> Type[jsonschema.Validator]:
629
+ def validator_cls(self) -> type[jsonschema.Validator]:
630
630
  if self.spec_version.startswith("3.1") and experimental.OPEN_API_3_1.is_enabled:
631
631
  return jsonschema.Draft202012Validator
632
632
  return jsonschema.Draft4Validator
@@ -791,11 +791,10 @@ class BaseOpenAPISchema(BaseSchema):
791
791
 
792
792
  def _maybe_raise_one_or_more(errors: list[Exception]) -> None:
793
793
  if not errors:
794
- return None
795
- elif len(errors) == 1:
794
+ return
795
+ if len(errors) == 1:
796
796
  raise errors[0]
797
- else:
798
- raise MultipleFailures("\n\n".join(str(error) for error in errors), errors)
797
+ raise MultipleFailures("\n\n".join(str(error) for error in errors), errors)
799
798
 
800
799
 
801
800
  def _make_reference_key(scopes: list[str], reference: str) -> str:
@@ -3,13 +3,15 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass
6
- from typing import Any, ClassVar, Generator
6
+ from typing import TYPE_CHECKING, Any, ClassVar, Generator
7
7
 
8
- from jsonschema import RefResolver
9
-
10
- from ...models import APIOperation
11
8
  from .parameters import OpenAPI20Parameter, OpenAPI30Parameter, OpenAPIParameter
12
9
 
10
+ if TYPE_CHECKING:
11
+ from jsonschema import RefResolver
12
+
13
+ from ...models import APIOperation
14
+
13
15
 
14
16
  @dataclass
15
17
  class BaseSecurityProcessor:
@@ -16,11 +16,11 @@ from .. import expressions
16
16
  from ..links import get_all_links
17
17
  from ..utils import expand_status_code
18
18
  from .statistic import OpenAPILinkStats
19
- from .types import FilterFunction, LinkName, StatusCode, TargetName
20
19
 
21
20
  if TYPE_CHECKING:
22
21
  from ....models import Case
23
22
  from ..schemas import BaseOpenAPISchema
23
+ from .types import FilterFunction, LinkName, StatusCode, TargetName
24
24
 
25
25
 
26
26
  class OpenAPIStateMachine(APIStateMachine):
@@ -193,7 +193,7 @@ def make_response_matcher(matchers: list[tuple[str, FilterFunction]]) -> Callabl
193
193
  return compare
194
194
 
195
195
 
196
- @lru_cache()
196
+ @lru_cache
197
197
  def make_response_filter(status_code: str, all_status_codes: Iterator[str]) -> FilterFunction:
198
198
  """Create a filter for stored responses.
199
199
 
@@ -1,14 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass, field
4
- from typing import TYPE_CHECKING, Iterator, List, Union
4
+ from typing import TYPE_CHECKING, Iterator, Union
5
5
 
6
6
  from ....internal.copy import fast_deepcopy
7
7
  from ....stateful.statistic import TransitionStats
8
- from .types import AggregatedResponseCounter, LinkName, ResponseCounter, SourceName, StatusCode, TargetName
9
8
 
10
9
  if TYPE_CHECKING:
11
10
  from ....stateful import events
11
+ from .types import AggregatedResponseCounter, LinkName, ResponseCounter, SourceName, StatusCode, TargetName
12
12
 
13
13
 
14
14
  @dataclass
@@ -136,7 +136,7 @@ class OpenAPILinkStats(TransitionStats):
136
136
  def to_formatted_table(self, width: int) -> str:
137
137
  """Format the statistic as a table."""
138
138
  entries = list(self.iter_with_format())
139
- lines: List[str | list[str]] = [HEADER, ""]
139
+ lines: list[str | list[str]] = [HEADER, ""]
140
140
  column_widths = [len(column) for column in HEADER]
141
141
  for entry in entries:
142
142
  if isinstance(entry.entry, Link):
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Callable, Dict, TypedDict, Union
3
+ from typing import TYPE_CHECKING, Callable, Dict, TypedDict, Union
4
4
 
5
- from ....stateful.state_machine import StepResult
5
+ if TYPE_CHECKING:
6
+ from ....stateful.state_machine import StepResult
6
7
 
7
8
  StatusCode = str
8
9
  LinkName = str
@@ -5,15 +5,15 @@ import json
5
5
  from dataclasses import dataclass, field
6
6
  from typing import TYPE_CHECKING, Any, Callable, Generator
7
7
 
8
- from .. import GenerationConfig
9
8
  from ..constants import NOT_SET
10
- from ..exceptions import OperationSchemaError
11
9
  from ..internal.result import Ok, Result
12
- from ..models import APIOperation, Case
13
10
 
14
11
  if TYPE_CHECKING:
15
12
  import hypothesis
16
13
 
14
+ from .. import GenerationConfig
15
+ from ..exceptions import OperationSchemaError
16
+ from ..models import APIOperation, Case
17
17
  from ..transports.responses import GenericResponse
18
18
  from .state_machine import APIStateMachine
19
19
 
@@ -21,7 +21,7 @@ def _default_checks_factory() -> tuple[CheckFunction, ...]:
21
21
  from ..checks import ALL_CHECKS
22
22
  from ..specs.openapi.checks import ensure_resource_availability, use_after_free
23
23
 
24
- return ALL_CHECKS + (use_after_free, ensure_resource_availability)
24
+ return (*ALL_CHECKS, use_after_free, ensure_resource_availability)
25
25
 
26
26
 
27
27
  def _get_default_hypothesis_settings_kwargs() -> dict[str, Any]:
@@ -57,11 +57,11 @@ class RunnerContext:
57
57
  def current_scenario_status(self) -> events.ScenarioStatus:
58
58
  if self.current_step_status == events.StepStatus.SUCCESS:
59
59
  return events.ScenarioStatus.SUCCESS
60
- elif self.current_step_status == events.StepStatus.FAILURE:
60
+ if self.current_step_status == events.StepStatus.FAILURE:
61
61
  return events.ScenarioStatus.FAILURE
62
- elif self.current_step_status == events.StepStatus.ERROR:
62
+ if self.current_step_status == events.StepStatus.ERROR:
63
63
  return events.ScenarioStatus.ERROR
64
- elif self.current_step_status == events.StepStatus.INTERRUPTED:
64
+ if self.current_step_status == events.StepStatus.INTERRUPTED:
65
65
  return events.ScenarioStatus.INTERRUPTED
66
66
  return events.ScenarioStatus.REJECTED
67
67
 
@@ -4,7 +4,7 @@ import time
4
4
  from dataclasses import asdict as _asdict
5
5
  from dataclasses import dataclass
6
6
  from enum import Enum
7
- from typing import TYPE_CHECKING, Any, Type
7
+ from typing import TYPE_CHECKING, Any
8
8
 
9
9
  from ..exceptions import format_exception
10
10
 
@@ -40,11 +40,11 @@ class RunStarted(StatefulEvent):
40
40
  """Before executing all scenarios."""
41
41
 
42
42
  started_at: float
43
- state_machine: Type[APIStateMachine]
43
+ state_machine: type[APIStateMachine]
44
44
 
45
45
  __slots__ = ("state_machine", "timestamp", "started_at")
46
46
 
47
- def __init__(self, *, state_machine: Type[APIStateMachine]) -> None:
47
+ def __init__(self, *, state_machine: type[APIStateMachine]) -> None:
48
48
  self.state_machine = state_machine
49
49
  self.started_at = time.time()
50
50
  self.timestamp = time.monotonic()
@@ -4,13 +4,12 @@ import queue
4
4
  import threading
5
5
  from contextlib import contextmanager
6
6
  from dataclasses import dataclass, field
7
- from typing import TYPE_CHECKING, Any, Generator, Iterator, Type
7
+ from typing import TYPE_CHECKING, Any, Generator, Iterator
8
8
 
9
9
  import hypothesis
10
10
  import requests
11
11
  from hypothesis.control import current_build_context
12
12
  from hypothesis.errors import Flaky, Unsatisfiable
13
- from hypothesis.stateful import Rule
14
13
 
15
14
  from ..exceptions import CheckFailed
16
15
  from ..targets import TargetMetricCollector
@@ -20,6 +19,8 @@ from .context import RunnerContext
20
19
  from .validation import validate_response
21
20
 
22
21
  if TYPE_CHECKING:
22
+ from hypothesis.stateful import Rule
23
+
23
24
  from ..models import Case, CheckFunction
24
25
  from ..transports.responses import GenericResponse
25
26
  from .state_machine import APIStateMachine, Direction, StepResult
@@ -36,7 +37,7 @@ class StatefulTestRunner:
36
37
  """
37
38
 
38
39
  # State machine class to use
39
- state_machine: Type[APIStateMachine]
40
+ state_machine: type[APIStateMachine]
40
41
  # Test runner configuration that defines the runtime behavior
41
42
  config: StatefulTestRunnerConfig = field(default_factory=StatefulTestRunnerConfig)
42
43
  # Event to stop the execution
@@ -105,7 +106,7 @@ def thread_manager(thread: threading.Thread) -> Generator[None, None, None]:
105
106
 
106
107
  def _execute_state_machine_loop(
107
108
  *,
108
- state_machine: Type[APIStateMachine],
109
+ state_machine: type[APIStateMachine],
109
110
  event_queue: queue.Queue,
110
111
  config: StatefulTestRunnerConfig,
111
112
  stop_event: threading.Event,
@@ -4,10 +4,10 @@ from dataclasses import dataclass, field
4
4
  from typing import TYPE_CHECKING
5
5
 
6
6
  from . import events
7
- from .statistic import TransitionStats
8
7
 
9
8
  if TYPE_CHECKING:
10
9
  from ..models import Check
10
+ from .statistic import TransitionStats
11
11
 
12
12
 
13
13
  @dataclass
@@ -4,7 +4,7 @@ import re
4
4
  import time
5
5
  from dataclasses import dataclass
6
6
  from functools import lru_cache
7
- from typing import TYPE_CHECKING, Any, ClassVar, Type
7
+ from typing import TYPE_CHECKING, Any, ClassVar
8
8
 
9
9
  from hypothesis.errors import InvalidDefinition
10
10
  from hypothesis.stateful import RuleBasedStateMachine
@@ -16,7 +16,6 @@ from ..models import APIOperation, Case, CheckFunction
16
16
  from .config import _default_hypothesis_settings_factory
17
17
  from .runner import StatefulTestRunner, StatefulTestRunnerConfig
18
18
  from .sink import StateMachineSink
19
- from .statistic import TransitionStats
20
19
 
21
20
  if TYPE_CHECKING:
22
21
  import hypothesis
@@ -24,6 +23,7 @@ if TYPE_CHECKING:
24
23
 
25
24
  from ..schemas import BaseSchema
26
25
  from ..transports.responses import GenericResponse
26
+ from .statistic import TransitionStats
27
27
 
28
28
 
29
29
  @dataclass
@@ -64,7 +64,7 @@ class APIStateMachine(RuleBasedStateMachine):
64
64
 
65
65
  @classmethod
66
66
  @lru_cache
67
- def _to_test_case(cls) -> Type:
67
+ def _to_test_case(cls) -> type:
68
68
  from . import run_state_machine_as_test
69
69
 
70
70
  class StateMachineTestCase(RuleBasedStateMachine.TestCase):
@@ -97,7 +97,7 @@ class APIStateMachine(RuleBasedStateMachine):
97
97
 
98
98
  def _add_result_to_targets(self, targets: tuple[str, ...], result: StepResult | None) -> None:
99
99
  if result is None:
100
- return None
100
+ return
101
101
  target = self._get_target_for_result(result)
102
102
  if target is not None:
103
103
  super()._add_result_to_targets((target,), result)
@@ -310,7 +310,7 @@ def _print_case(case: Case, kwargs: dict[str, Any]) -> str:
310
310
  headers.update(kwargs.get("headers", {}))
311
311
  case.headers = headers
312
312
  data = [
313
- f"{name}={repr(getattr(case, name))}"
313
+ f"{name}={getattr(case, name)!r}"
314
314
  for name in ("path_parameters", "headers", "cookies", "query", "body", "media_type")
315
315
  if getattr(case, name) not in (None, NOT_SET)
316
316
  ]
@@ -1,8 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
4
5
 
5
- from . import events
6
+ if TYPE_CHECKING:
7
+ from . import events
6
8
 
7
9
 
8
10
  @dataclass