schemathesis 3.35.5__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.
@@ -13,7 +13,6 @@ from hypothesis.errors import HypothesisWarning
13
13
 
14
14
  from ..._hypothesis import create_test
15
15
  from ...internal.result import Ok
16
- from ...models import CheckFunction
17
16
  from ...stateful import Feedback, Stateful
18
17
  from ...transports.auth import get_requests_auth
19
18
  from ...utils import capture_hypothesis_output
@@ -21,13 +20,13 @@ from .. import events
21
20
  from .core import BaseRunner, asgi_test, get_session, handle_schema_error, network_test, run_test, wsgi_test
22
21
 
23
22
  if TYPE_CHECKING:
24
- from .context import RunnerContext
25
23
  import hypothesis
26
24
 
27
25
  from ...generation import DataGenerationMethod, GenerationConfig
28
- from ...models import CheckFunction
26
+ from ...internal.checks import CheckFunction
29
27
  from ...targets import Target
30
28
  from ...types import RawAuth
29
+ from .context import RunnerContext
31
30
 
32
31
 
33
32
  def _run_task(
@@ -235,7 +234,7 @@ class ThreadPoolRunner(BaseRunner):
235
234
  # It would be better to have a separate producer thread and communicate via threading events.
236
235
  # Though it is a bit more complex, so the current solution is suboptimal in terms of resources utilization,
237
236
  # but good enough and easy enough to implement.
238
- tasks_generator = iter(self.schema.get_all_operations())
237
+ tasks_generator = iter(self.schema.get_all_operations(generation_config=self.generation_config))
239
238
  generator_done = threading.Event()
240
239
  tasks_queue: Queue = Queue()
241
240
  # Add at least `workers_num` tasks first, so all workers are busy
schemathesis/schemas.py CHANGED
@@ -241,7 +241,7 @@ class BaseSchema(Mapping):
241
241
  raise NotImplementedError
242
242
 
243
243
  def get_all_operations(
244
- self, hooks: HookDispatcher | None = None
244
+ self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
245
245
  ) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
246
246
  raise NotImplementedError
247
247
 
@@ -276,7 +276,7 @@ class BaseSchema(Mapping):
276
276
  _given_kwargs: dict[str, GivenInput] | None = None,
277
277
  ) -> Generator[Result[tuple[APIOperation, Callable], OperationSchemaError], None, None]:
278
278
  """Generate all operations and Hypothesis tests for them."""
279
- for result in self.get_all_operations(hooks=hooks):
279
+ for result in self.get_all_operations(hooks=hooks, generation_config=generation_config):
280
280
  if isinstance(result, Ok):
281
281
  operation = result.ok()
282
282
  _as_strategy_kwargs: dict[str, Any] | None
@@ -139,12 +139,12 @@ def from_url(
139
139
  interval=WAIT_FOR_SCHEMA_INTERVAL,
140
140
  )
141
141
  def _load_schema(_uri: str, **_kwargs: Any) -> requests.Response:
142
- _kwargs.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
143
- return requests.post(_uri, **kwargs)
142
+ return requests.post(_uri, **_kwargs)
144
143
 
145
144
  else:
146
145
  _load_schema = requests.post
147
146
 
147
+ kwargs.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
148
148
  response = load_schema_from_url(lambda: _load_schema(url, **kwargs))
149
149
  raw_schema = extract_schema_from_response(response)
