schemathesis 4.0.0a10__py3-none-any.whl → 4.0.0a12__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 (111) hide show
  1. schemathesis/__init__.py +29 -30
  2. schemathesis/auths.py +65 -24
  3. schemathesis/checks.py +73 -39
  4. schemathesis/cli/commands/__init__.py +51 -3
  5. schemathesis/cli/commands/data.py +10 -0
  6. schemathesis/cli/commands/run/__init__.py +163 -274
  7. schemathesis/cli/commands/run/context.py +8 -4
  8. schemathesis/cli/commands/run/events.py +11 -1
  9. schemathesis/cli/commands/run/executor.py +70 -78
  10. schemathesis/cli/commands/run/filters.py +15 -165
  11. schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
  12. schemathesis/cli/commands/run/handlers/junitxml.py +5 -4
  13. schemathesis/cli/commands/run/handlers/output.py +195 -121
  14. schemathesis/cli/commands/run/loaders.py +35 -50
  15. schemathesis/cli/commands/run/validation.py +52 -162
  16. schemathesis/cli/core.py +5 -3
  17. schemathesis/cli/ext/fs.py +7 -5
  18. schemathesis/cli/ext/options.py +0 -21
  19. schemathesis/config/__init__.py +189 -0
  20. schemathesis/config/_auth.py +51 -0
  21. schemathesis/config/_checks.py +268 -0
  22. schemathesis/config/_diff_base.py +99 -0
  23. schemathesis/config/_env.py +21 -0
  24. schemathesis/config/_error.py +156 -0
  25. schemathesis/config/_generation.py +149 -0
  26. schemathesis/config/_health_check.py +24 -0
  27. schemathesis/config/_operations.py +327 -0
  28. schemathesis/config/_output.py +171 -0
  29. schemathesis/config/_parameters.py +19 -0
  30. schemathesis/config/_phases.py +187 -0
  31. schemathesis/config/_projects.py +523 -0
  32. schemathesis/config/_rate_limit.py +17 -0
  33. schemathesis/config/_report.py +120 -0
  34. schemathesis/config/_validator.py +9 -0
  35. schemathesis/config/_warnings.py +25 -0
  36. schemathesis/config/schema.json +885 -0
  37. schemathesis/core/__init__.py +2 -0
  38. schemathesis/core/compat.py +16 -9
  39. schemathesis/core/errors.py +24 -4
  40. schemathesis/core/failures.py +6 -7
  41. schemathesis/core/hooks.py +20 -0
  42. schemathesis/core/output/__init__.py +14 -37
  43. schemathesis/core/output/sanitization.py +3 -146
  44. schemathesis/core/transport.py +36 -1
  45. schemathesis/core/validation.py +16 -0
  46. schemathesis/engine/__init__.py +2 -4
  47. schemathesis/engine/context.py +42 -43
  48. schemathesis/engine/core.py +7 -5
  49. schemathesis/engine/errors.py +60 -1
  50. schemathesis/engine/events.py +10 -2
  51. schemathesis/engine/phases/__init__.py +10 -0
  52. schemathesis/engine/phases/probes.py +11 -8
  53. schemathesis/engine/phases/stateful/__init__.py +2 -1
  54. schemathesis/engine/phases/stateful/_executor.py +104 -46
  55. schemathesis/engine/phases/stateful/context.py +2 -2
  56. schemathesis/engine/phases/unit/__init__.py +23 -15
  57. schemathesis/engine/phases/unit/_executor.py +110 -21
  58. schemathesis/engine/phases/unit/_pool.py +1 -1
  59. schemathesis/errors.py +2 -0
  60. schemathesis/filters.py +2 -3
  61. schemathesis/generation/__init__.py +5 -33
  62. schemathesis/generation/case.py +6 -3
  63. schemathesis/generation/coverage.py +154 -124
  64. schemathesis/generation/hypothesis/builder.py +70 -20
  65. schemathesis/generation/meta.py +3 -3
  66. schemathesis/generation/metrics.py +93 -0
  67. schemathesis/generation/modes.py +0 -8
  68. schemathesis/generation/overrides.py +37 -1
  69. schemathesis/generation/stateful/__init__.py +4 -0
  70. schemathesis/generation/stateful/state_machine.py +9 -1
  71. schemathesis/graphql/loaders.py +159 -16
  72. schemathesis/hooks.py +62 -35
  73. schemathesis/openapi/checks.py +12 -8
  74. schemathesis/openapi/generation/filters.py +10 -8
  75. schemathesis/openapi/loaders.py +142 -17
  76. schemathesis/pytest/lazy.py +2 -5
  77. schemathesis/pytest/loaders.py +24 -0
  78. schemathesis/pytest/plugin.py +33 -2
  79. schemathesis/schemas.py +21 -66
  80. schemathesis/specs/graphql/scalars.py +37 -3
  81. schemathesis/specs/graphql/schemas.py +23 -18
  82. schemathesis/specs/openapi/_hypothesis.py +26 -28
  83. schemathesis/specs/openapi/checks.py +37 -36
  84. schemathesis/specs/openapi/examples.py +4 -3
  85. schemathesis/specs/openapi/formats.py +32 -5
  86. schemathesis/specs/openapi/media_types.py +44 -1
  87. schemathesis/specs/openapi/negative/__init__.py +2 -2
  88. schemathesis/specs/openapi/patterns.py +46 -16
  89. schemathesis/specs/openapi/references.py +2 -3
  90. schemathesis/specs/openapi/schemas.py +19 -22
  91. schemathesis/specs/openapi/stateful/__init__.py +12 -6
  92. schemathesis/transport/__init__.py +54 -16
  93. schemathesis/transport/prepare.py +38 -13
  94. schemathesis/transport/requests.py +12 -9
  95. schemathesis/transport/wsgi.py +11 -12
  96. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/METADATA +50 -97
  97. schemathesis-4.0.0a12.dist-info/RECORD +164 -0
  98. schemathesis/cli/commands/run/checks.py +0 -79
  99. schemathesis/cli/commands/run/hypothesis.py +0 -78
  100. schemathesis/cli/commands/run/reports.py +0 -72
  101. schemathesis/cli/hooks.py +0 -36
  102. schemathesis/contrib/__init__.py +0 -9
  103. schemathesis/contrib/openapi/__init__.py +0 -9
  104. schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
  105. schemathesis/engine/config.py +0 -59
  106. schemathesis/experimental/__init__.py +0 -72
  107. schemathesis/generation/targets.py +0 -69
  108. schemathesis-4.0.0a10.dist-info/RECORD +0 -153
  109. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/WHEEL +0 -0
  110. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/entry_points.txt +0 -0
  111. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py CHANGED
