schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__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 +27 -65
- schemathesis/auths.py +26 -68
- schemathesis/checks.py +130 -60
- schemathesis/cli/__init__.py +5 -2105
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +30 -0
- schemathesis/cli/commands/run/executor.py +141 -0
- schemathesis/cli/commands/run/filters.py +202 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +1368 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +37 -16
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +243 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +49 -68
- schemathesis/engine/phases/stateful/__init__.py +66 -0
- schemathesis/engine/phases/stateful/_executor.py +301 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +175 -0
- schemathesis/engine/phases/unit/_executor.py +322 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +246 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +9 -40
- schemathesis/filters.py +7 -95
- schemathesis/generation/__init__.py +3 -3
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +22 -22
- schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +17 -62
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +387 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +456 -228
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +5 -3
- schemathesis/specs/graphql/schemas.py +122 -123
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +97 -134
- schemathesis/specs/openapi/checks.py +238 -219
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +22 -20
- schemathesis/specs/openapi/expressions/__init__.py +11 -15
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/nodes.py +33 -32
- schemathesis/specs/openapi/formats.py +3 -2
- schemathesis/specs/openapi/links.py +123 -299
- schemathesis/specs/openapi/media_types.py +10 -12
- schemathesis/specs/openapi/negative/__init__.py +2 -1
- schemathesis/specs/openapi/negative/mutations.py +3 -2
- schemathesis/specs/openapi/parameters.py +8 -6
- schemathesis/specs/openapi/patterns.py +1 -1
- schemathesis/specs/openapi/references.py +11 -51
- schemathesis/specs/openapi/schemas.py +177 -191
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +10 -6
- schemathesis/specs/openapi/stateful/__init__.py +97 -91
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -7
- schemathesis/transport/wsgi.py +165 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
- schemathesis-4.0.0a2.dist-info/RECORD +151 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -559
- schemathesis/_override.py +0 -50
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -936
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -56
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -277
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -84
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -38
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -104
- schemathesis/runner/impl/core.py +0 -1246
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/loaders.py +0 -708
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -359
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.7.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -1,26 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from typing import Any
|
4
|
-
|
5
|
-
from ..constants import FALSE_VALUES, TRUE_VALUES
|
6
|
-
|
7
|
-
|
8
|
-
def merge_recursively(a: dict[str, Any], b: dict[str, Any]) -> dict[str, Any]:
|
9
|
-
"""Merge two dictionaries recursively."""
|
10
|
-
for key in b:
|
11
|
-
if key in a:
|
12
|
-
if isinstance(a[key], dict) and isinstance(b[key], dict):
|
13
|
-
merge_recursively(a[key], b[key])
|
14
|
-
else:
|
15
|
-
a[key] = b[key]
|
16
|
-
else:
|
17
|
-
a[key] = b[key]
|
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
|
@@ -1,34 +0,0 @@
|
|
1
|
-
import pathlib
|
2
|
-
import re
|
3
|
-
from typing import Any
|
4
|
-
|
5
|
-
|
6
|
-
def require_relative_url(url: str) -> None:
|
7
|
-
"""Raise an error if the URL is not relative."""
|
8
|
-
from yarl import URL
|
9
|
-
|
10
|
-
if URL(url).is_absolute():
|
11
|
-
raise ValueError("Schema path should be relative for WSGI/ASGI loaders")
|
12
|
-
|
13
|
-
|
14
|
-
def file_exists(path: str) -> bool:
|
15
|
-
try:
|
16
|
-
return pathlib.Path(path).is_file()
|
17
|
-
except OSError:
|
18
|
-
# For example, path could be too long
|
19
|
-
return False
|
20
|
-
|
21
|
-
|
22
|
-
def is_filename(value: str) -> bool:
|
23
|
-
"""Detect if the input string is a filename by checking its extension."""
|
24
|
-
return bool(pathlib.Path(value).suffix)
|
25
|
-
|
26
|
-
|
27
|
-
SURROGATE_PAIR_RE = re.compile(r"[\ud800-\udfff]")
|
28
|
-
has_surrogate_pair = SURROGATE_PAIR_RE.search
|
29
|
-
|
30
|
-
|
31
|
-
def is_illegal_surrogate(item: Any) -> bool:
|
32
|
-
if isinstance(item, list):
|
33
|
-
return any(isinstance(item_, str) and bool(has_surrogate_pair(item_)) for item_ in item)
|
34
|
-
return isinstance(item, str) and bool(has_surrogate_pair(item))
|
schemathesis/lazy.py
DELETED
@@ -1,474 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from contextlib import nullcontext
|
4
|
-
from dataclasses import dataclass, field
|
5
|
-
from inspect import signature
|
6
|
-
from typing import TYPE_CHECKING, Any, Callable, Generator, Type
|
7
|
-
|
8
|
-
import pytest
|
9
|
-
from hypothesis.core import HypothesisHandle
|
10
|
-
from hypothesis.errors import Flaky
|
11
|
-
from hypothesis.internal.escalation import format_exception, get_trimmed_traceback
|
12
|
-
from hypothesis.internal.reflection import impersonate
|
13
|
-
from pytest_subtests import SubTests
|
14
|
-
|
15
|
-
from ._compat import MultipleFailures, get_interesting_origin
|
16
|
-
from ._override import CaseOverride, check_no_override_mark, get_override_from_mark, set_override_mark
|
17
|
-
from .auths import AuthStorage
|
18
|
-
from .code_samples import CodeSampleStyle
|
19
|
-
from .constants import FLAKY_FAILURE_MESSAGE, NOT_SET
|
20
|
-
from .exceptions import CheckFailed, OperationSchemaError, SkipTest, get_grouped_exception
|
21
|
-
from .filters import FilterSet, FilterValue, MatcherFunc, RegexValue, filter_set_from_components, is_deprecated
|
22
|
-
from .hooks import HookDispatcher, HookScope
|
23
|
-
from .internal.deprecation import warn_filtration_arguments
|
24
|
-
from .internal.result import Ok
|
25
|
-
from .schemas import BaseSchema
|
26
|
-
from .utils import (
|
27
|
-
GivenInput,
|
28
|
-
fail_on_no_matches,
|
29
|
-
get_given_args,
|
30
|
-
get_given_kwargs,
|
31
|
-
given_proxy,
|
32
|
-
is_given_applied,
|
33
|
-
merge_given_args,
|
34
|
-
validate_given_args,
|
35
|
-
)
|
36
|
-
|
37
|
-
if TYPE_CHECKING:
|
38
|
-
from _pytest.fixtures import FixtureRequest
|
39
|
-
from pyrate_limiter import Limiter
|
40
|
-
|
41
|
-
from .generation import DataGenerationMethodInput, GenerationConfig
|
42
|
-
from .internal.output import OutputConfig
|
43
|
-
from .models import APIOperation
|
44
|
-
from .types import Filter, GenericTest, NotSet
|
45
|
-
|
46
|
-
|
47
|
-
@dataclass
|
48
|
-
class LazySchema:
|
49
|
-
fixture_name: str
|
50
|
-
base_url: str | None | NotSet = NOT_SET
|
51
|
-
app: Any = NOT_SET
|
52
|
-
filter_set: FilterSet = field(default_factory=FilterSet)
|
53
|
-
hooks: HookDispatcher = field(default_factory=lambda: HookDispatcher(scope=HookScope.SCHEMA))
|
54
|
-
auth: AuthStorage = field(default_factory=AuthStorage)
|
55
|
-
validate_schema: bool = True
|
56
|
-
data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET
|
57
|
-
generation_config: GenerationConfig | NotSet = NOT_SET
|
58
|
-
output_config: OutputConfig | NotSet = NOT_SET
|
59
|
-
code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
|
60
|
-
rate_limiter: Limiter | None = None
|
61
|
-
sanitize_output: bool = True
|
62
|
-
|
63
|
-
def include(
|
64
|
-
self,
|
65
|
-
func: MatcherFunc | None = None,
|
66
|
-
*,
|
67
|
-
name: FilterValue | None = None,
|
68
|
-
name_regex: str | None = None,
|
69
|
-
method: FilterValue | None = None,
|
70
|
-
method_regex: str | None = None,
|
71
|
-
path: FilterValue | None = None,
|
72
|
-
path_regex: str | None = None,
|
73
|
-
tag: FilterValue | None = None,
|
74
|
-
tag_regex: RegexValue | None = None,
|
75
|
-
operation_id: FilterValue | None = None,
|
76
|
-
operation_id_regex: RegexValue | None = None,
|
77
|
-
) -> LazySchema:
|
78
|
-
"""Include only operations that match the given filters."""
|
79
|
-
filter_set = self.filter_set.clone()
|
80
|
-
filter_set.include(
|
81
|
-
func,
|
82
|
-
name=name,
|
83
|
-
name_regex=name_regex,
|
84
|
-
method=method,
|
85
|
-
method_regex=method_regex,
|
86
|
-
path=path,
|
87
|
-
path_regex=path_regex,
|
88
|
-
tag=tag,
|
89
|
-
tag_regex=tag_regex,
|
90
|
-
operation_id=operation_id,
|
91
|
-
operation_id_regex=operation_id_regex,
|
92
|
-
)
|
93
|
-
return self.__class__(
|
94
|
-
fixture_name=self.fixture_name,
|
95
|
-
base_url=self.base_url,
|
96
|
-
app=self.app,
|
97
|
-
hooks=self.hooks,
|
98
|
-
auth=self.auth,
|
99
|
-
validate_schema=self.validate_schema,
|
100
|
-
data_generation_methods=self.data_generation_methods,
|
101
|
-
generation_config=self.generation_config,
|
102
|
-
output_config=self.output_config,
|
103
|
-
code_sample_style=self.code_sample_style,
|
104
|
-
rate_limiter=self.rate_limiter,
|
105
|
-
sanitize_output=self.sanitize_output,
|
106
|
-
filter_set=filter_set,
|
107
|
-
)
|
108
|
-
|
109
|
-
def exclude(
|
110
|
-
self,
|
111
|
-
func: MatcherFunc | None = None,
|
112
|
-
*,
|
113
|
-
name: FilterValue | None = None,
|
114
|
-
name_regex: str | None = None,
|
115
|
-
method: FilterValue | None = None,
|
116
|
-
method_regex: str | None = None,
|
117
|
-
path: FilterValue | None = None,
|
118
|
-
path_regex: str | None = None,
|
119
|
-
tag: FilterValue | None = None,
|
120
|
-
tag_regex: RegexValue | None = None,
|
121
|
-
operation_id: FilterValue | None = None,
|
122
|
-
operation_id_regex: RegexValue | None = None,
|
123
|
-
deprecated: bool = False,
|
124
|
-
) -> LazySchema:
|
125
|
-
"""Exclude operations that match the given filters."""
|
126
|
-
filter_set = self.filter_set.clone()
|
127
|
-
if deprecated:
|
128
|
-
if func is None:
|
129
|
-
func = is_deprecated
|
130
|
-
else:
|
131
|
-
filter_set.exclude(is_deprecated)
|
132
|
-
filter_set.exclude(
|
133
|
-
func,
|
134
|
-
name=name,
|
135
|
-
name_regex=name_regex,
|
136
|
-
method=method,
|
137
|
-
method_regex=method_regex,
|
138
|
-
path=path,
|
139
|
-
path_regex=path_regex,
|
140
|
-
tag=tag,
|
141
|
-
tag_regex=tag_regex,
|
142
|
-
operation_id=operation_id,
|
143
|
-
operation_id_regex=operation_id_regex,
|
144
|
-
)
|
145
|
-
return self.__class__(
|
146
|
-
fixture_name=self.fixture_name,
|
147
|
-
base_url=self.base_url,
|
148
|
-
app=self.app,
|
149
|
-
hooks=self.hooks,
|
150
|
-
auth=self.auth,
|
151
|
-
validate_schema=self.validate_schema,
|
152
|
-
data_generation_methods=self.data_generation_methods,
|
153
|
-
generation_config=self.generation_config,
|
154
|
-
output_config=self.output_config,
|
155
|
-
code_sample_style=self.code_sample_style,
|
156
|
-
rate_limiter=self.rate_limiter,
|
157
|
-
sanitize_output=self.sanitize_output,
|
158
|
-
filter_set=filter_set,
|
159
|
-
)
|
160
|
-
|
161
|
-
def hook(self, hook: str | Callable) -> Callable:
|
162
|
-
return self.hooks.register(hook)
|
163
|
-
|
164
|
-
def parametrize(
|
165
|
-
self,
|
166
|
-
method: Filter | None = NOT_SET,
|
167
|
-
endpoint: Filter | None = NOT_SET,
|
168
|
-
tag: Filter | None = NOT_SET,
|
169
|
-
operation_id: Filter | None = NOT_SET,
|
170
|
-
validate_schema: bool | NotSet = NOT_SET,
|
171
|
-
skip_deprecated_operations: bool | NotSet = NOT_SET,
|
172
|
-
data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
|
173
|
-
generation_config: GenerationConfig | NotSet = NOT_SET,
|
174
|
-
output_config: OutputConfig | NotSet = NOT_SET,
|
175
|
-
code_sample_style: str | NotSet = NOT_SET,
|
176
|
-
) -> Callable:
|
177
|
-
for name in ("method", "endpoint", "tag", "operation_id", "skip_deprecated_operations"):
|
178
|
-
value = locals()[name]
|
179
|
-
if value is not NOT_SET:
|
180
|
-
warn_filtration_arguments(name)
|
181
|
-
if data_generation_methods is NOT_SET:
|
182
|
-
data_generation_methods = self.data_generation_methods
|
183
|
-
if generation_config is NOT_SET:
|
184
|
-
generation_config = self.generation_config
|
185
|
-
if output_config is NOT_SET:
|
186
|
-
output_config = self.output_config
|
187
|
-
if isinstance(code_sample_style, str):
|
188
|
-
_code_sample_style = CodeSampleStyle.from_str(code_sample_style)
|
189
|
-
else:
|
190
|
-
_code_sample_style = self.code_sample_style
|
191
|
-
|
192
|
-
def wrapper(test: Callable) -> Callable:
|
193
|
-
if is_given_applied(test):
|
194
|
-
# The user wrapped the test function with `@schema.given`
|
195
|
-
# These args & kwargs go as extra to the underlying test generator
|
196
|
-
given_args = get_given_args(test)
|
197
|
-
given_kwargs = get_given_kwargs(test)
|
198
|
-
test_function = validate_given_args(test, given_args, given_kwargs)
|
199
|
-
if test_function is not None:
|
200
|
-
return test_function
|
201
|
-
given_kwargs = merge_given_args(test, given_args, given_kwargs)
|
202
|
-
del given_args
|
203
|
-
else:
|
204
|
-
given_kwargs = {}
|
205
|
-
|
206
|
-
def wrapped_test(request: FixtureRequest) -> None:
|
207
|
-
"""The actual test, which is executed by pytest."""
|
208
|
-
__tracebackhide__ = True
|
209
|
-
if hasattr(wrapped_test, "_schemathesis_hooks"):
|
210
|
-
test._schemathesis_hooks = wrapped_test._schemathesis_hooks # type: ignore
|
211
|
-
schema = get_schema(
|
212
|
-
request=request,
|
213
|
-
name=self.fixture_name,
|
214
|
-
base_url=self.base_url,
|
215
|
-
method=method,
|
216
|
-
endpoint=endpoint,
|
217
|
-
tag=tag,
|
218
|
-
operation_id=operation_id,
|
219
|
-
hooks=self.hooks,
|
220
|
-
auth=self.auth if self.auth.providers is not None else NOT_SET,
|
221
|
-
test_function=test,
|
222
|
-
validate_schema=validate_schema,
|
223
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
224
|
-
data_generation_methods=data_generation_methods,
|
225
|
-
generation_config=generation_config,
|
226
|
-
output_config=output_config,
|
227
|
-
code_sample_style=_code_sample_style,
|
228
|
-
app=self.app,
|
229
|
-
rate_limiter=self.rate_limiter,
|
230
|
-
sanitize_output=self.sanitize_output,
|
231
|
-
filter_set=self.filter_set,
|
232
|
-
)
|
233
|
-
fixtures = get_fixtures(test, request, given_kwargs)
|
234
|
-
# Changing the node id is required for better reporting - the method and path will appear there
|
235
|
-
node_id = request.node._nodeid
|
236
|
-
settings = getattr(wrapped_test, "_hypothesis_internal_use_settings", None)
|
237
|
-
|
238
|
-
as_strategy_kwargs: Callable[[APIOperation], dict[str, Any]] | None = None
|
239
|
-
|
240
|
-
override = get_override_from_mark(test)
|
241
|
-
if override is not None:
|
242
|
-
|
243
|
-
def as_strategy_kwargs(_operation: APIOperation) -> dict[str, Any]:
|
244
|
-
nonlocal override
|
245
|
-
|
246
|
-
return {
|
247
|
-
location: entry for location, entry in override.for_operation(_operation).items() if entry
|
248
|
-
}
|
249
|
-
|
250
|
-
tests = list(
|
251
|
-
schema.get_all_tests(
|
252
|
-
test,
|
253
|
-
settings,
|
254
|
-
hooks=self.hooks,
|
255
|
-
as_strategy_kwargs=as_strategy_kwargs,
|
256
|
-
_given_kwargs=given_kwargs,
|
257
|
-
)
|
258
|
-
)
|
259
|
-
if not tests:
|
260
|
-
fail_on_no_matches(node_id)
|
261
|
-
request.session.testscollected += len(tests)
|
262
|
-
suspend_capture_ctx = _get_capturemanager(request)
|
263
|
-
subtests = SubTests(request.node.ihook, suspend_capture_ctx, request)
|
264
|
-
for result in tests:
|
265
|
-
if isinstance(result, Ok):
|
266
|
-
operation, sub_test = result.ok()
|
267
|
-
subtests.item._nodeid = _get_node_name(node_id, operation)
|
268
|
-
run_subtest(operation, fixtures, sub_test, subtests)
|
269
|
-
else:
|
270
|
-
_schema_error(subtests, result.err(), node_id)
|
271
|
-
subtests.item._nodeid = node_id
|
272
|
-
|
273
|
-
wrapped_test = pytest.mark.usefixtures(self.fixture_name)(wrapped_test)
|
274
|
-
_copy_marks(test, wrapped_test)
|
275
|
-
|
276
|
-
# Needed to prevent a failure when settings are applied to the test function
|
277
|
-
wrapped_test.is_hypothesis_test = True # type: ignore
|
278
|
-
wrapped_test.hypothesis = HypothesisHandle(test, wrapped_test, given_kwargs) # type: ignore
|
279
|
-
|
280
|
-
return wrapped_test
|
281
|
-
|
282
|
-
return wrapper
|
283
|
-
|
284
|
-
def given(self, *args: GivenInput, **kwargs: GivenInput) -> Callable:
|
285
|
-
return given_proxy(*args, **kwargs)
|
286
|
-
|
287
|
-
def override(
|
288
|
-
self,
|
289
|
-
*,
|
290
|
-
query: dict[str, str] | None = None,
|
291
|
-
headers: dict[str, str] | None = None,
|
292
|
-
cookies: dict[str, str] | None = None,
|
293
|
-
path_parameters: dict[str, str] | None = None,
|
294
|
-
) -> Callable[[GenericTest], GenericTest]:
|
295
|
-
"""Override Open API parameters with fixed values."""
|
296
|
-
|
297
|
-
def _add_override(test: GenericTest) -> GenericTest:
|
298
|
-
check_no_override_mark(test)
|
299
|
-
override = CaseOverride(
|
300
|
-
query=query or {}, headers=headers or {}, cookies=cookies or {}, path_parameters=path_parameters or {}
|
301
|
-
)
|
302
|
-
set_override_mark(test, override)
|
303
|
-
return test
|
304
|
-
|
305
|
-
return _add_override
|
306
|
-
|
307
|
-
|
308
|
-
def _copy_marks(source: Callable, target: Callable) -> None:
|
309
|
-
marks = getattr(source, "pytestmark", [])
|
310
|
-
# Pytest adds this attribute in `usefixtures`
|
311
|
-
target.pytestmark.extend(marks) # type: ignore
|
312
|
-
|
313
|
-
|
314
|
-
def _get_capturemanager(request: FixtureRequest) -> Generator | Type[nullcontext]:
|
315
|
-
capturemanager = request.node.config.pluginmanager.get_plugin("capturemanager")
|
316
|
-
if capturemanager is not None:
|
317
|
-
return capturemanager.global_and_fixture_disabled
|
318
|
-
return nullcontext
|
319
|
-
|
320
|
-
|
321
|
-
def _get_node_name(node_id: str, operation: APIOperation) -> str:
|
322
|
-
"""Make a test node name. For example: test_api[GET /users]."""
|
323
|
-
return f"{node_id}[{operation.method.upper()} {operation.full_path}]"
|
324
|
-
|
325
|
-
|
326
|
-
def _get_partial_node_name(node_id: str, **kwargs: Any) -> str:
|
327
|
-
"""Make a test node name for failing tests caused by schema errors."""
|
328
|
-
name = node_id
|
329
|
-
if "method" in kwargs:
|
330
|
-
name += f"[{kwargs['method']} {kwargs['path']}]"
|
331
|
-
else:
|
332
|
-
name += f"[{kwargs['path']}]"
|
333
|
-
return name
|
334
|
-
|
335
|
-
|
336
|
-
def run_subtest(
|
337
|
-
operation: APIOperation,
|
338
|
-
fixtures: dict[str, Any],
|
339
|
-
sub_test: Callable,
|
340
|
-
subtests: SubTests,
|
341
|
-
) -> None:
|
342
|
-
"""Run the given subtest with pytest fixtures."""
|
343
|
-
__tracebackhide__ = True
|
344
|
-
|
345
|
-
# Deduplicate found checks in case of Hypothesis finding multiple of them
|
346
|
-
failed_checks = {}
|
347
|
-
exceptions = []
|
348
|
-
inner_test = sub_test.hypothesis.inner_test # type: ignore
|
349
|
-
|
350
|
-
@impersonate(inner_test) # type: ignore
|
351
|
-
def collecting_wrapper(*args: Any, **kwargs: Any) -> None:
|
352
|
-
__tracebackhide__ = True
|
353
|
-
try:
|
354
|
-
inner_test(*args, **kwargs)
|
355
|
-
except CheckFailed as failed:
|
356
|
-
failed_checks[failed.__class__] = failed
|
357
|
-
raise failed
|
358
|
-
except Exception as exception:
|
359
|
-
# Deduplicate it later, as it is more costly than for `CheckFailed`
|
360
|
-
exceptions.append(exception)
|
361
|
-
raise
|
362
|
-
|
363
|
-
def get_exception_class() -> type[CheckFailed]:
|
364
|
-
return get_grouped_exception("Lazy", *failed_checks.values())
|
365
|
-
|
366
|
-
sub_test.hypothesis.inner_test = collecting_wrapper # type: ignore
|
367
|
-
|
368
|
-
with subtests.test(verbose_name=operation.verbose_name):
|
369
|
-
try:
|
370
|
-
sub_test(**fixtures)
|
371
|
-
except SkipTest as exc:
|
372
|
-
pytest.skip(exc.args[0])
|
373
|
-
except (MultipleFailures, CheckFailed) as exc:
|
374
|
-
# Hypothesis doesn't report the underlying failures in these circumstances, hence we display them manually
|
375
|
-
exc_class = get_exception_class()
|
376
|
-
failures = "".join(f"{SEPARATOR} {failure.args[0]}" for failure in failed_checks.values())
|
377
|
-
unique_exceptions = {get_interesting_origin(exception): exception for exception in exceptions}
|
378
|
-
total_problems = len(failed_checks) + len(unique_exceptions)
|
379
|
-
if total_problems == 1:
|
380
|
-
raise
|
381
|
-
message = f"Schemathesis found {total_problems} distinct sets of failures.{failures}"
|
382
|
-
for exception in unique_exceptions.values():
|
383
|
-
# Non-check exceptions
|
384
|
-
message += f"{SEPARATOR}\n\n"
|
385
|
-
tb = get_trimmed_traceback(exception)
|
386
|
-
message += format_exception(exception, tb)
|
387
|
-
raise exc_class(message, causes=tuple(failed_checks.values())).with_traceback(exc.__traceback__) from None
|
388
|
-
except Flaky as exc:
|
389
|
-
exc_class = get_exception_class()
|
390
|
-
failure = next(iter(failed_checks.values()))
|
391
|
-
message = f"{FLAKY_FAILURE_MESSAGE}{failure}"
|
392
|
-
# The outer frame is the one for user's test function, take it as the root one
|
393
|
-
traceback = exc.__traceback__.tb_next
|
394
|
-
# The next one comes from Hypothesis internals - remove it
|
395
|
-
traceback.tb_next = None
|
396
|
-
raise exc_class(message, causes=tuple(failed_checks.values())).with_traceback(traceback) from None
|
397
|
-
|
398
|
-
|
399
|
-
SEPARATOR = "\n===================="
|
400
|
-
|
401
|
-
|
402
|
-
def _schema_error(subtests: SubTests, error: OperationSchemaError, node_id: str) -> None:
|
403
|
-
"""Run a failing test, that will show the underlying problem."""
|
404
|
-
sub_test = error.as_failing_test_function()
|
405
|
-
# `full_path` is always available in this case
|
406
|
-
kwargs = {"path": error.full_path}
|
407
|
-
if error.method:
|
408
|
-
kwargs["method"] = error.method.upper()
|
409
|
-
subtests.item._nodeid = _get_partial_node_name(node_id, **kwargs)
|
410
|
-
__tracebackhide__ = True
|
411
|
-
with subtests.test(**kwargs):
|
412
|
-
sub_test()
|
413
|
-
|
414
|
-
|
415
|
-
def get_schema(
|
416
|
-
*,
|
417
|
-
request: FixtureRequest,
|
418
|
-
name: str,
|
419
|
-
base_url: str | None | NotSet = None,
|
420
|
-
method: Filter | None = None,
|
421
|
-
endpoint: Filter | None = None,
|
422
|
-
tag: Filter | None = None,
|
423
|
-
operation_id: Filter | None = None,
|
424
|
-
filter_set: FilterSet,
|
425
|
-
app: Any = None,
|
426
|
-
test_function: GenericTest,
|
427
|
-
hooks: HookDispatcher,
|
428
|
-
auth: AuthStorage | NotSet,
|
429
|
-
validate_schema: bool | NotSet = NOT_SET,
|
430
|
-
skip_deprecated_operations: bool | NotSet = NOT_SET,
|
431
|
-
data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
|
432
|
-
generation_config: GenerationConfig | NotSet = NOT_SET,
|
433
|
-
output_config: OutputConfig | NotSet = NOT_SET,
|
434
|
-
code_sample_style: CodeSampleStyle,
|
435
|
-
rate_limiter: Limiter | None,
|
436
|
-
sanitize_output: bool,
|
437
|
-
) -> BaseSchema:
|
438
|
-
"""Loads a schema from the fixture."""
|
439
|
-
schema = request.getfixturevalue(name)
|
440
|
-
if not isinstance(schema, BaseSchema):
|
441
|
-
raise ValueError(f"The given schema must be an instance of BaseSchema, got: {type(schema)}")
|
442
|
-
|
443
|
-
filter_set = filter_set_from_components(
|
444
|
-
include=True,
|
445
|
-
method=method,
|
446
|
-
endpoint=endpoint,
|
447
|
-
tag=tag,
|
448
|
-
operation_id=operation_id,
|
449
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
450
|
-
parent=schema.filter_set.merge(filter_set),
|
451
|
-
)
|
452
|
-
return schema.clone(
|
453
|
-
base_url=base_url,
|
454
|
-
filter_set=filter_set,
|
455
|
-
app=app,
|
456
|
-
test_function=test_function,
|
457
|
-
hooks=schema.hooks.merge(hooks),
|
458
|
-
auth=auth,
|
459
|
-
validate_schema=validate_schema,
|
460
|
-
data_generation_methods=data_generation_methods,
|
461
|
-
generation_config=generation_config,
|
462
|
-
output_config=output_config,
|
463
|
-
code_sample_style=code_sample_style,
|
464
|
-
rate_limiter=rate_limiter,
|
465
|
-
sanitize_output=sanitize_output,
|
466
|
-
)
|
467
|
-
|
468
|
-
|
469
|
-
def get_fixtures(func: Callable, request: FixtureRequest, given_kwargs: dict[str, Any]) -> dict[str, Any]:
|
470
|
-
"""Load fixtures, needed for the test function."""
|
471
|
-
sig = signature(func)
|
472
|
-
return {
|
473
|
-
name: request.getfixturevalue(name) for name in sig.parameters if name != "case" and name not in given_kwargs
|
474
|
-
}
|
schemathesis/loaders.py
DELETED
@@ -1,122 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import re
|
4
|
-
import sys
|
5
|
-
from functools import lru_cache
|
6
|
-
from typing import TYPE_CHECKING, Any, BinaryIO, Callable, TextIO, TypeVar
|
7
|
-
|
8
|
-
from .exceptions import SchemaError, SchemaErrorType, extract_requests_exception_details
|
9
|
-
|
10
|
-
if TYPE_CHECKING:
|
11
|
-
import yaml
|
12
|
-
|
13
|
-
from .transports.responses import GenericResponse
|
14
|
-
|
15
|
-
R = TypeVar("R", bound="GenericResponse")
|
16
|
-
|
17
|
-
|
18
|
-
def load_schema_from_url(loader: Callable[[], R]) -> R:
|
19
|
-
import requests
|
20
|
-
|
21
|
-
try:
|
22
|
-
response = loader()
|
23
|
-
except requests.RequestException as exc:
|
24
|
-
url = exc.request.url if exc.request is not None else None
|
25
|
-
if isinstance(exc, requests.exceptions.SSLError):
|
26
|
-
type_ = SchemaErrorType.CONNECTION_SSL
|
27
|
-
elif isinstance(exc, requests.exceptions.ConnectionError):
|
28
|
-
type_ = SchemaErrorType.CONNECTION_OTHER
|
29
|
-
else:
|
30
|
-
type_ = SchemaErrorType.NETWORK_OTHER
|
31
|
-
message, extras = extract_requests_exception_details(exc)
|
32
|
-
raise SchemaError(message=message, type=type_, url=url, response=exc.response, extras=extras) from exc
|
33
|
-
_raise_for_status(response)
|
34
|
-
return response
|
35
|
-
|
36
|
-
|
37
|
-
def _raise_for_status(response: GenericResponse) -> None:
|
38
|
-
from .transports.responses import get_reason
|
39
|
-
|
40
|
-
status_code = response.status_code
|
41
|
-
reason = get_reason(status_code)
|
42
|
-
if status_code >= 500:
|
43
|
-
message = f"Failed to load schema due to server error (HTTP {status_code} {reason})"
|
44
|
-
type_ = SchemaErrorType.HTTP_SERVER_ERROR
|
45
|
-
elif status_code >= 400:
|
46
|
-
message = f"Failed to load schema due to client error (HTTP {status_code} {reason})"
|
47
|
-
if status_code == 403:
|
48
|
-
type_ = SchemaErrorType.HTTP_FORBIDDEN
|
49
|
-
elif status_code == 404:
|
50
|
-
type_ = SchemaErrorType.HTTP_NOT_FOUND
|
51
|
-
else:
|
52
|
-
type_ = SchemaErrorType.HTTP_CLIENT_ERROR
|
53
|
-
else:
|
54
|
-
return
|
55
|
-
raise SchemaError(message=message, type=type_, url=response.request.url, response=response, extras=[])
|
56
|
-
|
57
|
-
|
58
|
-
def load_app(path: str) -> Any:
|
59
|
-
"""Import an application from a string."""
|
60
|
-
path, name = ([*re.split(":(?![\\\\/])", path, maxsplit=1), ""])[:2]
|
61
|
-
__import__(path)
|
62
|
-
# accessing the module from sys.modules returns a proper module, while `__import__`
|
63
|
-
# may return a parent module (system dependent)
|
64
|
-
module = sys.modules[path]
|
65
|
-
return getattr(module, name)
|
66
|
-
|
67
|
-
|
68
|
-
@lru_cache
|
69
|
-
def get_yaml_loader() -> type[yaml.SafeLoader]:
|
70
|
-
"""Create a YAML loader, that doesn't parse specific tokens into Python objects."""
|
71
|
-
import yaml
|
72
|
-
|
73
|
-
try:
|
74
|
-
from yaml import CSafeLoader as SafeLoader
|
75
|
-
except ImportError:
|
76
|
-
from yaml import SafeLoader # type: ignore
|
77
|
-
|
78
|
-
cls: type[yaml.SafeLoader] = type("YAMLLoader", (SafeLoader,), {})
|
79
|
-
cls.yaml_implicit_resolvers = {
|
80
|
-
key: [(tag, regexp) for tag, regexp in mapping if tag != "tag:yaml.org,2002:timestamp"]
|
81
|
-
for key, mapping in cls.yaml_implicit_resolvers.copy().items()
|
82
|
-
}
|
83
|
-
|
84
|
-
# Fix pyyaml scientific notation parse bug
|
85
|
-
# See PR: https://github.com/yaml/pyyaml/pull/174 for upstream fix
|
86
|
-
cls.add_implicit_resolver( # type: ignore
|
87
|
-
"tag:yaml.org,2002:float",
|
88
|
-
re.compile(
|
89
|
-
r"""^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+]?[0-9]+)?
|
90
|
-
|[-+]?(?:[0-9][0-9_]*)(?:[eE][-+]?[0-9]+)
|
91
|
-
|\.[0-9_]+(?:[eE][-+]?[0-9]+)?
|
92
|
-
|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*
|
93
|
-
|[-+]?\.(?:inf|Inf|INF)
|
94
|
-
|\.(?:nan|NaN|NAN))$""",
|
95
|
-
re.VERBOSE,
|
96
|
-
),
|
97
|
-
list("-+0123456789."),
|
98
|
-
)
|
99
|
-
|
100
|
-
def construct_mapping(self: SafeLoader, node: yaml.Node, deep: bool = False) -> dict[str, Any]:
|
101
|
-
if isinstance(node, yaml.MappingNode):
|
102
|
-
self.flatten_mapping(node) # type: ignore
|
103
|
-
mapping = {}
|
104
|
-
for key_node, value_node in node.value:
|
105
|
-
# If the key has a tag different from `str` - use its string value.
|
106
|
-
# With this change all integer keys or YAML 1.1 boolean-ish values like "on" / "off" will not be cast to
|
107
|
-
# a different type
|
108
|
-
if key_node.tag != "tag:yaml.org,2002:str":
|
109
|
-
key = key_node.value
|
110
|
-
else:
|
111
|
-
key = self.construct_object(key_node, deep) # type: ignore
|
112
|
-
mapping[key] = self.construct_object(value_node, deep) # type: ignore
|
113
|
-
return mapping
|
114
|
-
|
115
|
-
cls.construct_mapping = construct_mapping # type: ignore
|
116
|
-
return cls
|
117
|
-
|
118
|
-
|
119
|
-
def load_yaml(stream: str | bytes | TextIO | BinaryIO) -> Any:
|
120
|
-
import yaml
|
121
|
-
|
122
|
-
return yaml.load(stream, get_yaml_loader())
|