schemathesis 3.25.6__py3-none-any.whl → 3.39.7__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.
- schemathesis/__init__.py +6 -6
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +4 -2
- schemathesis/_hypothesis.py +369 -56
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +5 -4
- schemathesis/_patches.py +21 -0
- schemathesis/_rate_limiter.py +7 -0
- schemathesis/_xml.py +75 -22
- schemathesis/auths.py +78 -16
- schemathesis/checks.py +21 -9
- schemathesis/cli/__init__.py +783 -432
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/callbacks.py +58 -13
- schemathesis/cli/cassettes.py +233 -47
- schemathesis/cli/constants.py +8 -2
- schemathesis/cli/context.py +22 -5
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +4 -1
- schemathesis/cli/junitxml.py +103 -22
- schemathesis/cli/options.py +15 -4
- schemathesis/cli/output/default.py +258 -112
- schemathesis/cli/output/short.py +23 -8
- schemathesis/cli/reporting.py +79 -0
- schemathesis/cli/sanitization.py +6 -0
- schemathesis/code_samples.py +5 -3
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +3 -3
- schemathesis/exceptions.py +76 -65
- schemathesis/experimental/__init__.py +35 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +17 -25
- schemathesis/failures.py +77 -9
- schemathesis/filters.py +185 -8
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +20 -36
- schemathesis/generation/_hypothesis.py +59 -0
- schemathesis/generation/_methods.py +44 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +89 -12
- schemathesis/internal/checks.py +84 -0
- schemathesis/internal/copy.py +22 -3
- schemathesis/internal/deprecation.py +6 -2
- schemathesis/internal/diff.py +15 -0
- schemathesis/internal/extensions.py +27 -0
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +11 -0
- schemathesis/lazy.py +138 -25
- schemathesis/loaders.py +7 -5
- schemathesis/models.py +318 -211
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +50 -15
- schemathesis/runner/events.py +65 -5
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +388 -177
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/runner/probes.py +11 -9
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +7 -2
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +39 -6
- schemathesis/service/events.py +5 -1
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +6 -2
- schemathesis/service/metadata.py +25 -0
- schemathesis/service/models.py +211 -2
- schemathesis/service/report.py +6 -6
- schemathesis/service/serialization.py +45 -71
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +26 -0
- schemathesis/specs/graphql/loaders.py +25 -5
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +130 -100
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_cache.py +123 -0
- schemathesis/specs/openapi/_hypothesis.py +78 -60
- schemathesis/specs/openapi/checks.py +504 -25
- schemathesis/specs/openapi/converter.py +31 -4
- schemathesis/specs/openapi/definitions.py +10 -17
- schemathesis/specs/openapi/examples.py +126 -12
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +26 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +29 -6
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +44 -0
- schemathesis/specs/openapi/links.py +125 -42
- schemathesis/specs/openapi/loaders.py +77 -36
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/__init__.py +6 -3
- schemathesis/specs/openapi/negative/mutations.py +21 -6
- schemathesis/specs/openapi/parameters.py +39 -25
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +37 -7
- schemathesis/specs/openapi/schemas.py +360 -241
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +198 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +14 -0
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +35 -21
- schemathesis/stateful/config.py +97 -0
- schemathesis/stateful/context.py +135 -0
- schemathesis/stateful/events.py +274 -0
- schemathesis/stateful/runner.py +309 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +67 -38
- schemathesis/stateful/statistic.py +22 -0
- schemathesis/stateful/validation.py +100 -0
- schemathesis/targets.py +33 -1
- schemathesis/throttling.py +25 -5
- schemathesis/transports/__init__.py +354 -0
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +25 -2
- schemathesis/transports/content_types.py +3 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +9 -4
- schemathesis/types.py +9 -0
- schemathesis/utils.py +11 -16
- schemathesis-3.39.7.dist-info/METADATA +293 -0
- schemathesis-3.39.7.dist-info/RECORD +160 -0
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
schemathesis/graphql.py
CHANGED
schemathesis/hooks.py
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import inspect
|
|
3
4
|
from collections import defaultdict
|
|
4
5
|
from copy import deepcopy
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
from enum import Enum, unique
|
|
7
8
|
from functools import partial
|
|
8
|
-
from typing import TYPE_CHECKING, Any, Callable, ClassVar,
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Callable, ClassVar, cast
|
|
9
10
|
|
|
10
|
-
from .
|
|
11
|
+
from .filters import FilterSet, attach_filter_chain
|
|
11
12
|
from .internal.deprecation import deprecated_property
|
|
12
13
|
|
|
13
14
|
if TYPE_CHECKING:
|
|
14
15
|
from hypothesis import strategies as st
|
|
16
|
+
|
|
15
17
|
from .models import APIOperation, Case
|
|
16
18
|
from .schemas import BaseSchema
|
|
17
19
|
from .transports.responses import GenericResponse
|
|
20
|
+
from .types import GenericTest
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
@unique
|
|
@@ -29,6 +32,8 @@ class RegisteredHook:
|
|
|
29
32
|
signature: inspect.Signature
|
|
30
33
|
scopes: list[HookScope]
|
|
31
34
|
|
|
35
|
+
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
|
36
|
+
|
|
32
37
|
|
|
33
38
|
@dataclass
|
|
34
39
|
class HookContext:
|
|
@@ -40,11 +45,63 @@ class HookContext:
|
|
|
40
45
|
|
|
41
46
|
operation: APIOperation | None = None
|
|
42
47
|
|
|
43
|
-
@deprecated_property(removed_in="4.0", replacement="operation")
|
|
48
|
+
@deprecated_property(removed_in="4.0", replacement="`operation`")
|
|
44
49
|
def endpoint(self) -> APIOperation | None:
|
|
45
50
|
return self.operation
|
|
46
51
|
|
|
47
52
|
|
|
53
|
+
def to_filterable_hook(dispatcher: HookDispatcher) -> Callable:
|
|
54
|
+
filter_used = False
|
|
55
|
+
filter_set = FilterSet()
|
|
56
|
+
|
|
57
|
+
def register(hook: str | Callable) -> Callable:
|
|
58
|
+
nonlocal filter_set
|
|
59
|
+
|
|
60
|
+
if filter_used:
|
|
61
|
+
validate_filterable_hook(hook)
|
|
62
|
+
|
|
63
|
+
if isinstance(hook, str):
|
|
64
|
+
|
|
65
|
+
def decorator(func: Callable) -> Callable:
|
|
66
|
+
hook_name = cast(str, hook)
|
|
67
|
+
if filter_used:
|
|
68
|
+
validate_filterable_hook(hook)
|
|
69
|
+
func.filter_set = filter_set # type: ignore[attr-defined]
|
|
70
|
+
return dispatcher.register_hook_with_name(func, hook_name)
|
|
71
|
+
|
|
72
|
+
init_filter_set(decorator)
|
|
73
|
+
return decorator
|
|
74
|
+
|
|
75
|
+
hook.filter_set = filter_set # type: ignore[attr-defined]
|
|
76
|
+
init_filter_set(register)
|
|
77
|
+
return dispatcher.register_hook_with_name(hook, hook.__name__)
|
|
78
|
+
|
|
79
|
+
def init_filter_set(target: Callable) -> FilterSet:
|
|
80
|
+
nonlocal filter_used
|
|
81
|
+
|
|
82
|
+
filter_used = False
|
|
83
|
+
filter_set = FilterSet()
|
|
84
|
+
|
|
85
|
+
def include(*args: Any, **kwargs: Any) -> None:
|
|
86
|
+
nonlocal filter_used
|
|
87
|
+
|
|
88
|
+
filter_used = True
|
|
89
|
+
filter_set.include(*args, **kwargs)
|
|
90
|
+
|
|
91
|
+
def exclude(*args: Any, **kwargs: Any) -> None:
|
|
92
|
+
nonlocal filter_used
|
|
93
|
+
|
|
94
|
+
filter_used = True
|
|
95
|
+
filter_set.exclude(*args, **kwargs)
|
|
96
|
+
|
|
97
|
+
attach_filter_chain(target, "apply_to", include)
|
|
98
|
+
attach_filter_chain(target, "skip_for", exclude)
|
|
99
|
+
return filter_set
|
|
100
|
+
|
|
101
|
+
filter_set = init_filter_set(register)
|
|
102
|
+
return register
|
|
103
|
+
|
|
104
|
+
|
|
48
105
|
@dataclass
|
|
49
106
|
class HookDispatcher:
|
|
50
107
|
"""Generic hook dispatcher.
|
|
@@ -53,9 +110,12 @@ class HookDispatcher:
|
|
|
53
110
|
"""
|
|
54
111
|
|
|
55
112
|
scope: HookScope
|
|
56
|
-
_hooks:
|
|
113
|
+
_hooks: defaultdict[str, list[Callable]] = field(default_factory=lambda: defaultdict(list))
|
|
57
114
|
_specs: ClassVar[dict[str, RegisteredHook]] = {}
|
|
58
115
|
|
|
116
|
+
def __post_init__(self) -> None:
|
|
117
|
+
self.register = to_filterable_hook(self) # type: ignore[method-assign]
|
|
118
|
+
|
|
59
119
|
def register(self, hook: str | Callable) -> Callable:
|
|
60
120
|
"""Register a new hook.
|
|
61
121
|
|
|
@@ -78,14 +138,7 @@ class HookDispatcher:
|
|
|
78
138
|
def hook(context, strategy):
|
|
79
139
|
...
|
|
80
140
|
"""
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def decorator(func: Callable) -> Callable:
|
|
84
|
-
hook_name = cast(str, hook)
|
|
85
|
-
return self.register_hook_with_name(func, hook_name)
|
|
86
|
-
|
|
87
|
-
return decorator
|
|
88
|
-
return self.register_hook_with_name(hook, hook.__name__)
|
|
141
|
+
raise NotImplementedError
|
|
89
142
|
|
|
90
143
|
def merge(self, other: HookDispatcher) -> HookDispatcher:
|
|
91
144
|
"""Merge two dispatches together.
|
|
@@ -190,14 +243,22 @@ class HookDispatcher:
|
|
|
190
243
|
self, strategy: st.SearchStrategy, container: str, context: HookContext
|
|
191
244
|
) -> st.SearchStrategy:
|
|
192
245
|
for hook in self.get_all_by_name(f"before_generate_{container}"):
|
|
246
|
+
if _should_skip_hook(hook, context):
|
|
247
|
+
continue
|
|
193
248
|
strategy = hook(context, strategy)
|
|
194
249
|
for hook in self.get_all_by_name(f"filter_{container}"):
|
|
250
|
+
if _should_skip_hook(hook, context):
|
|
251
|
+
continue
|
|
195
252
|
hook = partial(hook, context)
|
|
196
253
|
strategy = strategy.filter(hook)
|
|
197
254
|
for hook in self.get_all_by_name(f"map_{container}"):
|
|
255
|
+
if _should_skip_hook(hook, context):
|
|
256
|
+
continue
|
|
198
257
|
hook = partial(hook, context)
|
|
199
258
|
strategy = strategy.map(hook)
|
|
200
259
|
for hook in self.get_all_by_name(f"flatmap_{container}"):
|
|
260
|
+
if _should_skip_hook(hook, context):
|
|
261
|
+
continue
|
|
201
262
|
hook = partial(hook, context)
|
|
202
263
|
strategy = strategy.flatmap(hook)
|
|
203
264
|
return strategy
|
|
@@ -205,6 +266,8 @@ class HookDispatcher:
|
|
|
205
266
|
def dispatch(self, name: str, context: HookContext, *args: Any, **kwargs: Any) -> None:
|
|
206
267
|
"""Run all hooks for the given name."""
|
|
207
268
|
for hook in self.get_all_by_name(name):
|
|
269
|
+
if _should_skip_hook(hook, context):
|
|
270
|
+
continue
|
|
208
271
|
hook(context, *args, **kwargs)
|
|
209
272
|
|
|
210
273
|
def unregister(self, hook: Callable) -> None:
|
|
@@ -224,6 +287,11 @@ class HookDispatcher:
|
|
|
224
287
|
self._hooks = defaultdict(list)
|
|
225
288
|
|
|
226
289
|
|
|
290
|
+
def _should_skip_hook(hook: Callable, ctx: HookContext) -> bool:
|
|
291
|
+
filter_set = getattr(hook, "filter_set", None)
|
|
292
|
+
return filter_set is not None and ctx.operation is not None and not filter_set.match(ctx)
|
|
293
|
+
|
|
294
|
+
|
|
227
295
|
def apply_to_all_dispatchers(
|
|
228
296
|
operation: APIOperation,
|
|
229
297
|
context: HookContext,
|
|
@@ -246,6 +314,15 @@ def should_skip_operation(dispatcher: HookDispatcher, context: HookContext) -> b
|
|
|
246
314
|
return False
|
|
247
315
|
|
|
248
316
|
|
|
317
|
+
def validate_filterable_hook(hook: str | Callable) -> None:
|
|
318
|
+
if callable(hook):
|
|
319
|
+
name = hook.__name__
|
|
320
|
+
else:
|
|
321
|
+
name = hook
|
|
322
|
+
if name in ("before_process_path", "before_load_schema", "after_load_schema", "after_init_cli_run_handlers"):
|
|
323
|
+
raise ValueError(f"Filters are not applicable to this hook: `{name}`")
|
|
324
|
+
|
|
325
|
+
|
|
249
326
|
all_scopes = HookDispatcher.register_spec(list(HookScope))
|
|
250
327
|
|
|
251
328
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import warnings
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING, Callable, Optional
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from requests.auth import HTTPDigestAuth
|
|
10
|
+
from requests.structures import CaseInsensitiveDict
|
|
11
|
+
|
|
12
|
+
from .._override import CaseOverride
|
|
13
|
+
from ..models import Case
|
|
14
|
+
from ..transports.responses import GenericResponse
|
|
15
|
+
from ..types import RawAuth
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
CheckFunction = Callable[["CheckContext", "GenericResponse", "Case"], Optional[bool]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class NegativeDataRejectionConfig:
|
|
23
|
+
# 5xx will pass through
|
|
24
|
+
allowed_statuses: list[str] = field(default_factory=lambda: ["400", "401", "403", "404", "422", "428", "5xx"])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class PositiveDataAcceptanceConfig:
|
|
29
|
+
allowed_statuses: list[str] = field(default_factory=lambda: ["2xx", "401", "403", "404"])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class MissingRequiredHeaderConfig:
|
|
34
|
+
allowed_statuses: list[str] = field(default_factory=lambda: ["406"])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class CheckConfig:
|
|
39
|
+
missing_required_header: MissingRequiredHeaderConfig = field(default_factory=MissingRequiredHeaderConfig)
|
|
40
|
+
negative_data_rejection: NegativeDataRejectionConfig = field(default_factory=NegativeDataRejectionConfig)
|
|
41
|
+
positive_data_acceptance: PositiveDataAcceptanceConfig = field(default_factory=PositiveDataAcceptanceConfig)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class CheckContext:
|
|
46
|
+
"""Context for Schemathesis checks.
|
|
47
|
+
|
|
48
|
+
Provides access to broader test execution data beyond individual test cases.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
override: CaseOverride | None
|
|
52
|
+
auth: HTTPDigestAuth | RawAuth | None
|
|
53
|
+
headers: CaseInsensitiveDict | None
|
|
54
|
+
config: CheckConfig = field(default_factory=CheckConfig)
|
|
55
|
+
transport_kwargs: dict | None = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def wrap_check(check: Callable) -> CheckFunction:
|
|
59
|
+
"""Make older checks compatible with the new signature."""
|
|
60
|
+
signature = inspect.signature(check)
|
|
61
|
+
parameters = len(signature.parameters)
|
|
62
|
+
|
|
63
|
+
if parameters == 3:
|
|
64
|
+
# New style check, return as is
|
|
65
|
+
return check
|
|
66
|
+
|
|
67
|
+
if parameters == 2:
|
|
68
|
+
# Old style check, wrap it
|
|
69
|
+
warnings.warn(
|
|
70
|
+
f"The check function '{check.__name__}' uses an outdated signature. "
|
|
71
|
+
"Please update it to accept 'ctx' as the first argument: "
|
|
72
|
+
"(ctx: CheckContext, response: GenericResponse, case: Case) -> Optional[bool]",
|
|
73
|
+
DeprecationWarning,
|
|
74
|
+
stacklevel=2,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def wrapper(_: CheckContext, response: GenericResponse, case: Case) -> Optional[bool]:
|
|
78
|
+
return check(response, case)
|
|
79
|
+
|
|
80
|
+
wrapper.__name__ = check.__name__
|
|
81
|
+
|
|
82
|
+
return wrapper
|
|
83
|
+
|
|
84
|
+
raise ValueError(f"Invalid check function signature. Expected 2 or 3 parameters, got {parameters}")
|
schemathesis/internal/copy.py
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
+
from .extensions import extensible
|
|
3
4
|
|
|
5
|
+
|
|
6
|
+
@extensible("SCHEMATHESIS_EXTENSION_FAST_DEEP_COPY")
|
|
4
7
|
def fast_deepcopy(value: Any) -> Any:
|
|
5
|
-
"""A specialized version of `deepcopy` that copies only `dict` and `list
|
|
8
|
+
"""A specialized version of `deepcopy` that copies only `dict` and `list` and does unrolling.
|
|
6
9
|
|
|
7
10
|
It is on average 3x faster than `deepcopy` and given the amount of calls, it is an important optimization.
|
|
8
11
|
"""
|
|
9
12
|
if isinstance(value, dict):
|
|
10
|
-
return {
|
|
13
|
+
return {
|
|
14
|
+
k1: (
|
|
15
|
+
{k2: fast_deepcopy(v2) for k2, v2 in v1.items()}
|
|
16
|
+
if isinstance(v1, dict)
|
|
17
|
+
else [fast_deepcopy(v2) for v2 in v1]
|
|
18
|
+
if isinstance(v1, list)
|
|
19
|
+
else v1
|
|
20
|
+
)
|
|
21
|
+
for k1, v1 in value.items()
|
|
22
|
+
}
|
|
11
23
|
if isinstance(value, list):
|
|
12
|
-
return [
|
|
24
|
+
return [
|
|
25
|
+
{k2: fast_deepcopy(v2) for k2, v2 in v1.items()}
|
|
26
|
+
if isinstance(v1, dict)
|
|
27
|
+
else [fast_deepcopy(v2) for v2 in v1]
|
|
28
|
+
if isinstance(v1, list)
|
|
29
|
+
else v1
|
|
30
|
+
for v1 in value
|
|
31
|
+
]
|
|
13
32
|
return value
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import warnings
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Any, Callable
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
def _warn_deprecation(*, kind: str, thing: str, removed_in: str, replacement: str) -> None:
|
|
6
6
|
warnings.warn(
|
|
7
7
|
f"{kind} `{thing}` is deprecated and will be removed in Schemathesis {removed_in}. "
|
|
8
|
-
f"Use
|
|
8
|
+
f"Use {replacement} instead.",
|
|
9
9
|
DeprecationWarning,
|
|
10
10
|
stacklevel=1,
|
|
11
11
|
)
|
|
@@ -23,6 +23,10 @@ def deprecated_property(*, removed_in: str, replacement: str) -> Callable:
|
|
|
23
23
|
return wrapper
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def warn_filtration_arguments(name: str) -> None:
|
|
27
|
+
_warn_deprecation(kind="Argument", thing=name, removed_in="4.0", replacement="`include` and `exclude` methods")
|
|
28
|
+
|
|
29
|
+
|
|
26
30
|
def deprecated_function(*, removed_in: str, replacement: str) -> Callable:
|
|
27
31
|
def wrapper(func: Callable) -> Callable:
|
|
28
32
|
def inner(*args: Any, **kwargs: Any) -> Any:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Mapping
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def diff(left: Mapping[str, Any], right: Mapping[str, Any]) -> dict[str, Any]:
|
|
7
|
+
"""Calculate the difference between two dictionaries."""
|
|
8
|
+
diff = {}
|
|
9
|
+
for key, value in right.items():
|
|
10
|
+
if key not in left or left[key] != value:
|
|
11
|
+
diff[key] = value
|
|
12
|
+
for key in left:
|
|
13
|
+
if key not in right:
|
|
14
|
+
diff[key] = None # Mark deleted items as None
|
|
15
|
+
return diff
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any, Callable
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ExtensionLoadingError(ImportError):
|
|
6
|
+
"""Raised when an extension cannot be loaded."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def import_extension(path: str) -> Any:
|
|
10
|
+
try:
|
|
11
|
+
module, item = path.rsplit(".", 1)
|
|
12
|
+
imported = __import__(module, fromlist=[item])
|
|
13
|
+
return getattr(imported, item)
|
|
14
|
+
except ValueError as exc:
|
|
15
|
+
raise ExtensionLoadingError(f"Invalid path: {path}") from exc
|
|
16
|
+
except (ImportError, AttributeError) as exc:
|
|
17
|
+
raise ExtensionLoadingError(f"Could not import {path}") from exc
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def extensible(env_var: str) -> Callable[[Any], Any]:
|
|
21
|
+
def decorator(item: Any) -> Any:
|
|
22
|
+
path = os.getenv(env_var)
|
|
23
|
+
if path is not None:
|
|
24
|
+
return import_extension(path)
|
|
25
|
+
return item
|
|
26
|
+
|
|
27
|
+
return decorator
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass, replace
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
TRUNCATED = "// Output truncated..."
|
|
8
|
+
MAX_PAYLOAD_SIZE = 512
|
|
9
|
+
MAX_LINES = 10
|
|
10
|
+
MAX_WIDTH = 80
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class OutputConfig:
|
|
15
|
+
"""Options for configuring various aspects of Schemathesis output."""
|
|
16
|
+
|
|
17
|
+
truncate: bool = True
|
|
18
|
+
max_payload_size: int = MAX_PAYLOAD_SIZE
|
|
19
|
+
max_lines: int = MAX_LINES
|
|
20
|
+
max_width: int = MAX_WIDTH
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_parent(cls, parent: OutputConfig | None = None, **changes: Any) -> OutputConfig:
|
|
24
|
+
parent = parent or OutputConfig()
|
|
25
|
+
return parent.replace(**changes)
|
|
26
|
+
|
|
27
|
+
def replace(self, **changes: Any) -> OutputConfig:
|
|
28
|
+
"""Create a new instance with updated values."""
|
|
29
|
+
return replace(self, **changes)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def truncate_json(data: Any, *, config: OutputConfig | None = None) -> str:
|
|
33
|
+
config = config or OutputConfig()
|
|
34
|
+
# Convert JSON to string with indentation
|
|
35
|
+
indent = 4
|
|
36
|
+
serialized = json.dumps(data, indent=indent)
|
|
37
|
+
if not config.truncate:
|
|
38
|
+
return serialized
|
|
39
|
+
|
|
40
|
+
# Split string by lines
|
|
41
|
+
|
|
42
|
+
lines = [
|
|
43
|
+
line[: config.max_width - 3] + "..." if len(line) > config.max_width else line
|
|
44
|
+
for line in serialized.split("\n")
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
if len(lines) <= config.max_lines:
|
|
48
|
+
return "\n".join(lines)
|
|
49
|
+
|
|
50
|
+
truncated_lines = lines[: config.max_lines - 1]
|
|
51
|
+
indentation = " " * indent
|
|
52
|
+
truncated_lines.append(f"{indentation}{TRUNCATED}")
|
|
53
|
+
truncated_lines.append(lines[-1])
|
|
54
|
+
|
|
55
|
+
return "\n".join(truncated_lines)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def prepare_response_payload(payload: str, *, config: OutputConfig | None = None) -> str:
|
|
59
|
+
if payload.endswith("\r\n"):
|
|
60
|
+
payload = payload[:-2]
|
|
61
|
+
elif payload.endswith("\n"):
|
|
62
|
+
payload = payload[:-1]
|
|
63
|
+
config = config or OutputConfig()
|
|
64
|
+
if not config.truncate:
|
|
65
|
+
return payload
|
|
66
|
+
if len(payload) > config.max_payload_size:
|
|
67
|
+
payload = payload[: config.max_payload_size] + f" {TRUNCATED}"
|
|
68
|
+
return payload
|
schemathesis/internal/result.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from typing import Any
|
|
3
4
|
|
|
5
|
+
from ..constants import FALSE_VALUES, TRUE_VALUES
|
|
6
|
+
|
|
4
7
|
|
|
5
8
|
def merge_recursively(a: dict[str, Any], b: dict[str, Any]) -> dict[str, Any]:
|
|
6
9
|
"""Merge two dictionaries recursively."""
|
|
@@ -13,3 +16,11 @@ def merge_recursively(a: dict[str, Any], b: dict[str, Any]) -> dict[str, Any]:
|
|
|
13
16
|
else:
|
|
14
17
|
a[key] = b[key]
|
|
15
18
|
return a
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def convert_boolean_string(value: str) -> str | bool:
|
|
22
|
+
if value.lower() in TRUE_VALUES:
|
|
23
|
+
return True
|
|
24
|
+
if value.lower() in FALSE_VALUES:
|
|
25
|
+
return False
|
|
26
|
+
return value
|