@@ -1,48 +1,47 @@
1
1
  from __future__ import annotations
2
2
 
3
- from schemathesis import auths, contrib, engine, errors, experimental, graphql, hooks, openapi, pytest, python
3
+ from schemathesis import errors, graphql, openapi, pytest
4
+ from schemathesis.auths import AuthContext, auth
4
5
  from schemathesis.checks import CheckContext, CheckFunction, check
5
- from schemathesis.core.output import OutputConfig, sanitization
6
6
  from schemathesis.core.transport import Response
7
7
  from schemathesis.core.version import SCHEMATHESIS_VERSION
8
- from schemathesis.generation import GenerationConfig, GenerationMode, HeaderConfig
8
+ from schemathesis.generation import GenerationMode
9
9
  from schemathesis.generation.case import Case
10
- from schemathesis.generation.targets import TargetContext, TargetFunction, target
11
- from schemathesis.hooks import HookContext
10
+ from schemathesis.generation.metrics import MetricContext, MetricFunction, metric
11
+ from schemathesis.hooks import HookContext, hook
12
12
  from schemathesis.schemas import BaseSchema
13
+ from schemathesis.transport import SerializationContext, serializer
13
14
 
14
15
  __version__ = SCHEMATHESIS_VERSION
15
16
 
16
- # Public API
17
- auth = auths.GLOBAL_AUTH_STORAGE
18
- hook = hooks.register
19
-
20
17
  __all__ = [
18
+ "__version__",
19
+ # Core data structures
21
20
  "Case",
22
- "CheckContext",
23
- "CheckFunction",
24
- "GenerationMode",
25
- "GenerationConfig",
26
- "HeaderConfig",
27
- "OutputConfig",
28
21
  "Response",
29
- "TargetContext",
30
- "TargetFunction",
31
- "HookContext",
22
+ "GenerationMode",
32
23
  "BaseSchema",
33
- "__version__",
34
- "auth",
35
- "check",
36
- "contrib",
37
- "engine",
24
+ # Public errors
38
25
  "errors",
39
- "experimental",
40
- "graphql",
41
- "hook",
42
- "hooks",
26
+ # Spec or usage specific namespaces
43
27
  "openapi",
28
+ "graphql",
44
29
  "pytest",
45
- "python",
46
- "sanitization",
47
- "target",
30
+ # Hooks
31
+ "hook",
32
+ "HookContext",
33
+ # Checks
34
+ "check",
35
+ "CheckContext",
36
+ "CheckFunction",
37
+ # Auth
38
+ "auth",
39
+ "AuthContext",
40
+ # Targeted Property-based Testing
41
+ "metric",
42
+ "MetricContext",
43
+ "MetricFunction",
44
+ # Serialization
45
+ "serializer",
46
+ "SerializationContext",
48
47
  ]
