schemathesis 3.39.15__py3-none-any.whl → 4.0.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.
- schemathesis/__init__.py +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -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/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -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 +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -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/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- 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 +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +238 -308
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -712
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- 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 -920
- 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 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- 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/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- 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 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- 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 -88
- schemathesis/runner/impl/core.py +0 -1280
- 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/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- 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/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- 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.15.dist-info/METADATA +0 -293
- schemathesis-3.39.15.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.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -1,27 +0,0 @@
|
|
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
|
@@ -1,36 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from typing import Any, Callable, Dict, List, Union, overload
|
4
|
-
|
5
|
-
JsonValue = Union[Dict[str, Any], List, str, float, int]
|
6
|
-
|
7
|
-
|
8
|
-
@overload
|
9
|
-
def traverse_schema(schema: dict[str, Any], callback: Callable, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
10
|
-
pass
|
11
|
-
|
12
|
-
|
13
|
-
@overload
|
14
|
-
def traverse_schema(schema: list, callback: Callable, *args: Any, **kwargs: Any) -> list:
|
15
|
-
pass
|
16
|
-
|
17
|
-
|
18
|
-
@overload
|
19
|
-
def traverse_schema(schema: str, callback: Callable, *args: Any, **kwargs: Any) -> str:
|
20
|
-
pass
|
21
|
-
|
22
|
-
|
23
|
-
@overload
|
24
|
-
def traverse_schema(schema: float, callback: Callable, *args: Any, **kwargs: Any) -> float:
|
25
|
-
pass
|
26
|
-
|
27
|
-
|
28
|
-
def traverse_schema(schema: JsonValue, callback: Callable[..., dict[str, Any]], *args: Any, **kwargs: Any) -> JsonValue:
|
29
|
-
"""Apply callback recursively to the given schema."""
|
30
|
-
if isinstance(schema, dict):
|
31
|
-
schema = callback(schema, *args, **kwargs)
|
32
|
-
for key, sub_item in schema.items():
|
33
|
-
schema[key] = traverse_schema(sub_item, callback, *args, **kwargs)
|
34
|
-
elif isinstance(schema, list):
|
35
|
-
schema = [traverse_schema(sub_item, callback, *args, **kwargs) for sub_item in schema]
|
36
|
-
return schema
|
schemathesis/internal/output.py
DELETED
@@ -1,68 +0,0 @@
|
|
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
|
@@ -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
|
-
}
|