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
@@ -0,0 +1,109 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
|
5
|
+
from schemathesis.core.failures import Failure, Severity
|
6
|
+
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from graphql.error import GraphQLFormattedError
|
9
|
+
|
10
|
+
|
11
|
+
class UnexpectedGraphQLResponse(Failure):
|
12
|
+
"""GraphQL response is not a JSON object."""
|
13
|
+
|
14
|
+
__slots__ = ("operation", "type_name", "title", "message", "case_id", "severity")
|
15
|
+
|
16
|
+
def __init__(
|
17
|
+
self,
|
18
|
+
*,
|
19
|
+
operation: str,
|
20
|
+
type_name: str,
|
21
|
+
title: str = "Unexpected GraphQL Response",
|
22
|
+
message: str,
|
23
|
+
case_id: str | None = None,
|
24
|
+
) -> None:
|
25
|
+
self.operation = operation
|
26
|
+
self.type_name = type_name
|
27
|
+
self.title = title
|
28
|
+
self.message = message
|
29
|
+
self.case_id = case_id
|
30
|
+
self.severity = Severity.MEDIUM
|
31
|
+
|
32
|
+
@property
|
33
|
+
def _unique_key(self) -> str:
|
34
|
+
return self.type_name
|
35
|
+
|
36
|
+
|
37
|
+
class GraphQLClientError(Failure):
|
38
|
+
"""GraphQL query has not been executed."""
|
39
|
+
|
40
|
+
__slots__ = ("operation", "errors", "title", "message", "case_id", "_unique_key_cache", "severity")
|
41
|
+
|
42
|
+
def __init__(
|
43
|
+
self,
|
44
|
+
*,
|
45
|
+
operation: str,
|
46
|
+
message: str,
|
47
|
+
errors: list[GraphQLFormattedError],
|
48
|
+
title: str = "GraphQL client error",
|
49
|
+
case_id: str | None = None,
|
50
|
+
) -> None:
|
51
|
+
self.operation = operation
|
52
|
+
self.errors = errors
|
53
|
+
self.title = title
|
54
|
+
self.message = message
|
55
|
+
self.case_id = case_id
|
56
|
+
self._unique_key_cache: str | None = None
|
57
|
+
self.severity = Severity.MEDIUM
|
58
|
+
|
59
|
+
@property
|
60
|
+
def _unique_key(self) -> str:
|
61
|
+
if self._unique_key_cache is None:
|
62
|
+
self._unique_key_cache = _group_graphql_errors(self.errors)
|
63
|
+
return self._unique_key_cache
|
64
|
+
|
65
|
+
|
66
|
+
class GraphQLServerError(Failure):
|
67
|
+
"""GraphQL response indicates at least one server error."""
|
68
|
+
|
69
|
+
__slots__ = ("operation", "errors", "title", "message", "case_id", "_unique_key_cache", "severity")
|
70
|
+
|
71
|
+
def __init__(
|
72
|
+
self,
|
73
|
+
*,
|
74
|
+
operation: str,
|
75
|
+
message: str,
|
76
|
+
errors: list[GraphQLFormattedError],
|
77
|
+
title: str = "GraphQL server error",
|
78
|
+
case_id: str | None = None,
|
79
|
+
) -> None:
|
80
|
+
self.operation = operation
|
81
|
+
self.errors = errors
|
82
|
+
self.title = title
|
83
|
+
self.message = message
|
84
|
+
self.case_id = case_id
|
85
|
+
self._unique_key_cache: str | None = None
|
86
|
+
self.severity = Severity.CRITICAL
|
87
|
+
|
88
|
+
@property
|
89
|
+
def _unique_key(self) -> str:
|
90
|
+
if self._unique_key_cache is None:
|
91
|
+
self._unique_key_cache = _group_graphql_errors(self.errors)
|
92
|
+
return self._unique_key_cache
|
93
|
+
|
94
|
+
|
95
|
+
def _group_graphql_errors(errors: list[GraphQLFormattedError]) -> str:
|
96
|
+
entries = []
|
97
|
+
for error in errors:
|
98
|
+
message = error["message"]
|
99
|
+
if "locations" in error:
|
100
|
+
message += ";locations:"
|
101
|
+
for location in sorted(error["locations"]):
|
102
|
+
message += f"({location['line'], location['column']})"
|
103
|
+
if "path" in error:
|
104
|
+
message += ";path:"
|
105
|
+
for chunk in error["path"]:
|
106
|
+
message += str(chunk)
|
107
|
+
entries.append(message)
|
108
|
+
entries.sort()
|
109
|
+
return "".join(entries)
|
@@ -0,0 +1,131 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import json
|
4
|
+
from functools import lru_cache
|
5
|
+
from os import PathLike
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import IO, TYPE_CHECKING, Any, Callable, Dict, NoReturn, TypeVar, cast
|
8
|
+
|
9
|
+
from schemathesis.core.errors import LoaderError, LoaderErrorKind
|
10
|
+
from schemathesis.core.loaders import load_from_url, prepare_request_kwargs, raise_for_status, require_relative_url
|
11
|
+
from schemathesis.hooks import HookContext, dispatch
|
12
|
+
from schemathesis.python import asgi, wsgi
|
13
|
+
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from graphql import DocumentNode
|
16
|
+
|
17
|
+
from schemathesis.specs.graphql.schemas import GraphQLSchema
|
18
|
+
|
19
|
+
|
20
|
+
def from_asgi(path: str, app: Any, **kwargs: Any) -> GraphQLSchema:
|
21
|
+
require_relative_url(path)
|
22
|
+
kwargs.setdefault("json", {"query": get_introspection_query()})
|
23
|
+
client = asgi.get_client(app)
|
24
|
+
response = load_from_url(client.post, url=path, **kwargs)
|
25
|
+
schema = extract_schema_from_response(response, lambda r: r.json())
|
26
|
+
return from_dict(schema=schema).configure(app=app, location=path)
|
27
|
+
|
28
|
+
|
29
|
+
def from_wsgi(path: str, app: Any, **kwargs: Any) -> GraphQLSchema:
|
30
|
+
require_relative_url(path)
|
31
|
+
prepare_request_kwargs(kwargs)
|
32
|
+
kwargs.setdefault("json", {"query": get_introspection_query()})
|
33
|
+
client = wsgi.get_client(app)
|
34
|
+
response = client.post(path=path, **kwargs)
|
35
|
+
raise_for_status(response)
|
36
|
+
schema = extract_schema_from_response(response, lambda r: r.json)
|
37
|
+
return from_dict(schema=schema).configure(app=app, location=path)
|
38
|
+
|
39
|
+
|
40
|
+
def from_url(url: str, *, wait_for_schema: float | None = None, **kwargs: Any) -> GraphQLSchema:
|
41
|
+
"""Load from URL."""
|
42
|
+
import requests
|
43
|
+
|
44
|
+
kwargs.setdefault("json", {"query": get_introspection_query()})
|
45
|
+
response = load_from_url(requests.post, url=url, wait_for_schema=wait_for_schema, **kwargs)
|
46
|
+
schema = extract_schema_from_response(response, lambda r: r.json())
|
47
|
+
return from_dict(schema).configure(location=url)
|
48
|
+
|
49
|
+
|
50
|
+
def from_path(path: PathLike | str, *, encoding: str = "utf-8") -> GraphQLSchema:
|
51
|
+
"""Load from a filesystem path."""
|
52
|
+
with open(path, encoding=encoding) as file:
|
53
|
+
return from_file(file=file).configure(location=Path(path).absolute().as_uri())
|
54
|
+
|
55
|
+
|
56
|
+
def from_file(file: IO[str] | str) -> GraphQLSchema:
|
57
|
+
"""Load from file-like object or string."""
|
58
|
+
import graphql
|
59
|
+
|
60
|
+
if isinstance(file, str):
|
61
|
+
data = file
|
62
|
+
else:
|
63
|
+
data = file.read()
|
64
|
+
try:
|
65
|
+
document = graphql.build_schema(data)
|
66
|
+
result = graphql.execute(document, get_introspection_query_ast())
|
67
|
+
# TYPES: We don't pass `is_awaitable` above, therefore `result` is of the `ExecutionResult` type
|
68
|
+
result = cast(graphql.ExecutionResult, result)
|
69
|
+
# TYPES:
|
70
|
+
# - `document` is a valid schema, because otherwise `build_schema` will rise an error;
|
71
|
+
# - `INTROSPECTION_QUERY` is a valid query - it is known upfront;
|
72
|
+
# Therefore the execution result is always valid at this point and `result.data` is not `None`
|
73
|
+
schema = cast(Dict[str, Any], result.data)
|
74
|
+
except Exception as exc:
|
75
|
+
try:
|
76
|
+
schema = json.loads(data)
|
77
|
+
if not isinstance(schema, dict) or "__schema" not in schema:
|
78
|
+
_on_invalid_schema(exc)
|
79
|
+
except json.JSONDecodeError:
|
80
|
+
_on_invalid_schema(exc, extras=[entry for entry in str(exc).splitlines() if entry])
|
81
|
+
return from_dict(schema)
|
82
|
+
|
83
|
+
|
84
|
+
def from_dict(schema: dict[str, Any]) -> GraphQLSchema:
|
85
|
+
"""Base loader that others build upon."""
|
86
|
+
from schemathesis.specs.graphql.schemas import GraphQLSchema
|
87
|
+
|
88
|
+
if "data" in schema:
|
89
|
+
schema = schema["data"]
|
90
|
+
hook_context = HookContext()
|
91
|
+
dispatch("before_load_schema", hook_context, schema)
|
92
|
+
instance = GraphQLSchema(schema)
|
93
|
+
dispatch("after_load_schema", hook_context, instance)
|
94
|
+
return instance
|
95
|
+
|
96
|
+
|
97
|
+
@lru_cache
|
98
|
+
def get_introspection_query() -> str:
|
99
|
+
import graphql
|
100
|
+
|
101
|
+
return graphql.get_introspection_query()
|
102
|
+
|
103
|
+
|
104
|
+
@lru_cache
|
105
|
+
def get_introspection_query_ast() -> DocumentNode:
|
106
|
+
import graphql
|
107
|
+
|
108
|
+
query = get_introspection_query()
|
109
|
+
return graphql.parse(query)
|
110
|
+
|
111
|
+
|
112
|
+
R = TypeVar("R")
|
113
|
+
|
114
|
+
|
115
|
+
def extract_schema_from_response(response: R, callback: Callable[[R], Any]) -> dict[str, Any]:
|
116
|
+
try:
|
117
|
+
decoded = callback(response)
|
118
|
+
except json.JSONDecodeError as exc:
|
119
|
+
raise LoaderError(
|
120
|
+
LoaderErrorKind.UNEXPECTED_CONTENT_TYPE,
|
121
|
+
"Received unsupported content while expecting a JSON payload for GraphQL",
|
122
|
+
) from exc
|
123
|
+
return decoded
|
124
|
+
|
125
|
+
|
126
|
+
def _on_invalid_schema(exc: Exception, extras: list[str] | None = None) -> NoReturn:
|
127
|
+
raise LoaderError(
|
128
|
+
LoaderErrorKind.GRAPHQL_INVALID_SCHEMA,
|
129
|
+
"The provided API schema does not appear to be a valid GraphQL schema",
|
130
|
+
extras=extras or [],
|
131
|
+
) from exc
|
schemathesis/hooks.py
CHANGED
@@ -2,22 +2,22 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import inspect
|
4
4
|
from collections import defaultdict
|
5
|
-
from copy import deepcopy
|
6
5
|
from dataclasses import dataclass, field
|
7
6
|
from enum import Enum, unique
|
8
7
|
from functools import partial
|
9
8
|
from typing import TYPE_CHECKING, Any, Callable, ClassVar, cast
|
10
9
|
|
11
|
-
from .
|
12
|
-
from .
|
10
|
+
from schemathesis.core.marks import Mark
|
11
|
+
from schemathesis.core.transport import Response
|
12
|
+
from schemathesis.filters import FilterSet, attach_filter_chain
|
13
13
|
|
14
14
|
if TYPE_CHECKING:
|
15
15
|
from hypothesis import strategies as st
|
16
16
|
|
17
|
-
from .
|
18
|
-
from .schemas import BaseSchema
|
19
|
-
|
20
|
-
|
17
|
+
from schemathesis.generation.case import Case
|
18
|
+
from schemathesis.schemas import APIOperation, BaseSchema
|
19
|
+
|
20
|
+
HookDispatcherMark = Mark["HookDispatcher"](attr_name="hook_dispatcher")
|
21
21
|
|
22
22
|
|
23
23
|
@unique
|
@@ -45,10 +45,6 @@ class HookContext:
|
|
45
45
|
|
46
46
|
operation: APIOperation | None = None
|
47
47
|
|
48
|
-
@deprecated_property(removed_in="4.0", replacement="`operation`")
|
49
|
-
def endpoint(self) -> APIOperation | None:
|
50
|
-
return self.operation
|
51
|
-
|
52
48
|
|
53
49
|
def to_filterable_hook(dispatcher: HookDispatcher) -> Callable:
|
54
50
|
filter_used = False
|
@@ -140,18 +136,6 @@ class HookDispatcher:
|
|
140
136
|
"""
|
141
137
|
raise NotImplementedError
|
142
138
|
|
143
|
-
def merge(self, other: HookDispatcher) -> HookDispatcher:
|
144
|
-
"""Merge two dispatches together.
|
145
|
-
|
146
|
-
The resulting dispatcher will call the `self` hooks first.
|
147
|
-
"""
|
148
|
-
all_hooks = deepcopy(self._hooks)
|
149
|
-
for name, hooks in other._hooks.items():
|
150
|
-
all_hooks[name].extend(hooks)
|
151
|
-
instance = self.__class__(scope=self.scope)
|
152
|
-
instance._hooks = all_hooks
|
153
|
-
return instance
|
154
|
-
|
155
139
|
def apply(self, hook: Callable, *, name: str | None = None) -> Callable[[Callable], Callable]:
|
156
140
|
"""Register hook to run only on one test function.
|
157
141
|
|
@@ -175,7 +159,7 @@ class HookDispatcher:
|
|
175
159
|
else:
|
176
160
|
hook_name = name
|
177
161
|
|
178
|
-
def decorator(func:
|
162
|
+
def decorator(func: Callable) -> Callable:
|
179
163
|
dispatcher = self.add_dispatcher(func)
|
180
164
|
dispatcher.register_hook_with_name(hook, hook_name)
|
181
165
|
return func
|
@@ -183,11 +167,13 @@ class HookDispatcher:
|
|
183
167
|
return decorator
|
184
168
|
|
185
169
|
@classmethod
|
186
|
-
def add_dispatcher(cls, func:
|
170
|
+
def add_dispatcher(cls, func: Callable) -> HookDispatcher:
|
187
171
|
"""Attach a new dispatcher instance to the test if it is not already present."""
|
188
|
-
if not
|
189
|
-
func
|
190
|
-
|
172
|
+
if not HookDispatcherMark.is_set(func):
|
173
|
+
HookDispatcherMark.set(func, cls(scope=HookScope.TEST))
|
174
|
+
dispatcher = HookDispatcherMark.get(func)
|
175
|
+
assert dispatcher is not None
|
176
|
+
return dispatcher
|
191
177
|
|
192
178
|
def register_hook_with_name(self, hook: Callable, name: str) -> Callable:
|
193
179
|
"""A helper for hooks registration."""
|
@@ -226,19 +212,10 @@ class HookDispatcher:
|
|
226
212
|
f"Hook '{name}' takes {len(spec.signature.parameters)} arguments but {len(signature.parameters)} is defined"
|
227
213
|
)
|
228
214
|
|
229
|
-
def collect_statistic(self) -> dict[str, int]:
|
230
|
-
return {name: len(hooks) for name, hooks in self._hooks.items()}
|
231
|
-
|
232
215
|
def get_all_by_name(self, name: str) -> list[Callable]:
|
233
216
|
"""Get a list of hooks registered for a name."""
|
234
217
|
return self._hooks.get(name, [])
|
235
218
|
|
236
|
-
def is_installed(self, name: str, needle: Callable) -> bool:
|
237
|
-
for hook in self.get_all_by_name(name):
|
238
|
-
if hook is needle:
|
239
|
-
return True
|
240
|
-
return False
|
241
|
-
|
242
219
|
def apply_to_container(
|
243
220
|
self, strategy: st.SearchStrategy, container: str, context: HookContext
|
244
221
|
) -> st.SearchStrategy:
|
@@ -307,19 +284,12 @@ def apply_to_all_dispatchers(
|
|
307
284
|
return strategy
|
308
285
|
|
309
286
|
|
310
|
-
def should_skip_operation(dispatcher: HookDispatcher, context: HookContext) -> bool:
|
311
|
-
for hook in dispatcher.get_all_by_name("filter_operations"):
|
312
|
-
if not hook(context):
|
313
|
-
return True
|
314
|
-
return False
|
315
|
-
|
316
|
-
|
317
287
|
def validate_filterable_hook(hook: str | Callable) -> None:
|
318
288
|
if callable(hook):
|
319
289
|
name = hook.__name__
|
320
290
|
else:
|
321
291
|
name = hook
|
322
|
-
if name in ("before_process_path", "before_load_schema", "after_load_schema"
|
292
|
+
if name in ("before_process_path", "before_load_schema", "after_load_schema"):
|
323
293
|
raise ValueError(f"Filters are not applicable to this hook: `{name}`")
|
324
294
|
|
325
295
|
|
@@ -373,11 +343,6 @@ def before_process_path(context: HookContext, path: str, methods: dict[str, Any]
|
|
373
343
|
"""Called before API path is processed."""
|
374
344
|
|
375
345
|
|
376
|
-
@all_scopes
|
377
|
-
def filter_operations(context: HookContext) -> bool | None:
|
378
|
-
"""Decide whether testing of this particular API operation should be skipped or not."""
|
379
|
-
|
380
|
-
|
381
346
|
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
382
347
|
def before_load_schema(context: HookContext, raw_schema: dict[str, Any]) -> None:
|
383
348
|
"""Called before schema instance is created."""
|
@@ -402,15 +367,7 @@ def before_init_operation(context: HookContext, operation: APIOperation) -> None
|
|
402
367
|
|
403
368
|
|
404
369
|
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
405
|
-
def
|
406
|
-
"""Creates an additional test per API operation. If this hook returns None, no additional test created.
|
407
|
-
|
408
|
-
Called with a copy of the original case object and the server's response to the original case.
|
409
|
-
"""
|
410
|
-
|
411
|
-
|
412
|
-
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
413
|
-
def before_call(context: HookContext, case: Case) -> None:
|
370
|
+
def before_call(context: HookContext, case: Case, **kwargs: Any) -> None:
|
414
371
|
"""Called before every network call in CLI tests.
|
415
372
|
|
416
373
|
Use cases:
|
@@ -420,7 +377,7 @@ def before_call(context: HookContext, case: Case) -> None:
|
|
420
377
|
|
421
378
|
|
422
379
|
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
423
|
-
def after_call(context: HookContext, case: Case, response:
|
380
|
+
def after_call(context: HookContext, case: Case, response: Response) -> None:
|
424
381
|
"""Called after every network call in CLI tests.
|
425
382
|
|
426
383
|
Note that you need to modify the response in-place.
|
@@ -434,8 +391,6 @@ def after_call(context: HookContext, case: Case, response: GenericResponse) -> N
|
|
434
391
|
GLOBAL_HOOK_DISPATCHER = HookDispatcher(scope=HookScope.GLOBAL)
|
435
392
|
dispatch = GLOBAL_HOOK_DISPATCHER.dispatch
|
436
393
|
get_all_by_name = GLOBAL_HOOK_DISPATCHER.get_all_by_name
|
437
|
-
is_installed = GLOBAL_HOOK_DISPATCHER.is_installed
|
438
|
-
collect_statistic = GLOBAL_HOOK_DISPATCHER.collect_statistic
|
439
394
|
register = GLOBAL_HOOK_DISPATCHER.register
|
440
395
|
unregister = GLOBAL_HOOK_DISPATCHER.unregister
|
441
396
|
unregister_all = GLOBAL_HOOK_DISPATCHER.unregister_all
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from schemathesis.openapi.loaders import from_asgi, from_dict, from_file, from_path, from_url, from_wsgi
|
2
|
+
from schemathesis.specs.openapi import format, media_type
|
3
|
+
|
4
|
+
__all__ = [
|
5
|
+
"from_url",
|
6
|
+
"from_asgi",
|
7
|
+
"from_wsgi",
|
8
|
+
"from_file",
|
9
|
+
"from_path",
|
10
|
+
"from_dict",
|
11
|
+
"format",
|
12
|
+
"media_type",
|
13
|
+
]
|