schemathesis 3.35.4__py3-none-any.whl → 3.36.0__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 (85) 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/checks.py +8 -5
  6. schemathesis/cli/__init__.py +23 -26
  7. schemathesis/cli/callbacks.py +6 -4
  8. schemathesis/cli/cassettes.py +67 -41
  9. schemathesis/cli/context.py +7 -6
  10. schemathesis/cli/junitxml.py +1 -1
  11. schemathesis/cli/options.py +7 -4
  12. schemathesis/cli/output/default.py +5 -5
  13. schemathesis/cli/reporting.py +4 -2
  14. schemathesis/code_samples.py +4 -3
  15. schemathesis/contrib/unique_data.py +1 -2
  16. schemathesis/exceptions.py +4 -3
  17. schemathesis/extra/_flask.py +4 -1
  18. schemathesis/extra/pytest_plugin.py +6 -3
  19. schemathesis/failures.py +2 -1
  20. schemathesis/filters.py +2 -2
  21. schemathesis/generation/__init__.py +2 -2
  22. schemathesis/generation/_hypothesis.py +1 -1
  23. schemathesis/generation/coverage.py +53 -12
  24. schemathesis/graphql.py +0 -1
  25. schemathesis/hooks.py +3 -3
  26. schemathesis/internal/checks.py +53 -0
  27. schemathesis/lazy.py +10 -7
  28. schemathesis/loaders.py +3 -3
  29. schemathesis/models.py +59 -23
  30. schemathesis/runner/__init__.py +12 -6
  31. schemathesis/runner/events.py +1 -1
  32. schemathesis/runner/impl/context.py +72 -0
  33. schemathesis/runner/impl/core.py +105 -67
  34. schemathesis/runner/impl/solo.py +17 -20
  35. schemathesis/runner/impl/threadpool.py +65 -72
  36. schemathesis/runner/serialization.py +4 -3
  37. schemathesis/sanitization.py +2 -1
  38. schemathesis/schemas.py +20 -22
  39. schemathesis/serializers.py +2 -0
  40. schemathesis/service/client.py +1 -1
  41. schemathesis/service/events.py +4 -1
  42. schemathesis/service/extensions.py +2 -2
  43. schemathesis/service/hosts.py +4 -2
  44. schemathesis/service/models.py +3 -3
  45. schemathesis/service/report.py +3 -3
  46. schemathesis/service/serialization.py +4 -2
  47. schemathesis/specs/graphql/loaders.py +5 -4
  48. schemathesis/specs/graphql/schemas.py +13 -8
  49. schemathesis/specs/openapi/checks.py +76 -27
  50. schemathesis/specs/openapi/definitions.py +1 -5
  51. schemathesis/specs/openapi/examples.py +92 -2
  52. schemathesis/specs/openapi/expressions/__init__.py +7 -0
  53. schemathesis/specs/openapi/expressions/extractors.py +4 -1
  54. schemathesis/specs/openapi/expressions/nodes.py +5 -3
  55. schemathesis/specs/openapi/links.py +4 -4
  56. schemathesis/specs/openapi/loaders.py +6 -5
  57. schemathesis/specs/openapi/negative/__init__.py +5 -3
  58. schemathesis/specs/openapi/negative/mutations.py +5 -4
  59. schemathesis/specs/openapi/parameters.py +4 -2
  60. schemathesis/specs/openapi/schemas.py +28 -13
  61. schemathesis/specs/openapi/security.py +6 -4
  62. schemathesis/specs/openapi/stateful/__init__.py +2 -2
  63. schemathesis/specs/openapi/stateful/statistic.py +3 -3
  64. schemathesis/specs/openapi/stateful/types.py +3 -2
  65. schemathesis/stateful/__init__.py +3 -3
  66. schemathesis/stateful/config.py +2 -1
  67. schemathesis/stateful/context.py +13 -3
  68. schemathesis/stateful/events.py +3 -3
  69. schemathesis/stateful/runner.py +24 -6
  70. schemathesis/stateful/sink.py +1 -1
  71. schemathesis/stateful/state_machine.py +7 -6
  72. schemathesis/stateful/statistic.py +3 -1
  73. schemathesis/stateful/validation.py +10 -5
  74. schemathesis/transports/__init__.py +2 -2
  75. schemathesis/transports/asgi.py +7 -0
  76. schemathesis/transports/auth.py +2 -1
  77. schemathesis/transports/content_types.py +1 -1
  78. schemathesis/transports/responses.py +2 -1
  79. schemathesis/utils.py +4 -2
  80. {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/METADATA +1 -1
  81. schemathesis-3.36.0.dist-info/RECORD +157 -0
  82. schemathesis-3.35.4.dist-info/RECORD +0 -154
  83. {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/WHEEL +0 -0
  84. {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/entry_points.txt +0 -0
  85. {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/licenses/LICENSE +0 -0
@@ -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)
@@ -253,7 +253,7 @@ class BaseOpenAPISchema(BaseSchema):
253
253
  return self.collect_parameters(itertools.chain(parameters, shared_parameters), operation)
254
254
 
255
255
  def get_all_operations(
256
- self, hooks: HookDispatcher | None = None
256
+ self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
257
257
  ) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
258
258
  """Iterate over all operations defined in the API.
259
259
 
@@ -308,7 +308,17 @@ class BaseOpenAPISchema(BaseSchema):
308
308
  continue
309
309
  parameters = resolved.get("parameters", ())
310
310
  parameters = collect_parameters(itertools.chain(parameters, shared_parameters), resolved)
311
- operation = make_operation(path, method, parameters, entry, resolved, scope)
311
+ operation = make_operation(
312
+ path,
313
+ method,
314
+ parameters,
315
+ entry,
316
+ resolved,
317
+ scope,
318
+ with_security_parameters=generation_config.with_security_parameters
319
+ if generation_config
320
+ else None,
321
+ )
312
322
  context = HookContext(operation=operation)
313
323
  if (
314
324
  should_skip_operation(GLOBAL_HOOK_DISPATCHER, context)
@@ -383,6 +393,7 @@ class BaseOpenAPISchema(BaseSchema):
383
393
  raw: dict[str, Any],
384
394
  resolved: dict[str, Any],
385
395
  scope: str,
396
+ with_security_parameters: bool | None = None,
386
397
  ) -> APIOperation:
387
398
  """Create JSON schemas for the query, body, etc from Swagger parameters definitions."""
388
399
  __tracebackhide__ = True
@@ -397,7 +408,12 @@ class BaseOpenAPISchema(BaseSchema):
397
408
  )
398
409
  for parameter in parameters:
399
410
  operation.add_parameter(parameter)
400
- if self.generation_config.with_security_parameters:
411
+ with_security_parameters = (
412
+ with_security_parameters
413
+ if with_security_parameters is not None
414
+ else self.generation_config.with_security_parameters
415
+ )
416
+ if with_security_parameters:
401
417
  self.security.process_definitions(self.raw_schema, operation, self.resolver)
402
418
  self.dispatch_hook("before_init_operation", HookContext(operation=operation), operation)
403
419
  return operation
@@ -626,7 +642,7 @@ class BaseOpenAPISchema(BaseSchema):
626
642
  return operation.definition.raw.get("tags")
627
643
 
628
644
  @property
629
- def validator_cls(self) -> Type[jsonschema.Validator]:
645
+ def validator_cls(self) -> type[jsonschema.Validator]:
630
646
  if self.spec_version.startswith("3.1") and experimental.OPEN_API_3_1.is_enabled:
631
647
  return jsonschema.Draft202012Validator
632
648
  return jsonschema.Draft4Validator
@@ -791,11 +807,10 @@ class BaseOpenAPISchema(BaseSchema):
791
807
 
792
808
  def _maybe_raise_one_or_more(errors: list[Exception]) -> None:
793
809
  if not errors:
794
- return None
795
- elif len(errors) == 1:
810
+ return
811
+ if len(errors) == 1:
796
812
  raise errors[0]
797
- else:
798
- raise MultipleFailures("\n\n".join(str(error) for error in errors), errors)
813
+ raise MultipleFailures("\n\n".join(str(error) for error in errors), errors)
799
814
 
800
815
 
801
816
  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]:
@@ -69,6 +69,7 @@ class StatefulTestRunnerConfig:
69
69
  max_response_time: int | None = None
70
70
  dry_run: bool = False
71
71
  targets: list[Target] = field(default_factory=list)
72
+ unique_data: bool = False
72
73
 
73
74
  def __post_init__(self) -> None:
74
75
  import hypothesis
@@ -4,6 +4,7 @@ import traceback
4
4
  from dataclasses import dataclass, field
5
5
  from typing import TYPE_CHECKING, Tuple, Type, Union
6
6
 
7
+ from ..constants import NOT_SET
7
8
  from ..exceptions import CheckFailed
8
9
  from ..targets import TargetMetricCollector
9
10
  from . import events
@@ -11,6 +12,7 @@ from . import events
11
12
  if TYPE_CHECKING:
12
13
  from ..models import Case, Check
13
14
  from ..transports.responses import GenericResponse
15
+ from ..types import NotSet
14
16
 
15
17
  FailureKey = Union[Type[CheckFailed], Tuple[str, int]]
16
18
 
@@ -52,16 +54,17 @@ class RunnerContext:
52
54
  completed_scenarios: int = 0
53
55
  # Metrics collector for targeted testing
54
56
  metric_collector: TargetMetricCollector = field(default_factory=lambda: TargetMetricCollector(targets=[]))
57
+ step_outcomes: dict[int, BaseException | None] = field(default_factory=dict)
55
58
 
56
59
  @property
57
60
  def current_scenario_status(self) -> events.ScenarioStatus:
58
61
  if self.current_step_status == events.StepStatus.SUCCESS:
59
62
  return events.ScenarioStatus.SUCCESS
60
- elif self.current_step_status == events.StepStatus.FAILURE:
63
+ if self.current_step_status == events.StepStatus.FAILURE:
61
64
  return events.ScenarioStatus.FAILURE
62
- elif self.current_step_status == events.StepStatus.ERROR:
65
+ if self.current_step_status == events.StepStatus.ERROR:
63
66
  return events.ScenarioStatus.ERROR
64
- elif self.current_step_status == events.StepStatus.INTERRUPTED:
67
+ if self.current_step_status == events.StepStatus.INTERRUPTED:
65
68
  return events.ScenarioStatus.INTERRUPTED
66
69
  return events.ScenarioStatus.REJECTED
67
70
 
@@ -69,6 +72,7 @@ class RunnerContext:
69
72
  self.completed_scenarios += 1
70
73
  self.current_step_status = None
71
74
  self.current_response = None
75
+ self.step_outcomes.clear()
72
76
 
73
77
  def reset_step(self) -> None:
74
78
  self.checks_for_step = []
@@ -123,3 +127,9 @@ class RunnerContext:
123
127
  self.seen_in_suite.clear()
124
128
  self.reset_scenario()
125
129
  self.metric_collector.reset()
130
+
131
+ def store_step_outcome(self, case: Case, outcome: BaseException | None) -> None:
132
+ self.step_outcomes[hash(case)] = outcome
133
+
134
+ def get_step_outcome(self, case: Case) -> BaseException | None | NotSet:
135
+ return self.step_outcomes.get(hash(case), NOT_SET)
@@ -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,
@@ -162,17 +163,34 @@ def _execute_state_machine_loop(
162
163
  try:
163
164
  if config.dry_run:
164
165
  return None
166
+ if config.unique_data:
167
+ cached = ctx.get_step_outcome(case)
168
+ if isinstance(cached, BaseException):
169
+ raise cached
170
+ elif cached is None:
171
+ return None
165
172
  result = super().step(case, previous)
166
173
  ctx.step_succeeded()
167
- except CheckFailed:
174
+ except CheckFailed as exc:
175
+ if config.unique_data:
176
+ ctx.store_step_outcome(case, exc)
168
177
  ctx.step_failed()
169
178
  raise
170
- except Exception:
179
+ except Exception as exc:
180
+ if config.unique_data:
181
+ ctx.store_step_outcome(case, exc)
171
182
  ctx.step_errored()
172
183
  raise
173
184
  except KeyboardInterrupt:
174
185
  ctx.step_interrupted()
175
186
  raise
187
+ except BaseException as exc:
188
+ if config.unique_data:
189
+ ctx.store_step_outcome(case, exc)
190
+ raise exc
191
+ else:
192
+ if config.unique_data:
193
+ ctx.store_step_outcome(case, None)
176
194
  finally:
177
195
  transition_id: events.TransitionId | None
178
196
  if previous is not None:
@@ -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
@@ -12,11 +12,11 @@ from hypothesis.stateful import RuleBasedStateMachine
12
12
  from .._dependency_versions import HYPOTHESIS_HAS_STATEFUL_NAMING_IMPROVEMENTS
13
13
  from ..constants import NO_LINKS_ERROR_MESSAGE, NOT_SET
14
14
  from ..exceptions import UsageError
15
- from ..models import APIOperation, Case, CheckFunction
15
+ from ..internal.checks import CheckFunction
16
+ from ..models import APIOperation, Case
16
17
  from .config import _default_hypothesis_settings_factory
17
18
  from .runner import StatefulTestRunner, StatefulTestRunnerConfig
18
19
  from .sink import StateMachineSink
19
- from .statistic import TransitionStats
20
20
 
21
21
  if TYPE_CHECKING:
22
22
  import hypothesis
@@ -24,6 +24,7 @@ if TYPE_CHECKING:
24
24
 
25
25
  from ..schemas import BaseSchema
26
26
  from ..transports.responses import GenericResponse
27
+ from .statistic import TransitionStats
27
28
 
28
29
 
29
30
  @dataclass
@@ -64,7 +65,7 @@ class APIStateMachine(RuleBasedStateMachine):
64
65
 
65
66
  @classmethod
66
67
  @lru_cache
67
- def _to_test_case(cls) -> Type:
68
+ def _to_test_case(cls) -> type:
68
69
  from . import run_state_machine_as_test
69
70
 
70
71
  class StateMachineTestCase(RuleBasedStateMachine.TestCase):
@@ -97,7 +98,7 @@ class APIStateMachine(RuleBasedStateMachine):
97
98
 
98
99
  def _add_result_to_targets(self, targets: tuple[str, ...], result: StepResult | None) -> None:
99
100
  if result is None:
100
- return None
101
+ return
101
102
  target = self._get_target_for_result(result)
102
103
  if target is not None:
103
104
  super()._add_result_to_targets((target,), result)
@@ -310,7 +311,7 @@ def _print_case(case: Case, kwargs: dict[str, Any]) -> str:
310
311
  headers.update(kwargs.get("headers", {}))
311
312
  case.headers = headers
312
313
  data = [
313
- f"{name}={repr(getattr(case, name))}"
314
+ f"{name}={getattr(case, name)!r}"
314
315
  for name in ("path_parameters", "headers", "cookies", "query", "body", "media_type")
315
316
  if getattr(case, name) not in (None, NOT_SET)
316
317
  ]
@@ -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
@@ -1,14 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from ..exceptions import CheckFailed, get_grouped_exception
6
- from .context import RunnerContext
6
+ from ..internal.checks import CheckContext
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from ..failures import FailureContext
10
- from ..models import Case, CheckFunction
10
+ from ..internal.checks import CheckFunction
11
+ from ..models import Case
11
12
  from ..transports.responses import GenericResponse
13
+ from .context import RunnerContext
12
14
 
13
15
 
14
16
  def validate_response(
@@ -19,8 +21,11 @@ def validate_response(
19
21
  checks: tuple[CheckFunction, ...],
20
22
  additional_checks: tuple[CheckFunction, ...] = (),
21
23
  max_response_time: int | None = None,
24
+ headers: dict[str, Any] | None = None,
22
25
  ) -> None:
23
26
  """Validate the response against the provided checks."""
27
+ from requests.structures import CaseInsensitiveDict
28
+
24
29
  from .._compat import MultipleFailures
25
30
  from ..checks import _make_max_response_time_failure_message
26
31
  from ..failures import ResponseTimeExceeded
@@ -28,6 +33,7 @@ def validate_response(
28
33
 
29
34
  exceptions: list[CheckFailed | AssertionError] = []
30
35
  check_results = ctx.checks_for_step
36
+ check_ctx = CheckContext(headers=CaseInsensitiveDict(headers) if headers else None)
31
37
 
32
38
  def _on_failure(exc: CheckFailed | AssertionError, message: str, context: FailureContext | None) -> None:
33
39
  exceptions.append(exc)
@@ -62,8 +68,7 @@ def validate_response(
62
68
  name = check.__name__
63
69
  copied_case = case.partial_deepcopy()
64
70
  try:
65
- check(response, copied_case)
66
- skip_check = check(response, copied_case)
71
+ skip_check = check(check_ctx, response, copied_case)
67
72
  if not skip_check:
68
73
  _on_passed(name, copied_case)
69
74
  except CheckFailed as exc:
@@ -101,7 +101,7 @@ class RequestsTransport:
101
101
  cookies: dict[str, Any] | None = None,
102
102
  ) -> dict[str, Any]:
103
103
  final_headers = case._get_headers(headers)
104
- media_type: Optional[str]
104
+ media_type: str | None
105
105
  if case.body is not NOT_SET and case.media_type is None:
106
106
  media_type = case.operation._get_default_media_type()
107
107
  else:
@@ -274,7 +274,7 @@ class WSGITransport:
274
274
  cookies: dict[str, Any] | None = None,
275
275
  ) -> dict[str, Any]:
276
276
  final_headers = case._get_headers(headers)
277
- media_type: Optional[str]
277
+ media_type: str | None
278
278
  if case.body is not NOT_SET and case.media_type is None:
279
279
  media_type = case.operation._get_default_media_type()
280
280
  else:
@@ -0,0 +1,7 @@
1
+ from inspect import iscoroutinefunction
2
+
3
+
4
+ def is_asgi_app(app: object) -> bool:
5
+ return iscoroutinefunction(app) or (
6
+ hasattr(app, "__call__") and iscoroutinefunction(app.__call__) # noqa: B004
7
+ )
@@ -3,11 +3,12 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from ..constants import USER_AGENT
6
- from ..types import RawAuth
7
6
 
8
7
  if TYPE_CHECKING:
9
8
  from requests.auth import HTTPDigestAuth
10
9
 
10
+ from ..types import RawAuth
11
+
11
12
 
12
13
  def get_requests_auth(auth: RawAuth | None, auth_type: str | None) -> HTTPDigestAuth | RawAuth | None:
13
14
  from requests.auth import HTTPDigestAuth
@@ -31,7 +31,7 @@ def parse_header(line: str) -> Tuple[str, dict]:
31
31
  return key, pdict
32
32
 
33
33
 
34
- @lru_cache()
34
+ @lru_cache
35
35
  def parse_content_type(content_type: str) -> Tuple[str, str]:
36
36
  """Parse Content Type and return main type and subtype."""
37
37
  try:
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import sys
5
- from datetime import timedelta
6
5
  from typing import TYPE_CHECKING, Any, NoReturn, Union
7
6
 
8
7
  from werkzeug.wrappers import Response as BaseResponse
@@ -10,6 +9,8 @@ from werkzeug.wrappers import Response as BaseResponse
10
9
  from .._compat import JSONMixin
11
10
 
12
11
  if TYPE_CHECKING:
12
+ from datetime import timedelta
13
+
13
14
  from httpx import Response as httpxResponse
14
15
  from requests import PreparedRequest
15
16
  from requests import Response as requestsResponse
schemathesis/utils.py CHANGED
@@ -5,6 +5,7 @@ from contextlib import contextmanager
5
5
  from inspect import getfullargspec
6
6
  from pathlib import Path
7
7
  from typing import (
8
+ TYPE_CHECKING,
8
9
  Any,
9
10
  Callable,
10
11
  Generator,
@@ -20,9 +21,10 @@ from hypothesis.strategies import SearchStrategy
20
21
  from ._compat import InferType, get_signature
21
22
 
22
23
  # Backward-compat
23
- from .constants import NOT_SET # noqa: F401
24
24
  from .exceptions import SkipTest, UsageError
25
- from .types import GenericTest, PathLike
25
+
26
+ if TYPE_CHECKING:
27
+ from .types import GenericTest, PathLike
26
28
 
27
29
 
28
30
  def is_schemathesis_test(func: Callable) -> bool:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: schemathesis
3
- Version: 3.35.4
3
+ Version: 3.36.0
4
4
  Summary: Property-based testing framework for Open API and GraphQL based apps
5
5
  Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
6
6
  Project-URL: Changelog, https://schemathesis.readthedocs.io/en/stable/changelog.html