schemathesis/auths.py CHANGED
@@ -34,14 +34,34 @@ Auth = TypeVar("Auth")
34
34
 
35
35
  @dataclass
36
36
  class AuthContext:
37
- """Holds state relevant for the authentication process.
37
+ """Runtime context passed to authentication providers during token generation.
38
+
39
+ Provides access to the current API operation and application instance when
40
+ auth providers need operation-specific tokens or application state.
41
+
42
+ Example:
43
+ ```python
44
+ @schemathesis.auth()
45
+ class ContextAwareAuth:
46
+ def get(self, case, context):
47
+ # Access operation details
48
+ if "/admin/" in context.operation.path:
49
+ return self.get_admin_token()
50
+ else:
51
+ return self.get_user_token()
52
+
53
+ def set(self, case, data, context):
54
+ case.headers = {"Authorization": f"Bearer {data}"}
55
+ ```
38
56
 
39
- :ivar APIOperation operation: API operation that is currently being processed.
40
- :ivar app: Optional Python application if the WSGI / ASGI integration is used.
41
57
  """
42
58
 
43
59
  operation: APIOperation
60
+ """API operation currently being processed for authentication."""
44
61
  app: Any | None
62
+ """Python application instance (ASGI/WSGI app) when using app integration, `None` otherwise."""
63
+
64
+ __slots__ = ("operation", "app")
45
65
 
46
66
 
47
67
  CacheKeyFunction = Callable[["Case", "AuthContext"], Union[str, int]]
@@ -289,7 +309,7 @@ class AuthStorage(Generic[Auth]):
289
309
  ) -> FilterableRegisterAuth | FilterableApplyAuth:
290
310
  if provider_class is not None:
291
311
  return self.apply(provider_class, refresh_interval=refresh_interval, cache_by_key=cache_by_key)
292
- return self.register(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
312
+ return self.auth(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
293
313
 
294
314
  def set_from_requests(self, auth: requests.auth.AuthBase) -> FilterableRequestsAuth:
295
315
  """Use `requests` auth instance as an auth provider."""
@@ -333,30 +353,12 @@ class AuthStorage(Generic[Auth]):
333
353
  provider = SelectiveAuthProvider(provider, filter_set)
334
354
  self.providers.append(provider)
335
355
 
336
- def register(
356
+ def auth(
337
357
  self,
338
358
  *,
339
359
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
340
360
  cache_by_key: CacheKeyFunction | None = None,
341
361
  ) -> FilterableRegisterAuth:
342
- """Register a new auth provider.
343
-
344
- .. code-block:: python
345
-
346
- @schemathesis.auth()
347
- class TokenAuth:
348
- def get(self, context):
349
- response = requests.post(
350
- "https://example.schemathesis.io/api/token/",
351
- json={"username": "demo", "password": "test"},
352
- )
353
- data = response.json()
354
- return data["access_token"]
355
-
356
- def set(self, case, data, context):
357
- # Modify `case` the way you need
358
- case.headers = {"Authorization": f"Bearer {data}"}
359
- """
360
362
  filter_set = FilterSet()
361
363
 
362
364
  def wrapper(provider_class: type[AuthProvider]) -> type[AuthProvider]:
@@ -451,5 +453,44 @@ def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | No
451
453
 
452
454
  # Global auth API
453
455
  GLOBAL_AUTH_STORAGE: AuthStorage = AuthStorage()
454
- register = GLOBAL_AUTH_STORAGE.register
455
456
  unregister = GLOBAL_AUTH_STORAGE.unregister
457
+
458
+
459
+ def auth(
460
+ *,
461
+ refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
462
+ cache_by_key: CacheKeyFunction | None = None,
463
+ ) -> FilterableRegisterAuth:
464
+ """Register a dynamic authentication provider for APIs with expiring tokens.
465
+
466
+ Args:
467
+ refresh_interval: Seconds between token refreshes. Default is `300`. Use `None` to disable caching
468
+ cache_by_key: Function to generate cache keys for different auth contexts (e.g., OAuth scopes)
469
+
470
+ Example:
471
+ ```python
472
+ import schemathesis
473
+ import requests
474
+
475
+ @schemathesis.auth()
476
+ class TokenAuth:
477
+ def get(self, case, context):
478
+ \"\"\"Fetch fresh authentication token\"\"\"
479
+ response = requests.post(
480
+ "http://localhost:8000/auth/token",
481
+ json={"username": "demo", "password": "test"}
482
+ )
483
+ return response.json()["access_token"]
484
+
485
+ def set(self, case, data, context):
486
+ \"\"\"Apply token to test case headers\"\"\"
487
+ case.headers = case.headers or {}
488
+ case.headers["Authorization"] = f"Bearer {data}"
489
+ ```
490
+
491
+ """
492
+ return GLOBAL_AUTH_STORAGE.auth(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
493
+
494
+
495
+ auth.__dict__ = GLOBAL_AUTH_STORAGE.auth.__dict__
496
+ auth.set_from_requests = GLOBAL_AUTH_STORAGE.set_from_requests # type: ignore[attr-defined]
schemathesis/checks.py CHANGED
@@ -3,12 +3,12 @@ from __future__ import annotations
3
3
  import json
4
4
  from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, Optional
5
5
 
6
+ from schemathesis.config import ChecksConfig
6
7
  from schemathesis.core.failures import (
7
8
  CustomFailure,
8
9
  Failure,
9
10
  FailureGroup,
10
11
  MalformedJson,
11
- MaxResponseTimeConfig,
12
12
  ResponseTimeExceeded,
13
13
  ServerError,
14
14
  )
@@ -23,23 +23,24 @@ if TYPE_CHECKING:
23
23
  from schemathesis.generation.case import Case
24
24
 
25
25
  CheckFunction = Callable[["CheckContext", "Response", "Case"], Optional[bool]]
26
- ChecksConfig = dict[CheckFunction, Any]
27
26
 
28
27
 
29
28
  class CheckContext:
30
- """Context for Schemathesis checks.
29
+ """Runtime context passed to validation check functions during API testing.
31
30
 
32
- Provides access to broader test execution data beyond individual test cases.
31
+ Provides access to configuration for currently checked endpoint.
33
32
  """
