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
schemathesis/models.py CHANGED
@@ -9,7 +9,6 @@ from dataclasses import dataclass, field
9
9
  from enum import Enum
10
10
  from functools import lru_cache, partial
11
11
  from itertools import chain
12
- from logging import LogRecord
13
12
  from typing import (
14
13
  TYPE_CHECKING,
15
14
  Any,
@@ -18,7 +17,6 @@ from typing import (
18
17
  Generic,
19
18
  Iterator,
20
19
  NoReturn,
21
- Optional,
22
20
  Sequence,
23
21
  Type,
24
22
  TypeVar,
@@ -28,7 +26,6 @@ from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
28
26
 
29
27
  from . import serializers
30
28
  from ._dependency_versions import IS_WERKZEUG_ABOVE_3
31
- from .auths import AuthStorage
32
29
  from .code_samples import CodeSampleStyle
33
30
  from .constants import (
34
31
  NOT_SET,
@@ -38,7 +35,6 @@ from .constants import (
38
35
  )
39
36
  from .exceptions import (
40
37
  CheckFailed,
41
- FailureContext,
42
38
  OperationSchemaError,
43
39
  SerializationNotPossible,
44
40
  SkipTest,
@@ -49,24 +45,29 @@ from .exceptions import (
49
45
  )
50
46
  from .generation import DataGenerationMethod, GenerationConfig, generate_random_case_id
51
47
  from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, dispatch
48
+ from .internal.checks import CheckContext
52
49
  from .internal.copy import fast_deepcopy
53
50
  from .internal.deprecation import deprecated_function, deprecated_property
54
51
  from .internal.output import prepare_response_payload
55
52
  from .parameters import Parameter, ParameterSet, PayloadAlternatives
56
53
  from .sanitization import sanitize_request, sanitize_response
57
- from .serializers import Serializer
58
54
  from .transports import ASGITransport, RequestsTransport, WSGITransport, deserialize_payload, serialize_payload
59
55
  from .types import Body, Cookies, FormData, Headers, NotSet, PathParameters, Query
60
56
 
61
57
  if TYPE_CHECKING:
62
58
  import unittest
59
+ from logging import LogRecord
63
60
 
64
61
  import requests.auth
65
62
  import werkzeug
66
63
  from hypothesis import strategies as st
67
64
  from requests.structures import CaseInsensitiveDict
68
65
 
66
+ from .auths import AuthStorage
67
+ from .failures import FailureContext
68
+ from .internal.checks import CheckFunction
69
69
  from .schemas import BaseSchema
70
+ from .serializers import Serializer
70
71
  from .stateful import Stateful, StatefulTest
71
72
  from .transports.responses import GenericResponse, WSGIResponse
72
73
 
@@ -421,6 +422,7 @@ class Case:
421
422
  additional_checks: tuple[CheckFunction, ...] = (),
422
423
  excluded_checks: tuple[CheckFunction, ...] = (),
423
424
  code_sample_style: str | None = None,
425
+ headers: dict[str, Any] | None = None,
424
426
  ) -> None:
425
427
  """Validate application response.
426
428
 
@@ -434,17 +436,30 @@ class Case:
434
436
  :param code_sample_style: Controls the style of code samples for failure reproduction.
435
437
  """
436
438
  __tracebackhide__ = True
439
+ from requests.structures import CaseInsensitiveDict
440
+
437
441
  from .checks import ALL_CHECKS
442
+ from .internal.checks import wrap_check
438
443
  from .transports.responses import get_payload, get_reason
439
444
 
440
- checks = checks or ALL_CHECKS
445
+ if checks:
446
+ _checks = tuple(wrap_check(check) for check in checks)
447
+ else:
448
+ _checks = checks
449
+ if additional_checks:
450
+ _additional_checks = tuple(wrap_check(check) for check in additional_checks)
451
+ else:
452
+ _additional_checks = additional_checks
453
+
454
+ checks = _checks or ALL_CHECKS
441
455
  checks = tuple(check for check in checks if check not in excluded_checks)
442
- additional_checks = tuple(check for check in additional_checks if check not in excluded_checks)
456
+ additional_checks = tuple(check for check in _additional_checks if check not in excluded_checks)
443
457
  failed_checks = []
458
+ ctx = CheckContext(headers=CaseInsensitiveDict(headers) if headers else None)
444
459
  for check in chain(checks, additional_checks):
445
460
  copied_case = self.partial_deepcopy()
446
461
  try:
447
- check(response, copied_case)
462
+ check(ctx, response, copied_case)
448
463
  except AssertionError as exc:
449
464
  maybe_set_assertion_message(exc, check.__name__)
450
465
  failed_checks.append(exc)
@@ -514,7 +529,7 @@ class Case:
514
529
  ) -> requests.Response:
515
530
  __tracebackhide__ = True
516
531
  response = self.call(base_url, session, headers, **kwargs)
517
- self.validate_response(response, checks, code_sample_style=code_sample_style)
532
+ self.validate_response(response, checks, code_sample_style=code_sample_style, headers=headers)
518
533
  return response
519
534
 
520
535
  def _get_url(self, base_url: str | None) -> str:
@@ -995,7 +1010,7 @@ class Interaction:
995
1010
  """A single interaction with the target app."""
996
1011
 
997
1012
  request: Request
998
- response: Response
1013
+ response: Response | None
999
1014
  checks: list[Check]
1000
1015
  status: Status
1001
1016
  data_generation_method: DataGenerationMethod
@@ -1003,10 +1018,28 @@ class Interaction:
1003
1018
  recorded_at: str = field(default_factory=lambda: datetime.datetime.now(TIMEZONE).isoformat())
1004
1019
 
1005
1020
  @classmethod
1006
- def from_requests(cls, case: Case, response: requests.Response, status: Status, checks: list[Check]) -> Interaction:
1021
+ def from_requests(
1022
+ cls,
1023
+ case: Case,
1024
+ response: requests.Response | None,
1025
+ status: Status,
1026
+ checks: list[Check],
1027
+ headers: dict[str, Any] | None,
1028
+ session: requests.Session | None,
1029
+ ) -> Interaction:
1030
+ if response is not None:
1031
+ prepared = response.request
1032
+ request = Request.from_prepared_request(prepared)
1033
+ else:
1034
+ import requests
1035
+
1036
+ if session is None:
1037
+ session = requests.Session()
1038
+ session.headers.update(headers or {})
1039
+ request = Request.from_case(case, session)
1007
1040
  return cls(
1008
- request=Request.from_prepared_request(response.request),
1009
- response=Response.from_requests(response),
1041
+ request=request,
1042
+ response=Response.from_requests(response) if response is not None else None,
1010
1043
  status=status,
1011
1044
  checks=checks,
1012
1045
  data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
@@ -1017,9 +1050,9 @@ class Interaction:
1017
1050
  def from_wsgi(
1018
1051
  cls,
1019
1052
  case: Case,
1020
- response: WSGIResponse,
1053
+ response: WSGIResponse | None,
1021
1054
  headers: dict[str, Any],
1022
- elapsed: float,
1055
+ elapsed: float | None,
1023
1056
  status: Status,
1024
1057
  checks: list[Check],
1025
1058
  ) -> Interaction:
@@ -1029,7 +1062,7 @@ class Interaction:
1029
1062
  session.headers.update(headers)
1030
1063
  return cls(
1031
1064
  request=Request.from_case(case, session),
1032
- response=Response.from_wsgi(response, elapsed),
1065
+ response=Response.from_wsgi(response, elapsed) if response is not None and elapsed is not None else None,
1033
1066
  status=status,
1034
1067
  checks=checks,
1035
1068
  data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
@@ -1119,16 +1152,22 @@ class TestResult:
1119
1152
  self.errors.append(exception)
1120
1153
 
1121
1154
  def store_requests_response(
1122
- self, case: Case, response: requests.Response, status: Status, checks: list[Check]
1155
+ self,
1156
+ case: Case,
1157
+ response: requests.Response | None,
1158
+ status: Status,
1159
+ checks: list[Check],
1160
+ headers: dict[str, Any] | None,
1161
+ session: requests.Session | None,
1123
1162
  ) -> None:
1124
- self.interactions.append(Interaction.from_requests(case, response, status, checks))
1163
+ self.interactions.append(Interaction.from_requests(case, response, status, checks, headers, session))
1125
1164
 
1126
1165
  def store_wsgi_response(
1127
1166
  self,
1128
1167
  case: Case,
1129
- response: WSGIResponse,
1168
+ response: WSGIResponse | None,
1130
1169
  headers: dict[str, Any],
1131
- elapsed: float,
1170
+ elapsed: float | None,
1132
1171
  status: Status,
1133
1172
  checks: list[Check],
1134
1173
  ) -> None:
@@ -1209,6 +1248,3 @@ class TestResultSet:
1209
1248
  def add_warning(self, warning: str) -> None:
1210
1249
  """Add a new warning to the warnings list."""
1211
1250
  self.warnings.append(warning)
1212
-
1213
-
1214
- CheckFunction = Callable[["GenericResponse", Case], Optional[bool]]
@@ -4,7 +4,6 @@ from random import Random
4
4
  from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable
5
5
  from urllib.parse import urlparse
6
6
 
7
- from .._override import CaseOverride
8
7
  from ..constants import (
9
8
  DEFAULT_DEADLINE,
10
9
  DEFAULT_STATEFUL_RECURSION_LIMIT,
@@ -22,16 +21,17 @@ from ..targets import DEFAULT_TARGETS, Target
22
21
  from ..transports import RequestConfig
23
22
  from ..transports.auth import get_requests_auth
24
23
  from ..types import Filter, NotSet, RawAuth, RequestCert
24
+ from . import events
25
25
  from .probes import ProbeConfig
26
26
 
27
27
  if TYPE_CHECKING:
28
28
  import hypothesis
29
29
 
30
+ from .._override import CaseOverride
30
31
  from ..models import CheckFunction
31
32
  from ..schemas import BaseSchema
32
33
  from ..service.client import ServiceClient
33
34
  from ..stateful import Stateful
34
- from . import events
35
35
  from .impl import BaseRunner
36
36
 
37
37
 
@@ -347,6 +347,7 @@ def from_schema(
347
347
  exit_first: bool = False,
348
348
  max_failures: int | None = None,
349
349
  started_at: str | None = None,
350
+ unique_data: bool = False,
350
351
  dry_run: bool = False,
351
352
  store_interactions: bool = False,
352
353
  stateful: Stateful | None = None,
@@ -357,9 +358,9 @@ def from_schema(
357
358
  service_client: ServiceClient | None = None,
358
359
  ) -> BaseRunner:
359
360
  import hypothesis
360
- from starlette.applications import Starlette
361
361
 
362
362
  from ..checks import DEFAULT_CHECKS
363
+ from ..transports.asgi import is_asgi_app
363
364
  from .impl import (
364
365
  SingleThreadASGIRunner,
365
366
  SingleThreadRunner,
@@ -373,7 +374,6 @@ def from_schema(
373
374
  probe_config = probe_config or ProbeConfig()
374
375
 
375
376
  hypothesis_settings = hypothesis_settings or hypothesis.settings(deadline=DEFAULT_DEADLINE)
376
- generation_config = generation_config or GenerationConfig()
377
377
  request_config = RequestConfig(
378
378
  timeout=request_timeout,
379
379
  tls_verify=request_tls_verify,
@@ -405,6 +405,7 @@ def from_schema(
405
405
  exit_first=exit_first,
406
406
  max_failures=max_failures,
407
407
  started_at=started_at,
408
+ unique_data=unique_data,
408
409
  dry_run=dry_run,
409
410
  store_interactions=store_interactions,
410
411
  stateful=stateful,
@@ -414,7 +415,7 @@ def from_schema(
414
415
  probe_config=probe_config,
415
416
  service_client=service_client,
416
417
  )
417
- if isinstance(schema.app, Starlette):
418
+ if is_asgi_app(schema.app):
418
419
  return ThreadPoolASGIRunner(
419
420
  schema=schema,
420
421
  checks=checks,
@@ -430,6 +431,7 @@ def from_schema(
430
431
  exit_first=exit_first,
431
432
  max_failures=max_failures,
432
433
  started_at=started_at,
434
+ unique_data=unique_data,
433
435
  dry_run=dry_run,
434
436
  store_interactions=store_interactions,
435
437
  stateful=stateful,
@@ -455,6 +457,7 @@ def from_schema(
455
457
  exit_first=exit_first,
456
458
  max_failures=max_failures,
457
459
  started_at=started_at,
460
+ unique_data=unique_data,
458
461
  dry_run=dry_run,
459
462
  store_interactions=store_interactions,
460
463
  stateful=stateful,
@@ -481,6 +484,7 @@ def from_schema(
481
484
  exit_first=exit_first,
482
485
  max_failures=max_failures,
483
486
  started_at=started_at,
487
+ unique_data=unique_data,
484
488
  dry_run=dry_run,
485
489
  store_interactions=store_interactions,
486
490
  stateful=stateful,
@@ -490,7 +494,7 @@ def from_schema(
490
494
  probe_config=probe_config,
491
495
  service_client=service_client,
492
496
  )
493
- if isinstance(schema.app, Starlette):
497
+ if is_asgi_app(schema.app):
494
498
  return SingleThreadASGIRunner(
495
499
  schema=schema,
496
500
  checks=checks,
@@ -506,6 +510,7 @@ def from_schema(
506
510
  exit_first=exit_first,
507
511
  max_failures=max_failures,
508
512
  started_at=started_at,
513
+ unique_data=unique_data,
509
514
  dry_run=dry_run,
510
515
  store_interactions=store_interactions,
511
516
  stateful=stateful,
@@ -530,6 +535,7 @@ def from_schema(
530
535
  exit_first=exit_first,
531
536
  max_failures=max_failures,
532
537
  started_at=started_at,
538
+ unique_data=unique_data,
533
539
  dry_run=dry_run,
534
540
  store_interactions=store_interactions,
535
541
  stateful=stateful,
@@ -7,12 +7,12 @@ from dataclasses import asdict, dataclass, field
7
7
  from typing import TYPE_CHECKING, Any
8
8
 
9
9
  from ..exceptions import RuntimeErrorType, SchemaError, SchemaErrorType, format_exception
10
- from ..generation import DataGenerationMethod
11
10
  from ..internal.datetime import current_datetime
12
11
  from ..internal.result import Err, Ok, Result
13
12
  from .serialization import SerializedError, SerializedTestResult
14
13
 
15
14
  if TYPE_CHECKING:
15
+ from ..generation import DataGenerationMethod
16
16
  from ..models import APIOperation, Status, TestResult, TestResultSet
17
17
  from ..schemas import BaseSchema, Specification
18
18
  from ..service.models import AnalysisResult
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
5
+
6
+ from ...constants import NOT_SET
7
+ from ...models import TestResult, TestResultSet
8
+
9
+ if TYPE_CHECKING:
10
+ import threading
11
+
12
+ from ...exceptions import OperationSchemaError
13
+ from ...models import Case
14
+ from ...types import NotSet
15
+
16
+
17
+ @dataclass
18
+ class RunnerContext:
19
+ """Holds context shared for a test run."""
20
+
21
+ data: TestResultSet
22
+ seed: int | None
23
+ stop_event: threading.Event
24
+ unique_data: bool
25
+ outcome_cache: dict[int, BaseException | None]
26
+
27
+ __slots__ = ("data", "seed", "stop_event", "unique_data", "outcome_cache")
28
+
29
+ def __init__(self, *, seed: int | None, stop_event: threading.Event, unique_data: bool) -> None:
30
+ self.data = TestResultSet(seed=seed)
31
+ self.seed = seed
32
+ self.stop_event = stop_event
33
+ self.outcome_cache = {}
34
+ self.unique_data = unique_data
35
+
36
+ @property
37
+ def is_stopped(self) -> bool:
38
+ return self.stop_event.is_set()
39
+
40
+ @property
41
+ def has_all_not_found(self) -> bool:
42
+ """Check if all responses are 404."""
43
+ has_not_found = False
44
+ for entry in self.data.results:
45
+ for check in entry.checks:
46
+ if check.response is not None:
47
+ if check.response.status_code == 404:
48
+ has_not_found = True
49
+ else:
50
+ # There are non-404 responses, no reason to check any other response
51
+ return False
52
+ # Only happens if all responses are 404, or there are no responses at all.
53
+ # In the first case, it returns True, for the latter - False
54
+ return has_not_found
55
+
56
+ def add_result(self, result: TestResult) -> None:
57
+ self.data.append(result)
58
+
59
+ def add_generic_error(self, error: OperationSchemaError) -> None:
60
+ self.data.generic_errors.append(error)
61
+
62
+ def add_warning(self, message: str) -> None:
63
+ self.data.add_warning(message)
64
+
65
+ def cache_outcome(self, case: Case, outcome: BaseException | None) -> None:
66
+ self.outcome_cache[hash(case)] = outcome
67
+
68
+ def get_cached_outcome(self, case: Case) -> BaseException | None | NotSet:
69
+ return self.outcome_cache.get(hash(case), NOT_SET)
70
+
71
+
72
+ ALL_NOT_FOUND_WARNING_MESSAGE = "All API responses have a 404 status code. Did you specify the proper API location?"