schemathesis 3.15.4__py3-none-any.whl → 4.4.2__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 +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1219
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -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 +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +682 -257
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +26 -2
- schemathesis/specs/graphql/scalars.py +77 -12
- schemathesis/specs/graphql/schemas.py +367 -148
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +555 -318
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +748 -82
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +93 -73
- schemathesis/specs/openapi/negative/mutations.py +294 -103
- schemathesis/specs/openapi/negative/utils.py +0 -9
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +647 -666
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +403 -68
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -57
- schemathesis/_hypothesis.py +0 -123
- schemathesis/auth.py +0 -214
- schemathesis/cli/callbacks.py +0 -240
- schemathesis/cli/cassettes.py +0 -351
- schemathesis/cli/context.py +0 -38
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -70
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -521
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -88
- schemathesis/exceptions.py +0 -257
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -251
- schemathesis/failures.py +0 -145
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/graphql.py +0 -5
- schemathesis/internal.py +0 -6
- schemathesis/lazy.py +0 -301
- schemathesis/models.py +0 -1113
- schemathesis/parameters.py +0 -91
- schemathesis/runner/__init__.py +0 -470
- schemathesis/runner/events.py +0 -242
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -791
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -206
- schemathesis/serializers.py +0 -253
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -10
- schemathesis/service/client.py +0 -62
- schemathesis/service/constants.py +0 -25
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -46
- schemathesis/service/hosts.py +0 -74
- schemathesis/service/metadata.py +0 -42
- schemathesis/service/models.py +0 -21
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/worker.py +0 -39
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -303
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -430
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -358
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -475
- schemathesis-3.15.4.dist-info/METADATA +0 -202
- schemathesis-3.15.4.dist-info/RECORD +0 -99
- schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, fields, is_dataclass
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T", bound="DiffBase")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class DiffBase:
|
|
11
|
+
def __repr__(self) -> str:
|
|
12
|
+
"""Show only the fields that differ from the default."""
|
|
13
|
+
assert is_dataclass(self)
|
|
14
|
+
default = self.__class__()
|
|
15
|
+
diffs = []
|
|
16
|
+
for field in fields(self):
|
|
17
|
+
name = field.name
|
|
18
|
+
if name.startswith("_") and name not in ("_seed", "_filter_set"):
|
|
19
|
+
continue
|
|
20
|
+
current_value = getattr(self, name)
|
|
21
|
+
default_value = getattr(default, name)
|
|
22
|
+
if name == "_seed":
|
|
23
|
+
name = "seed"
|
|
24
|
+
if name == "_filter_set":
|
|
25
|
+
name = "filter_set"
|
|
26
|
+
if name == "rate_limit" and current_value is not None:
|
|
27
|
+
assert hasattr(self, "_rate_limit")
|
|
28
|
+
current_value = self._rate_limit
|
|
29
|
+
if self._has_diff(current_value, default_value):
|
|
30
|
+
diffs.append(f"{name}={self._diff_repr(current_value, default_value)}")
|
|
31
|
+
return f"{self.__class__.__name__}({', '.join(diffs)})"
|
|
32
|
+
|
|
33
|
+
def _has_diff(self, value: object, default: object) -> bool:
|
|
34
|
+
if is_dataclass(value):
|
|
35
|
+
return repr(value) != repr(default)
|
|
36
|
+
if isinstance(value, list) and isinstance(default, list):
|
|
37
|
+
if len(value) != len(default):
|
|
38
|
+
return True
|
|
39
|
+
return any(self._has_diff(v, d) for v, d in zip(value, default))
|
|
40
|
+
if isinstance(value, dict) and isinstance(default, dict):
|
|
41
|
+
if set(value.keys()) != set(default.keys()):
|
|
42
|
+
return True
|
|
43
|
+
return any(self._has_diff(value[k], default[k]) for k in value)
|
|
44
|
+
return value != default
|
|
45
|
+
|
|
46
|
+
def _diff_repr(self, value: object, default: object) -> str:
|
|
47
|
+
if is_dataclass(value):
|
|
48
|
+
# If the nested object is a dataclass, recursively show its diff.
|
|
49
|
+
return repr(value)
|
|
50
|
+
if isinstance(value, list) and isinstance(default, list):
|
|
51
|
+
diff_items = []
|
|
52
|
+
# Compare items pairwise.
|
|
53
|
+
for v, d in zip(value, default):
|
|
54
|
+
if self._has_diff(v, d):
|
|
55
|
+
diff_items.append(self._diff_repr(v, d))
|
|
56
|
+
# Include any extra items in value.
|
|
57
|
+
if len(value) > len(default):
|
|
58
|
+
diff_items.extend(_repr(item) for item in value[len(default) :])
|
|
59
|
+
return f"[{', '.join(_repr(item) for item in value)}]"
|
|
60
|
+
if isinstance(value, dict) and isinstance(default, dict):
|
|
61
|
+
diff_items = []
|
|
62
|
+
for k, v in value.items():
|
|
63
|
+
d = default.get(k)
|
|
64
|
+
if self._has_diff(v, d):
|
|
65
|
+
diff_items.append(f"{k!r}: {self._diff_repr(v, d)}")
|
|
66
|
+
return f"{{{', '.join(diff_items)}}}"
|
|
67
|
+
return repr(value)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_hierarchy(cls, configs: list[T]) -> T:
|
|
71
|
+
# This config will accumulate "merged" config options
|
|
72
|
+
if len(configs) == 1:
|
|
73
|
+
return configs[0]
|
|
74
|
+
output = cls()
|
|
75
|
+
for option in cls.__slots__: # type: ignore[attr-defined]
|
|
76
|
+
if option.startswith("_"):
|
|
77
|
+
continue
|
|
78
|
+
default = getattr(output, option)
|
|
79
|
+
if hasattr(default, "__dataclass_fields__"):
|
|
80
|
+
# Sub-configs require merging of nested config options
|
|
81
|
+
sub_configs = [getattr(config, option) for config in configs]
|
|
82
|
+
merged = type(default).from_hierarchy(sub_configs)
|
|
83
|
+
setattr(output, option, merged)
|
|
84
|
+
else:
|
|
85
|
+
# Primitive config options can be compared directly and do not
|
|
86
|
+
# require merging of nested options
|
|
87
|
+
for config in configs:
|
|
88
|
+
current = getattr(config, option)
|
|
89
|
+
if current != default:
|
|
90
|
+
setattr(output, option, current)
|
|
91
|
+
# As we go from the highest priority to the lowest one,
|
|
92
|
+
# we can just stop on the first non-default value
|
|
93
|
+
break
|
|
94
|
+
return output # type: ignore[return-value]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _repr(item: object) -> str:
|
|
98
|
+
if callable(item) and hasattr(item, "__name__"):
|
|
99
|
+
return f"<function {item.__name__}>"
|
|
100
|
+
|
|
101
|
+
return repr(item)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from string import Template
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from schemathesis.config._error import ConfigError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def resolve(value: Any) -> Any:
|
|
11
|
+
"""Resolve environment variables using string templates."""
|
|
12
|
+
if value is None:
|
|
13
|
+
return None
|
|
14
|
+
if not isinstance(value, str):
|
|
15
|
+
return value
|
|
16
|
+
try:
|
|
17
|
+
return Template(value).substitute(os.environ)
|
|
18
|
+
except ValueError:
|
|
19
|
+
raise ConfigError(f"Invalid placeholder in string: `{value}`") from None
|
|
20
|
+
except KeyError:
|
|
21
|
+
raise ConfigError(f"Missing environment variable: `{value}`") from None
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from schemathesis.core.errors import SchemathesisError
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from jsonschema import ValidationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConfigError(SchemathesisError):
|
|
13
|
+
"""Invalid configuration."""
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_validation_error(cls, error: ValidationError) -> ConfigError:
|
|
17
|
+
message = error.message
|
|
18
|
+
if error.validator == "enum":
|
|
19
|
+
message = _format_enum_error(error)
|
|
20
|
+
elif error.validator == "minimum":
|
|
21
|
+
message = _format_minimum_error(error)
|
|
22
|
+
elif error.validator == "required":
|
|
23
|
+
message = _format_required_error(error)
|
|
24
|
+
elif error.validator == "type":
|
|
25
|
+
message = _format_type_error(error)
|
|
26
|
+
elif error.validator == "additionalProperties":
|
|
27
|
+
message = _format_additional_properties_error(error)
|
|
28
|
+
elif error.validator == "anyOf":
|
|
29
|
+
message = _format_anyof_error(error)
|
|
30
|
+
return cls(message)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _format_minimum_error(error: ValidationError) -> str:
|
|
34
|
+
assert isinstance(error.validator_value, (int, float))
|
|
35
|
+
section = path_to_section_name(list(error.path)[:-1] if error.path else [])
|
|
36
|
+
assert error.path
|
|
37
|
+
|
|
38
|
+
prop_name = error.path[-1]
|
|
39
|
+
min_value = error.validator_value
|
|
40
|
+
actual_value = error.instance
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
f"Error in {section} section:\n Value too low:\n\n"
|
|
44
|
+
f" - '{prop_name}' → Must be at least {min_value}, but got {actual_value}."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _format_required_error(error: ValidationError) -> str:
|
|
49
|
+
assert isinstance(error.validator_value, list)
|
|
50
|
+
missing_keys = sorted(set(error.validator_value) - set(error.instance))
|
|
51
|
+
|
|
52
|
+
section = path_to_section_name(list(error.path))
|
|
53
|
+
|
|
54
|
+
details = "\n".join(f" - '{key}'" for key in missing_keys)
|
|
55
|
+
return f"Error in {section} section:\n Missing required properties:\n\n{details}\n\n"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _format_enum_error(error: ValidationError) -> str:
|
|
59
|
+
assert isinstance(error.validator_value, list)
|
|
60
|
+
valid_values = sorted(error.validator_value)
|
|
61
|
+
|
|
62
|
+
path = list(error.path)
|
|
63
|
+
|
|
64
|
+
if path and isinstance(path[-1], int):
|
|
65
|
+
idx = path[-1]
|
|
66
|
+
prop_name = path[-2]
|
|
67
|
+
section_path = path[:-2]
|
|
68
|
+
description = f"Item #{idx} in the '{prop_name}' array"
|
|
69
|
+
else:
|
|
70
|
+
prop_name = path[-1] if path else "value"
|
|
71
|
+
section_path = path[:-1]
|
|
72
|
+
description = f"'{prop_name}'"
|
|
73
|
+
|
|
74
|
+
suggestion = ""
|
|
75
|
+
if isinstance(error.instance, str) and all(isinstance(v, str) for v in valid_values):
|
|
76
|
+
match = _find_closest_match(error.instance, valid_values)
|
|
77
|
+
if match:
|
|
78
|
+
suggestion = f" Did you mean '{match}'?"
|
|
79
|
+
|
|
80
|
+
section = path_to_section_name(section_path)
|
|
81
|
+
valid_values_str = ", ".join(repr(v) for v in valid_values)
|
|
82
|
+
return (
|
|
83
|
+
f"Error in {section} section:\n Invalid value:\n\n"
|
|
84
|
+
f" - {description} → '{error.instance}' is not a valid value.{suggestion}\n\n"
|
|
85
|
+
f"Valid values are: {valid_values_str}."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _format_type_error(error: ValidationError) -> str:
|
|
90
|
+
expected = error.validator_value
|
|
91
|
+
assert isinstance(expected, (str, list))
|
|
92
|
+
section = path_to_section_name(list(error.path)[:-1] if error.path else [])
|
|
93
|
+
assert error.path
|
|
94
|
+
|
|
95
|
+
type_phrases = {
|
|
96
|
+
"object": "an object",
|
|
97
|
+
"array": "an array",
|
|
98
|
+
"number": "a number",
|
|
99
|
+
"boolean": "a boolean",
|
|
100
|
+
"string": "a string",
|
|
101
|
+
"integer": "an integer",
|
|
102
|
+
"null": "null",
|
|
103
|
+
}
|
|
104
|
+
message = f"Error in {section} section:\n Type error:\n\n - '{error.path[-1]}' → Must be "
|
|
105
|
+
|
|
106
|
+
if isinstance(expected, list):
|
|
107
|
+
message += f"one of: {' or '.join(expected)}"
|
|
108
|
+
else:
|
|
109
|
+
message += type_phrases[expected]
|
|
110
|
+
actual = type(error.instance).__name__
|
|
111
|
+
message += f", but got {actual}: {error.instance}"
|
|
112
|
+
return message
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _format_additional_properties_error(error: ValidationError) -> str:
|
|
116
|
+
valid = list(error.schema.get("properties", {}))
|
|
117
|
+
unknown = sorted(set(error.instance) - set(valid))
|
|
118
|
+
valid_list = ", ".join(f"'{prop}'" for prop in valid)
|
|
119
|
+
section = path_to_section_name(list(error.path))
|
|
120
|
+
|
|
121
|
+
details = []
|
|
122
|
+
for prop in unknown:
|
|
123
|
+
match = _find_closest_match(prop, valid)
|
|
124
|
+
if match:
|
|
125
|
+
details.append(f"- '{prop}' → Did you mean '{match}'?")
|
|
126
|
+
else:
|
|
127
|
+
details.append(f"- '{prop}'")
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
f"Error in {section} section:\n Unknown properties:\n\n"
|
|
131
|
+
+ "\n".join(f" {detail}" for detail in details)
|
|
132
|
+
+ f"\n\nValid properties for {section} are: {valid_list}."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _format_anyof_error(error: ValidationError) -> str:
|
|
137
|
+
if list(error.schema_path) == ["properties", "operations", "items", "anyOf"]:
|
|
138
|
+
section = path_to_section_name(list(error.path))
|
|
139
|
+
return (
|
|
140
|
+
f"Error in {section} section:\n At least one filter is required when defining [[operations]].\n\n"
|
|
141
|
+
"Please specify at least one include or exclude filter property (e.g., include-path, exclude-tag, etc.)."
|
|
142
|
+
)
|
|
143
|
+
elif list(error.schema_path) == ["properties", "workers", "anyOf"]:
|
|
144
|
+
return (
|
|
145
|
+
f"Invalid value for 'workers': {error.instance!r}\n\n"
|
|
146
|
+
f"Expected either:\n"
|
|
147
|
+
f" - A positive integer (e.g., workers = 4)\n"
|
|
148
|
+
f' - The string "auto" for automatic detection (workers = "auto")'
|
|
149
|
+
)
|
|
150
|
+
return error.message
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def path_to_section_name(path: list[int | str]) -> str:
|
|
154
|
+
"""Convert a JSON path to a TOML-like section name."""
|
|
155
|
+
if not path:
|
|
156
|
+
return "root"
|
|
157
|
+
|
|
158
|
+
return f"[{'.'.join(str(p) for p in path)}]"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _find_closest_match(value: str, variants: list[str]) -> str | None:
|
|
162
|
+
matches = difflib.get_close_matches(value, variants, n=1, cutoff=0.6)
|
|
163
|
+
return matches[0] if matches else None
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from schemathesis.config._diff_base import DiffBase
|
|
7
|
+
from schemathesis.generation.modes import GenerationMode
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from schemathesis.generation.metrics import MetricFunction
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(repr=False)
|
|
14
|
+
class GenerationConfig(DiffBase):
|
|
15
|
+
modes: list[GenerationMode]
|
|
16
|
+
max_examples: int | None
|
|
17
|
+
no_shrink: bool
|
|
18
|
+
deterministic: bool
|
|
19
|
+
# Allow generating `\x00` bytes in strings
|
|
20
|
+
allow_x00: bool
|
|
21
|
+
# Allow generating unexpected parameters in generated requests
|
|
22
|
+
allow_extra_parameters: bool
|
|
23
|
+
# Generate strings using the given codec
|
|
24
|
+
codec: str | None
|
|
25
|
+
maximize: list[MetricFunction]
|
|
26
|
+
# Whether to generate security parameters
|
|
27
|
+
with_security_parameters: bool
|
|
28
|
+
# Allowing using `null` for optional arguments in GraphQL queries
|
|
29
|
+
graphql_allow_null: bool
|
|
30
|
+
database: str | None
|
|
31
|
+
unique_inputs: bool
|
|
32
|
+
exclude_header_characters: str | None
|
|
33
|
+
|
|
34
|
+
__slots__ = (
|
|
35
|
+
"modes",
|
|
36
|
+
"max_examples",
|
|
37
|
+
"no_shrink",
|
|
38
|
+
"deterministic",
|
|
39
|
+
"allow_x00",
|
|
40
|
+
"allow_extra_parameters",
|
|
41
|
+
"codec",
|
|
42
|
+
"maximize",
|
|
43
|
+
"with_security_parameters",
|
|
44
|
+
"graphql_allow_null",
|
|
45
|
+
"database",
|
|
46
|
+
"unique_inputs",
|
|
47
|
+
"exclude_header_characters",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
modes: list[GenerationMode] | None = None,
|
|
54
|
+
max_examples: int | None = None,
|
|
55
|
+
no_shrink: bool = False,
|
|
56
|
+
deterministic: bool = False,
|
|
57
|
+
allow_x00: bool = True,
|
|
58
|
+
allow_extra_parameters: bool = True,
|
|
59
|
+
codec: str | None = "utf-8",
|
|
60
|
+
maximize: list[MetricFunction] | None = None,
|
|
61
|
+
with_security_parameters: bool = True,
|
|
62
|
+
graphql_allow_null: bool = True,
|
|
63
|
+
database: str | None = None,
|
|
64
|
+
unique_inputs: bool = False,
|
|
65
|
+
exclude_header_characters: str | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
from schemathesis.generation import GenerationMode
|
|
68
|
+
|
|
69
|
+
self.modes = modes or list(GenerationMode)
|
|
70
|
+
self.max_examples = max_examples
|
|
71
|
+
self.no_shrink = no_shrink
|
|
72
|
+
self.deterministic = deterministic
|
|
73
|
+
self.allow_x00 = allow_x00
|
|
74
|
+
self.allow_extra_parameters = allow_extra_parameters
|
|
75
|
+
self.codec = codec
|
|
76
|
+
self.maximize = maximize or []
|
|
77
|
+
self.with_security_parameters = with_security_parameters
|
|
78
|
+
self.graphql_allow_null = graphql_allow_null
|
|
79
|
+
self.database = database
|
|
80
|
+
self.unique_inputs = unique_inputs
|
|
81
|
+
self.exclude_header_characters = exclude_header_characters
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_dict(cls, data: dict[str, Any]) -> GenerationConfig:
|
|
85
|
+
mode_raw = data.get("mode")
|
|
86
|
+
if mode_raw == "all":
|
|
87
|
+
modes = list(GenerationMode)
|
|
88
|
+
elif mode_raw is not None:
|
|
89
|
+
modes = [GenerationMode(mode_raw)]
|
|
90
|
+
else:
|
|
91
|
+
modes = None
|
|
92
|
+
maximize = _get_maximize(data.get("maximize"))
|
|
93
|
+
return cls(
|
|
94
|
+
modes=modes,
|
|
95
|
+
max_examples=data.get("max-examples"),
|
|
96
|
+
no_shrink=data.get("no-shrink", False),
|
|
97
|
+
deterministic=data.get("deterministic", False),
|
|
98
|
+
allow_x00=data.get("allow-x00", True),
|
|
99
|
+
allow_extra_parameters=data.get("allow-extra-parameters", True),
|
|
100
|
+
codec=data.get("codec", "utf-8"),
|
|
101
|
+
maximize=maximize,
|
|
102
|
+
with_security_parameters=data.get("with-security-parameters", True),
|
|
103
|
+
graphql_allow_null=data.get("graphql-allow-null", True),
|
|
104
|
+
database=data.get("database"),
|
|
105
|
+
unique_inputs=data.get("unique-inputs", False),
|
|
106
|
+
exclude_header_characters=data.get("exclude-header-characters"),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def update(
|
|
110
|
+
self,
|
|
111
|
+
*,
|
|
112
|
+
modes: list[GenerationMode] | None = None,
|
|
113
|
+
max_examples: int | None = None,
|
|
114
|
+
no_shrink: bool | None = None,
|
|
115
|
+
deterministic: bool | None = None,
|
|
116
|
+
allow_x00: bool | None = None,
|
|
117
|
+
allow_extra_parameters: bool | None = None,
|
|
118
|
+
codec: str | None = None,
|
|
119
|
+
maximize: list[MetricFunction] | None = None,
|
|
120
|
+
with_security_parameters: bool | None = None,
|
|
121
|
+
graphql_allow_null: bool | None = None,
|
|
122
|
+
database: str | None = None,
|
|
123
|
+
unique_inputs: bool | None = None,
|
|
124
|
+
exclude_header_characters: str | None = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
if modes is not None:
|
|
127
|
+
self.modes = modes
|
|
128
|
+
if max_examples is not None:
|
|
129
|
+
self.max_examples = max_examples
|
|
130
|
+
self.no_shrink = no_shrink or False
|
|
131
|
+
self.deterministic = deterministic or False
|
|
132
|
+
self.allow_x00 = allow_x00 if allow_x00 is not None else True
|
|
133
|
+
self.allow_extra_parameters = allow_extra_parameters if allow_extra_parameters is not None else True
|
|
134
|
+
if codec is not None:
|
|
135
|
+
self.codec = codec
|
|
136
|
+
if maximize is not None:
|
|
137
|
+
self.maximize = maximize
|
|
138
|
+
if with_security_parameters is not None:
|
|
139
|
+
self.with_security_parameters = with_security_parameters
|
|
140
|
+
self.graphql_allow_null = graphql_allow_null if graphql_allow_null is not None else True
|
|
141
|
+
if database is not None:
|
|
142
|
+
self.database = database
|
|
143
|
+
self.unique_inputs = unique_inputs or False
|
|
144
|
+
if exclude_header_characters is not None:
|
|
145
|
+
self.exclude_header_characters = exclude_header_characters
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _get_maximize(value: Any) -> list[MetricFunction]:
|
|
149
|
+
from schemathesis.generation.metrics import METRICS
|
|
150
|
+
|
|
151
|
+
if isinstance(value, list):
|
|
152
|
+
metrics = value
|
|
153
|
+
elif isinstance(value, str):
|
|
154
|
+
metrics = [value]
|
|
155
|
+
else:
|
|
156
|
+
metrics = []
|
|
157
|
+
return METRICS.get_by_names(metrics)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum, unique
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
import hypothesis
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@unique
|
|
11
|
+
class HealthCheck(str, Enum):
|
|
12
|
+
data_too_large = "data_too_large"
|
|
13
|
+
filter_too_much = "filter_too_much"
|
|
14
|
+
too_slow = "too_slow"
|
|
15
|
+
large_base_example = "large_base_example"
|
|
16
|
+
all = "all"
|
|
17
|
+
|
|
18
|
+
def as_hypothesis(self) -> list[hypothesis.HealthCheck]:
|
|
19
|
+
from hypothesis import HealthCheck
|
|
20
|
+
|
|
21
|
+
if self.name == "all":
|
|
22
|
+
return list(HealthCheck)
|
|
23
|
+
|
|
24
|
+
return [HealthCheck[self.name]]
|