34
33
 
35
- override: Override | None
36
- auth: tuple[str, str] | None
37
- headers: CaseInsensitiveDict | None
34
+ _override: Override | None
35
+ _auth: tuple[str, str] | None
36
+ _headers: CaseInsensitiveDict | None
38
37
  config: ChecksConfig
39
- transport_kwargs: dict[str, Any] | None
40
- recorder: ScenarioRecorder | None
38
+ """Configuration settings for validation checks."""
39
+ _transport_kwargs: dict[str, Any] | None
40
+ _recorder: ScenarioRecorder | None
41
+ _checks: list[CheckFunction]
41
42
 
42
- __slots__ = ("override", "auth", "headers", "config", "transport_kwargs", "recorder")
43
+ __slots__ = ("_override", "_auth", "_headers", "config", "_transport_kwargs", "_recorder", "_checks")
43
44
 
44
45
  def __init__(
45
46
  self,
@@ -50,48 +51,78 @@ class CheckContext:
50
51
  transport_kwargs: dict[str, Any] | None,
51
52
  recorder: ScenarioRecorder | None = None,
52
53
  ) -> None:
53
- self.override = override
54
- self.auth = auth
55
- self.headers = headers
54
+ self._override = override
55
+ self._auth = auth
56
+ self._headers = headers
56
57
  self.config = config
57
- self.transport_kwargs = transport_kwargs
58
- self.recorder = recorder
59
-
60
- def find_parent(self, *, case_id: str) -> Case | None:
61
- if self.recorder is not None:
62
- return self.recorder.find_parent(case_id=case_id)
58
+ self._transport_kwargs = transport_kwargs
59
+ self._recorder = recorder
60
+ self._checks = []
61
+ for check in CHECKS.get_all():
62
+ name = check.__name__
63
+ if self.config.get_by_name(name=name).enabled:
64
+ self._checks.append(check)
65
+ if self.config.max_response_time.enabled:
66
+ self._checks.append(max_response_time)
67
+
68
+ def _find_parent(self, *, case_id: str) -> Case | None:
69
+ if self._recorder is not None:
70
+ return self._recorder.find_parent(case_id=case_id)
63
71
  return None