150
150
  return from_dict(
@@ -27,7 +27,7 @@ from requests.structures import CaseInsensitiveDict
27
27
 
28
28
  from ... import auths
29
29
  from ...checks import not_a_server_error
30
- from ...constants import NOT_SET
30
+ from ...constants import NOT_SET, SCHEMATHESIS_TEST_CASE_HEADER
31
31
  from ...exceptions import OperationNotFound, OperationSchemaError
32
32
  from ...generation import DataGenerationMethod, GenerationConfig
33
33
  from ...hooks import (
@@ -38,7 +38,7 @@ from ...hooks import (
38
38
  should_skip_operation,
39
39
  )
40
40
  from ...internal.result import Ok, Result
41
- from ...models import APIOperation, Case, CheckFunction, OperationDefinition
41
+ from ...models import APIOperation, Case, OperationDefinition
42
42
  from ...schemas import APIOperationMap, BaseSchema
43
43
  from ...types import Body, Cookies, Headers, NotSet, PathParameters, Query
44
44
  from ..openapi.constants import LOCATION_TO_CONTAINER
@@ -49,6 +49,7 @@ if TYPE_CHECKING:
49
49
  from hypothesis.strategies import SearchStrategy
50
50
 
51
51
  from ...auths import AuthStorage
52
+ from ...internal.checks import CheckFunction
52
53
  from ...stateful import Stateful, StatefulTest
53
54
  from ...transports.responses import GenericResponse
54
55
 
@@ -61,6 +62,9 @@ class RootType(enum.Enum):
61
62
 
62
63
  @dataclass(repr=False)
63
64
  class GraphQLCase(Case):
65
+ def __hash__(self) -> int:
66
+ return hash(self.as_curl_command({SCHEMATHESIS_TEST_CASE_HEADER: "0"}))
67
+
64
68
  def _get_url(self, base_url: str | None) -> str:
65
69
  base_url = self._get_base_url(base_url)
66
70
  # Replace the path, in case if the user provided any path parameters via hooks
@@ -78,11 +82,12 @@ class GraphQLCase(Case):
78
82
  additional_checks: tuple[CheckFunction, ...] = (),
79
83
  excluded_checks: tuple[CheckFunction, ...] = (),
80
84
  code_sample_style: str | None = None,
85
+ headers: dict[str, Any] | None = None,
81
86
  ) -> None:
82
87
  checks = checks or (not_a_server_error,)
83
88
  checks += additional_checks
84
89
  checks = tuple(check for check in checks if check not in excluded_checks)
85
- return super().validate_response(response, checks, code_sample_style=code_sample_style)
90
+ return super().validate_response(response, checks, code_sample_style=code_sample_style, headers=headers)
86
91
 
87
92
 
88
93
  C = TypeVar("C", bound=Case)
@@ -186,8 +191,7 @@ class GraphQLSchema(BaseSchema):
186
191
  return 0
187
192
 
188
193
  def get_all_operations(
189
- self,
190
- hooks: HookDispatcher | None = None,
194
+ self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
191
195
  ) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
192
196
  schema = self.client_schema
193
197
  for root_type, operation_type in (
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
+ import enum
4
5
  from http.cookies import SimpleCookie
5
6
  from typing import TYPE_CHECKING, Any, Dict, Generator, NoReturn, cast
6
7
  from urllib.parse import parse_qs, urlparse
@@ -25,11 +26,12 @@ from .utils import expand_status_code
25
26
  if TYPE_CHECKING:
26
27
  from requests import PreparedRequest
27
28
 
29
+ from ...internal.checks import CheckContext
28
30
  from ...models import APIOperation, Case
29
31
  from ...transports.responses import GenericResponse
30
32
 
31
33
 
32
- def status_code_conformance(response: GenericResponse, case: Case) -> bool | None:
34
+ def status_code_conformance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
33
35
  from .schemas import BaseOpenAPISchema
34
36
 
35
37
  if not isinstance(case.operation.schema, BaseOpenAPISchema):
@@ -60,7 +62,7 @@ def _expand_responses(responses: dict[str | int, Any]) -> Generator[int, None, N
60
62
  yield from expand_status_code(code)
61
63
 
62
64
 
63
- def content_type_conformance(response: GenericResponse, case: Case) -> bool | None:
65
+ def content_type_conformance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
64
66
  from .schemas import BaseOpenAPISchema
65
67
 
66
68
  if not isinstance(case.operation.schema, BaseOpenAPISchema):
@@ -115,7 +117,7 @@ def _reraise_malformed_media_type(case: Case, exc: ValueError, location: str, ac
115
117
  ) from exc
116
118
 
117
119
 
118
- def response_headers_conformance(response: GenericResponse, case: Case) -> bool | None:
120
+ def response_headers_conformance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
119
121
  import jsonschema
120
122
 
121
123
  from .parameters import OpenAPI20Parameter, OpenAPI30Parameter
@@ -171,11 +173,11 @@ def response_headers_conformance(response: GenericResponse, case: Case) -> bool
171
173
  )
172
174
  except jsonschema.ValidationError as exc:
173
175
  exc_class = get_schema_validation_error(case.operation.verbose_name, exc)
174
- ctx = failures.ValidationErrorContext.from_exception(
176
+ error_ctx = failures.ValidationErrorContext.from_exception(
175
177
  exc, output_config=case.operation.schema.output_config
176
178
  )
177
179
  try:
178
- raise exc_class("Response header does not conform to the schema", context=ctx) from exc
180
+ raise exc_class("Response header does not conform to the schema", context=error_ctx) from exc
179
181
  except Exception as exc:
180
182
  errors.append(exc)
181
183
  return _maybe_raise_one_or_more(errors) # type: ignore[func-returns-value]
@@ -203,7 +205,7 @@ def _coerce_header_value(value: str, schema: dict[str, Any]) -> str | int | floa
203
205
  return value
204
206
 
205
207
 
206
- def response_schema_conformance(response: GenericResponse, case: Case) -> bool | None:
208
+ def response_schema_conformance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
207
209
  from .schemas import BaseOpenAPISchema
208
210
 
209
211
  if not isinstance(case.operation.schema, BaseOpenAPISchema):
@@ -211,7 +213,7 @@ def response_schema_conformance(response: GenericResponse, case: Case) -> bool |
211
213
  return case.operation.validate_response(response)
212
214
 
213
215
 
214
- def negative_data_rejection(response: GenericResponse, case: Case) -> bool | None:
216
+ def negative_data_rejection(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
215
217
  from .schemas import BaseOpenAPISchema
216
218
 
217
219
  if not isinstance(case.operation.schema, BaseOpenAPISchema):
@@ -258,7 +260,7 @@ def has_only_additional_properties_in_non_body_parameters(case: Case) -> bool:
258
260
  return True
259
261
 
260
262
 
261
- def use_after_free(response: GenericResponse, original: Case) -> bool | None:
263
+ def use_after_free(ctx: CheckContext, response: GenericResponse, original: Case) -> bool | None:
262
264
  from ...transports.responses import get_reason
263
265
  from .schemas import BaseOpenAPISchema
264
266
 
@@ -298,7 +300,7 @@ def use_after_free(response: GenericResponse, original: Case) -> bool | None:
298
300
  return None
299
301
 
300
302
 
301
- def ensure_resource_availability(response: GenericResponse, original: Case) -> bool | None:
303
+ def ensure_resource_availability(ctx: CheckContext, response: GenericResponse, original: Case) -> bool | None:
302
304
  from ...transports.responses import get_reason
303
305
  from .schemas import BaseOpenAPISchema
304
306
 
@@ -332,7 +334,12 @@ def ensure_resource_availability(response: GenericResponse, original: Case) -> b
332
334
  return None
333
335
 
334
336
 
335
- def ignored_auth(response: GenericResponse, case: Case) -> bool | None:
337
+ class AuthKind(enum.Enum):
338
+ EXPLICIT = "explicit"
339
+ GENERATED = "generated"
340
+
341
+
342
+ def ignored_auth(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
336
343
  """Check if an operation declares authentication as a requirement but does not actually enforce it."""
337
344
  from .schemas import BaseOpenAPISchema
338
345
 
@@ -340,32 +347,49 @@ def ignored_auth(response: GenericResponse, case: Case) -> bool | None:
340
347
  return True
341
348
  security_parameters = _get_security_parameters(case.operation)
342
349
  # Authentication is required for this API operation and response is successful
343
- # Will it still be successful if there is no auth?
344
350
  if security_parameters and 200 <= response.status_code < 300:
345
- if _contains_auth(response.request, security_parameters):
346
- # If there is auth in the request, then drop it and retry the call
351
+ auth = _contains_auth(ctx, response.request, security_parameters)
352
+ if auth == AuthKind.EXPLICIT:
353
+ # Auth is explicitly set, it is expected to be valid
354
+ # Check if invalid auth will give an error
347
355
  _remove_auth_from_case(case, security_parameters)
348
356
  new_response = case.operation.schema.transport.send(case)
349
357
  if 200 <= new_response.status_code < 300:
350
- # Mutate the response object in place on the best effort basis
351
- if hasattr(response, "__attrs__"):
352
- for attribute in new_response.__attrs__:
353
- setattr(response, attribute, getattr(new_response, attribute))
354
- else:
355
- response.__dict__.update(new_response.__dict__)
356
- _raise_auth_error(new_response, case.operation.verbose_name)
358
+ _update_response(response, new_response)
359
+ _raise_no_auth_error(new_response, case.operation.verbose_name, "that requires authentication")
360
+ # Try to set invalid auth and check if it succeeds
361
+ for parameter in security_parameters:
362
+ _set_auth_for_case(case, parameter)
363
+ new_response = case.operation.schema.transport.send(case)
364
+ if 200 <= new_response.status_code < 300:
365
+ _update_response(response, new_response)
366
+ _raise_no_auth_error(new_response, case.operation.verbose_name, "with any auth")
367
+ _remove_auth_from_case(case, security_parameters)
368
+ elif auth == AuthKind.GENERATED:
369
+ # If this auth is generated which means it is likely invalid, then
370
+ # this request should have been an error
371
+ _raise_no_auth_error(response, case.operation.verbose_name, "with invalid auth")
357
372
  else:
358
373
  # Successful response when there is no auth
359
- _raise_auth_error(response, case.operation.verbose_name)
374
+ _raise_no_auth_error(response, case.operation.verbose_name, "that requires authentication")
360
375
  return None
361
376
 
362
377
 
363
- def _raise_auth_error(response: GenericResponse, operation: str) -> NoReturn:
378
+ def _update_response(old: GenericResponse, new: GenericResponse) -> None:
379
+ # Mutate the response object in place on the best effort basis
380
+ if hasattr(old, "__attrs__"):
381
+ for attribute in new.__attrs__:
382
+ setattr(old, attribute, getattr(new, attribute))
383
+ else:
384
+ old.__dict__.update(new.__dict__)
385
+
386
+
387
+ def _raise_no_auth_error(response: GenericResponse, operation: str, suffix: str) -> NoReturn:
364
388
  from ...transports.responses import get_reason
365
389
 
366
390
  exc_class = get_ignored_auth_error(operation)
367
391
  reason = get_reason(response.status_code)
368
- message = f"The API returned `{response.status_code} {reason}` for `{operation}` that requires authentication."
392
+ message = f"The API returned `{response.status_code} {reason}` for `{operation}` {suffix}."
369
393
  raise exc_class(
370
394
  failures.IgnoredAuth.title,
371
395
  context=failures.IgnoredAuth(message=message),
@@ -387,7 +411,9 @@ def _get_security_parameters(operation: APIOperation) -> list[SecurityParameter]
387
411
  ]
388
412
 
389
413
 
390
- def _contains_auth(request: PreparedRequest, security_parameters: list[SecurityParameter]) -> bool:
414
+ def _contains_auth(
415
+ ctx: CheckContext, request: PreparedRequest, security_parameters: list[SecurityParameter]
416
+ ) -> AuthKind | None:
391
417
  """Whether a request has authentication declared in the schema."""
392
418
  from requests.cookies import RequestsCookieJar
393
419
 
@@ -410,10 +436,20 @@ def _contains_auth(request: PreparedRequest, security_parameters: list[SecurityP
410
436
  return p["in"] == "cookie" and (p["name"] in cookies or p["name"] in header_cookies)
411
437
 
412
438
  for parameter in security_parameters:
413
- if has_header(parameter) or has_query(parameter) or has_cookie(parameter):
414
- return True
439
+ if has_header(parameter):
440
+ if ctx.headers is not None and parameter["name"] in ctx.headers:
441
+ return AuthKind.EXPLICIT
442
+ return AuthKind.GENERATED
443
+ if has_cookie(parameter):
444
+ if ctx.headers is not None and "Cookie" in ctx.headers:
445
+ cookies = cast(RequestsCookieJar, ctx.headers["Cookie"]) # type: ignore
446
+ if parameter["name"] in cookies:
447
+ return AuthKind.EXPLICIT
448
+ return AuthKind.GENERATED
449
+ if has_query(parameter):
450
+ return AuthKind.GENERATED
415
451
 
416
- return False
452
+ return None
417
453
 
418
454
 
419
455
  def _remove_auth_from_case(case: Case, security_parameters: list[SecurityParameter]) -> None:
@@ -431,6 +467,19 @@ def _remove_auth_from_case(case: Case, security_parameters: list[SecurityParamet
431
467
  case.cookies.pop(name, None)
432
468
 
433
469
 
470
+ def _set_auth_for_case(case: Case, parameter: SecurityParameter) -> None:
471
+ name = parameter["name"]
472
+ for location, attr_name in (
473
+ ("header", "headers"),
474
+ ("query", "query"),
475
+ ("cookie", "cookies"),
476
+ ):
477
+ if parameter["in"] == location:
478
+ container = getattr(case, attr_name, {})
479
+ container[name] = "SCHEMATHESIS-INVALID-VALUE"
480
+ setattr(case, attr_name, container)
481
+
482
+
434
483
  @dataclass
435
484
  class ResourcePath:
436
485
  """A path to a resource with variables."""
@@ -163,12 +163,12 @@ 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)
167
- return requests.get(_uri, **kwargs)
166
+ return requests.get(_uri, **_kwargs)
168
167
 
169
168
  else:
170
169
  _load_schema = requests.get
171
170
 
171
+ kwargs.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
172
172
  response = load_schema_from_url(lambda: _load_schema(uri, **kwargs))
173
173
  return from_file(
174
174
  response.text,
@@ -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
@@ -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,6 +54,7 @@ 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:
@@ -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)
@@ -163,17 +163,34 @@ def _execute_state_machine_loop(
163
163
  try:
164
164
  if config.dry_run:
165
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
166
172
  result = super().step(case, previous)
167
173
  ctx.step_succeeded()
168
- except CheckFailed:
174
+ except CheckFailed as exc:
175
+ if config.unique_data:
176
+ ctx.store_step_outcome(case, exc)
169
177
  ctx.step_failed()
170
178
  raise
171
- except Exception:
179
+ except Exception as exc:
180
+ if config.unique_data:
181
+ ctx.store_step_outcome(case, exc)
172
182
  ctx.step_errored()
173
183
  raise
174
184
  except KeyboardInterrupt:
175
185
  ctx.step_interrupted()
176
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)
177
194
  finally:
178
195
  transition_id: events.TransitionId | None
179
196
  if previous is not None:
@@ -12,7 +12,8 @@ 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
@@ -1,12 +1,14 @@
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 ..internal.checks import CheckContext
6
7
 
7
8
  if TYPE_CHECKING:
8
9
  from ..failures import FailureContext
9
- from ..models import Case, CheckFunction
10
+ from ..internal.checks import CheckFunction
11
+ from ..models import Case
10
12
  from ..transports.responses import GenericResponse
11
13
  from .context import RunnerContext
12
14
 
@@ -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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: schemathesis
3
- Version: 3.35.5
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