64
72
 
65
- def find_related(self, *, case_id: str) -> Iterator[Case]:
66
- if self.recorder is not None:
67
- yield from self.recorder.find_related(case_id=case_id)
73
+ def _find_related(self, *, case_id: str) -> Iterator[Case]:
74
+ if self._recorder is not None:
75
+ yield from self._recorder.find_related(case_id=case_id)
68
76
 
69
- def find_response(self, *, case_id: str) -> Response | None:
70
- if self.recorder is not None:
71
- return self.recorder.find_response(case_id=case_id)
77
+ def _find_response(self, *, case_id: str) -> Response | None:
78
+ if self._recorder is not None:
79
+ return self._recorder.find_response(case_id=case_id)
72
80
  return None
73
81
 
74
- def record_case(self, *, parent_id: str, case: Case) -> None:
75
- if self.recorder is not None:
76
- self.recorder.record_case(parent_id=parent_id, transition=None, case=case)
82
+ def _record_case(self, *, parent_id: str, case: Case) -> None:
83
+ if self._recorder is not None:
84
+ self._recorder.record_case(parent_id=parent_id, transition=None, case=case)
77
85
 
78
- def record_response(self, *, case_id: str, response: Response) -> None:
79
- if self.recorder is not None:
80
- self.recorder.record_response(case_id=case_id, response=response)
86
+ def _record_response(self, *, case_id: str, response: Response) -> None:
87
+ if self._recorder is not None:
88
+ self._recorder.record_response(case_id=case_id, response=response)
81
89
 
82
90
 
83
91
  CHECKS = Registry[CheckFunction]()
84
- check = CHECKS.register
92
+
93
+
94
+ def check(func: CheckFunction) -> CheckFunction:
95
+ """Register a custom validation check to run against API responses.
96
+
97
+ Args:
98
+ func: Function that takes `(ctx: CheckContext, response: Response, case: Case)` and raises `AssertionError` on validation failure
99
+
100
+ Example:
101
+ ```python
102
+ import schemathesis
103
+
104
+ @schemathesis.check
105
+ def check_cors_headers(ctx, response, case):
106
+ \"\"\"Verify CORS headers are present\"\"\"
107
+ if "Access-Control-Allow-Origin" not in response.headers:
108
+ raise AssertionError("Missing CORS headers")
109
+ ```
110
+
111
+ """
112
+ return CHECKS.register(func)
85
113
 
86
114
 
87
115
  @check
88
116
  def not_a_server_error(ctx: CheckContext, response: Response, case: Case) -> bool | None:
89
117
  """A check to verify that the response is not a server-side error."""
90
- from .specs.graphql.schemas import GraphQLSchema
91
- from .specs.graphql.validation import validate_graphql_response
118
+ from schemathesis.specs.graphql.schemas import GraphQLSchema
119
+ from schemathesis.specs.graphql.validation import validate_graphql_response
120
+ from schemathesis.specs.openapi.utils import expand_status_codes
121
+
122
+ expected_statuses = expand_status_codes(ctx.config.not_a_server_error.expected_statuses or [])
92
123
 
93
124
  status_code = response.status_code
94
- if status_code >= 500:
125
+ if status_code not in expected_statuses:
95
126
  raise ServerError(operation=case.operation.label, status_code=status_code)
96
127
  if isinstance(case.operation.schema, GraphQLSchema):
97
128
  try:
@@ -102,15 +133,18 @@ def not_a_server_error(ctx: CheckContext, response: Response, case: Case) -> boo
102
133
  return None
103
134
 
104
135
 
136
+ DEFAULT_MAX_RESPONSE_TIME = 10.0
137
+
138
+
105
139
  def max_response_time(ctx: CheckContext, response: Response, case: Case) -> bool | None:
106
- config = ctx.config.get(max_response_time, MaxResponseTimeConfig())
140
+ limit = ctx.config.max_response_time.limit or DEFAULT_MAX_RESPONSE_TIME
107
141
  elapsed = response.elapsed
108
- if elapsed > config.limit:
142
+ if elapsed > limit:
109
143
  raise ResponseTimeExceeded(
110
144
  operation=case.operation.label,
111
- message=f"Actual: {elapsed:.2f}ms\nLimit: {config.limit * 1000:.2f}ms",
145
+ message=f"Actual: {elapsed:.2f}ms\nLimit: {limit * 1000:.2f}ms",
112
146
  elapsed=elapsed,
113
- deadline=config.limit,
147
+ deadline=limit,
114
148
  )
115
149
  return None
116
150
 
@@ -4,20 +4,68 @@ from dataclasses import dataclass
4
4
  from typing import Any
5
5
 
6
6
  import click
7
+ from tomli import TOMLDecodeError
7
8
 
8
- from schemathesis.cli import hooks
9
+ from schemathesis.cli.commands.data import Data
9
10
  from schemathesis.cli.commands.run import run as run_command
11
+ from schemathesis.cli.commands.run.handlers.output import display_header
12
+ from schemathesis.cli.constants import EXTENSIONS_DOCUMENTATION_URL
10
13
  from schemathesis.cli.core import get_terminal_width
11
14
  from schemathesis.cli.ext.groups import CommandWithGroupedOptions, GroupedOption
15
+ from schemathesis.config import ConfigError, SchemathesisConfig
16
+ from schemathesis.core.errors import HookError, format_exception
17
+ from schemathesis.core.version import SCHEMATHESIS_VERSION
12
18
 
13
19
  CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
14
20
 
15
21
 
16
22
  @click.group(context_settings=CONTEXT_SETTINGS) # type: ignore[misc]
23
+ @click.option( # type: ignore[misc]
24
+ "--config-file",
25
+ "config_file",
26
+ help="The path to `schemathesis.toml` file to use for configuration",
27
+ metavar="PATH",
28
+ type=str,
29
+ )
30
+ @click.pass_context # type: ignore[misc]
17
31
  @click.version_option() # type: ignore[misc]
18
- def schemathesis() -> None:
32
+ def schemathesis(ctx: click.Context, config_file: str | None) -> None:
19
33
  """Property-based API testing for OpenAPI and GraphQL."""
20
- hooks.load()
34
+ try:
35
+ if config_file is not None:
36
+ config = SchemathesisConfig.from_path(config_file)
37
+ else:
38
+ config = SchemathesisConfig.discover()
39
+ except (TOMLDecodeError, ConfigError) as exc:
40
+ display_header(SCHEMATHESIS_VERSION)
41
+ click.secho(
42
+ f"❌ Failed to load configuration file{f' from {config_file}' if config_file else ''}",
43
+ fg="red",
44
+ bold=True,
45
+ )
46
+ if isinstance(exc, TOMLDecodeError):
47
+ detail = "The configuration file content is not valid TOML"
48
+ else:
49
+ detail = "The loaded configuration is incorrect"
50
+ click.echo(f"\n{detail}\n\n{exc}")
51
+ ctx.exit(1)
52
+ except HookError as exc:
53
+ click.secho("Unable to load Schemathesis extension hooks", fg="red", bold=True)
54
+ formatted_module_name = click.style(f"'{exc.module_path}'", bold=True)
55
+ cause = exc.__cause__
56
+ assert isinstance(cause, Exception)
57
+ if isinstance(cause, ModuleNotFoundError) and cause.name == exc.module_path:
58
+ click.echo(
59
+ f"\nAn attempt to import the module {formatted_module_name} failed because it could not be found."
60
+ )
61
+ click.echo("\nEnsure the module name is correctly spelled and reachable from the current directory.")
62
+ else:
63
+ click.echo(f"\nAn error occurred while importing the module {formatted_module_name}. Traceback:")
64
+ message = format_exception(cause, with_traceback=True, skip_frames=1)
65
+ click.secho(f"\n{message}", fg="red")
66
+ click.echo(f"\nFor more information on how to work with hooks, visit {EXTENSIONS_DOCUMENTATION_URL}")
67
+ ctx.exit(1)
68
+ ctx.obj = Data(config=config)
21
69
 
22
70
 
23
71
  @dataclass
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass
2
+
3
+ from schemathesis.config import SchemathesisConfig
4
+
5
+
6
+ @dataclass
7
+ class Data:
8
+ config: SchemathesisConfig
9
+
10
+ __slots__ = ("config",)