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
@@ -11,7 +11,8 @@ from hypothesis import reject
|
|
11
11
|
from hypothesis import strategies as st
|
12
12
|
from hypothesis.strategies._internal.featureflags import FeatureStrategy
|
13
13
|
|
14
|
-
from
|
14
|
+
from schemathesis.core.transforms import deepclone
|
15
|
+
|
15
16
|
from ..utils import get_type, is_header_location
|
16
17
|
from .types import Draw, Schema
|
17
18
|
from .utils import can_negate
|
@@ -19,7 +20,7 @@ from .utils import can_negate
|
|
19
20
|
T = TypeVar("T")
|
20
21
|
|
21
22
|
|
22
|
-
class MutationResult(enum.Enum):
|
23
|
+
class MutationResult(int, enum.Enum):
|
23
24
|
"""The result of applying some mutation to some schema.
|
24
25
|
|
25
26
|
Failing to mutate something means that by applying some mutation, it is not possible to change
|
@@ -111,7 +112,7 @@ class MutationContext:
|
|
111
112
|
# Body can be of any type and does not have any specific type semantic.
|
112
113
|
mutations = draw(ordered(get_mutations(draw, self.keywords)))
|
113
114
|
# Deep copy all keywords to avoid modifying the original schema
|
114
|
-
new_schema =
|
115
|
+
new_schema = deepclone(self.keywords)
|
115
116
|
enabled_mutations = draw(st.shared(FeatureStrategy(), key="mutations")) # type: ignore
|
116
117
|
# Always apply at least one mutation, otherwise everything is rejected, and we'd like to avoid it
|
117
118
|
# for performance reasons
|
@@ -401,8 +402,8 @@ def negate_constraints(context: MutationContext, draw: Draw, schema: Schema) ->
|
|
401
402
|
if key in DEPENDENCIES:
|
402
403
|
# If this keyword has a dependency, then it should be also negated
|
403
404
|
dependency = DEPENDENCIES[key]
|
404
|
-
if dependency not in negated:
|
405
|
-
negated[dependency] = copied[dependency]
|
405
|
+
if dependency not in negated and dependency in copied:
|
406
|
+
negated[dependency] = copied[dependency]
|
406
407
|
else:
|
407
408
|
schema[key] = value
|
408
409
|
if is_negated:
|
@@ -4,12 +4,13 @@ import json
|
|
4
4
|
from dataclasses import dataclass
|
5
5
|
from typing import TYPE_CHECKING, Any, ClassVar, Iterable
|
6
6
|
|
7
|
-
from
|
8
|
-
from
|
7
|
+
from schemathesis.core.errors import InvalidSchema
|
8
|
+
from schemathesis.schemas import Parameter
|
9
|
+
|
9
10
|
from .converter import to_json_schema_recursive
|
10
11
|
|
11
12
|
if TYPE_CHECKING:
|
12
|
-
from ...
|
13
|
+
from ...schemas import APIOperation
|
13
14
|
|
14
15
|
|
15
16
|
@dataclass(eq=False)
|
@@ -22,6 +23,7 @@ class OpenAPIParameter(Parameter):
|
|
22
23
|
supported_jsonschema_keywords: ClassVar[tuple[str, ...]]
|
23
24
|
|
24
25
|
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
26
|
+
|
25
27
|
@property
|
26
28
|
def description(self) -> str | None:
|
27
29
|
"""A brief parameter description."""
|
@@ -312,9 +314,6 @@ def parameters_to_json_schema(
|
|
312
314
|
) -> dict[str, Any]:
|
313
315
|
"""Create an "object" JSON schema from a list of Open API parameters.
|
314
316
|
|
315
|
-
:param List[OpenAPIParameter] parameters: A list of Open API parameters related to the same location. All of
|
316
|
-
them are expected to have the same "in" value.
|
317
|
-
|
318
317
|
For each input parameter, there will be a property in the output schema.
|
319
318
|
|
320
319
|
This:
|
@@ -371,13 +370,12 @@ def get_parameter_schema(operation: APIOperation, data: dict[str, Any]) -> dict[
|
|
371
370
|
# In Open API 3.0, there could be "schema" or "content" field. They are mutually exclusive.
|
372
371
|
if "schema" in data:
|
373
372
|
if not isinstance(data["schema"], dict):
|
374
|
-
raise
|
373
|
+
raise InvalidSchema(
|
375
374
|
INVALID_SCHEMA_MESSAGE.format(
|
376
375
|
location=data.get("in", ""), name=data.get("name", "<UNKNOWN>"), schema=data["schema"]
|
377
376
|
),
|
378
377
|
path=operation.path,
|
379
378
|
method=operation.method,
|
380
|
-
full_path=operation.full_path,
|
381
379
|
)
|
382
380
|
return data["schema"]
|
383
381
|
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#fixed-fields-10
|
@@ -385,11 +383,10 @@ def get_parameter_schema(operation: APIOperation, data: dict[str, Any]) -> dict[
|
|
385
383
|
try:
|
386
384
|
content = data["content"]
|
387
385
|
except KeyError as exc:
|
388
|
-
raise
|
386
|
+
raise InvalidSchema(
|
389
387
|
MISSING_SCHEMA_OR_CONTENT_MESSAGE.format(location=data.get("in", ""), name=data.get("name", "<UNKNOWN>")),
|
390
388
|
path=operation.path,
|
391
389
|
method=operation.method,
|
392
|
-
full_path=operation.full_path,
|
393
390
|
) from exc
|
394
391
|
options = iter(content.values())
|
395
392
|
media_type_object = next(options)
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
3
3
|
import re
|
4
4
|
from functools import lru_cache
|
5
5
|
|
6
|
-
from
|
6
|
+
from schemathesis.core.errors import InternalError
|
7
7
|
|
8
8
|
try: # pragma: no cover
|
9
9
|
import re._constants as sre
|
@@ -19,11 +19,12 @@ if hasattr(sre, "POSSESSIVE_REPEAT"):
|
|
19
19
|
else:
|
20
20
|
REPEATS = (sre.MIN_REPEAT, sre.MAX_REPEAT)
|
21
21
|
LITERAL = sre.LITERAL
|
22
|
+
NOT_LITERAL = sre.NOT_LITERAL
|
22
23
|
IN = sre.IN
|
23
24
|
MAXREPEAT = sre_parse.MAXREPEAT
|
24
25
|
|
25
26
|
|
26
|
-
@lru_cache
|
27
|
+
@lru_cache
|
27
28
|
def update_quantifier(pattern: str, min_length: int | None, max_length: int | None) -> str:
|
28
29
|
"""Update the quantifier of a regular expression based on given min and max lengths."""
|
29
30
|
if not pattern or (min_length in (None, 0) and max_length is None):
|
@@ -69,13 +70,18 @@ def _handle_parsed_pattern(parsed: list, pattern: str, min_length: int | None, m
|
|
69
70
|
trailing_anchor_length = _get_anchor_length(parsed[2][1])
|
70
71
|
leading_anchor = pattern[:leading_anchor_length]
|
71
72
|
trailing_anchor = pattern[-trailing_anchor_length:]
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
)
|
73
|
+
# Special case for patterns canonicalisation. Some frameworks generate `\\w\\W` instead of `.`
|
74
|
+
# Such patterns lead to significantly slower data generation
|
75
|
+
if op == sre.IN and _matches_anything(value):
|
76
|
+
op = sre.ANY
|
77
|
+
value = None
|
78
|
+
inner_pattern = "."
|
79
|
+
elif op in REPEATS and len(value[2]) == 1 and value[2][0][0] == sre.IN and _matches_anything(value[2][0][1]):
|
80
|
+
value = (value[0], value[1], [(sre.ANY, None)], *value[3:])
|
81
|
+
inner_pattern = "."
|
82
|
+
else:
|
83
|
+
inner_pattern = pattern[leading_anchor_length:-trailing_anchor_length]
|
84
|
+
return leading_anchor + _update_quantifier(op, value, inner_pattern, min_length, max_length) + trailing_anchor
|
79
85
|
elif (
|
80
86
|
len(parsed) > 3
|
81
87
|
and parsed[0][0] == ANCHOR
|
@@ -86,6 +92,19 @@ def _handle_parsed_pattern(parsed: list, pattern: str, min_length: int | None, m
|
|
86
92
|
return pattern
|
87
93
|
|
88
94
|
|
95
|
+
def _matches_anything(value: list) -> bool:
|
96
|
+
"""Check if the given pattern is equivalent to '.' (match any character)."""
|
97
|
+
# Common forms: [\w\W], [\s\S], etc.
|
98
|
+
return value in (
|
99
|
+
[(sre.CATEGORY, sre.CATEGORY_WORD), (sre.CATEGORY, sre.CATEGORY_NOT_WORD)],
|
100
|
+
[(sre.CATEGORY, sre.CATEGORY_SPACE), (sre.CATEGORY, sre.CATEGORY_NOT_SPACE)],
|
101
|
+
[(sre.CATEGORY, sre.CATEGORY_DIGIT), (sre.CATEGORY, sre.CATEGORY_NOT_DIGIT)],
|
102
|
+
[(sre.CATEGORY, sre.CATEGORY_NOT_WORD), (sre.CATEGORY, sre.CATEGORY_WORD)],
|
103
|
+
[(sre.CATEGORY, sre.CATEGORY_NOT_SPACE), (sre.CATEGORY, sre.CATEGORY_SPACE)],
|
104
|
+
[(sre.CATEGORY, sre.CATEGORY_NOT_DIGIT), (sre.CATEGORY, sre.CATEGORY_DIGIT)],
|
105
|
+
)
|
106
|
+
|
107
|
+
|
89
108
|
def _handle_anchored_pattern(parsed: list, pattern: str, min_length: int | None, max_length: int | None) -> str:
|
90
109
|
"""Update regex pattern with multiple quantified patterns to satisfy length constraints."""
|
91
110
|
# Extract anchors
|
@@ -96,8 +115,20 @@ def _handle_anchored_pattern(parsed: list, pattern: str, min_length: int | None,
|
|
96
115
|
|
97
116
|
pattern_parts = parsed[1:-1]
|
98
117
|
|
118
|
+
# Calculate total fixed length and per-repetition lengths
|
119
|
+
fixed_length = 0
|
120
|
+
quantifier_bounds = []
|
121
|
+
repetition_lengths = []
|
122
|
+
|
123
|
+
for op, value in pattern_parts:
|
124
|
+
if op in (LITERAL, NOT_LITERAL):
|
125
|
+
fixed_length += 1
|
126
|
+
elif op in REPEATS:
|
127
|
+
min_repeat, max_repeat, subpattern = value
|
128
|
+
quantifier_bounds.append((min_repeat, max_repeat))
|
129
|
+
repetition_lengths.append(_calculate_min_repetition_length(subpattern))
|
130
|
+
|
99
131
|
# Adjust length constraints by subtracting fixed literals length
|
100
|
-
fixed_length = sum(1 for op, _ in pattern_parts if op == LITERAL)
|
101
132
|
if min_length is not None:
|
102
133
|
min_length -= fixed_length
|
103
134
|
if min_length < 0:
|
@@ -107,13 +138,10 @@ def _handle_anchored_pattern(parsed: list, pattern: str, min_length: int | None,
|
|
107
138
|
if max_length < 0:
|
108
139
|
return pattern
|
109
140
|
|
110
|
-
# Extract only min/max bounds from quantified parts
|
111
|
-
quantifier_bounds = [value[:2] for op, value in pattern_parts if op in REPEATS]
|
112
|
-
|
113
141
|
if not quantifier_bounds:
|
114
142
|
return pattern
|
115
143
|
|
116
|
-
length_distribution = _distribute_length_constraints(quantifier_bounds, min_length, max_length)
|
144
|
+
length_distribution = _distribute_length_constraints(quantifier_bounds, repetition_lengths, min_length, max_length)
|
117
145
|
if not length_distribution:
|
118
146
|
return pattern
|
119
147
|
|
@@ -194,7 +222,7 @@ def _find_quantified_end(pattern: str, start: int) -> int:
|
|
194
222
|
|
195
223
|
|
196
224
|
def _distribute_length_constraints(
|
197
|
-
bounds: list[tuple[int, int]], min_length: int | None, max_length: int | None
|
225
|
+
bounds: list[tuple[int, int]], repetition_lengths: list[int], min_length: int | None, max_length: int | None
|
198
226
|
) -> list[tuple[int, int]] | None:
|
199
227
|
"""Distribute length constraints among quantified pattern parts."""
|
200
228
|
# Handle exact length case with dynamic programming
|
@@ -210,18 +238,22 @@ def _distribute_length_constraints(
|
|
210
238
|
if pos == len(bounds):
|
211
239
|
return [()] if remaining == 0 else None
|
212
240
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
241
|
+
max_repeat: int
|
242
|
+
min_repeat, max_repeat = bounds[pos]
|
243
|
+
repeat_length = repetition_lengths[pos]
|
244
|
+
|
245
|
+
if max_repeat == MAXREPEAT:
|
246
|
+
max_repeat = remaining // repeat_length + 1 if repeat_length > 0 else remaining + 1
|
219
247
|
|
220
248
|
# Try each possible length for current quantifier
|
221
|
-
for
|
222
|
-
|
249
|
+
for repeat_count in range(min_repeat, max_repeat + 1):
|
250
|
+
used_length = repeat_count * repeat_length
|
251
|
+
if used_length > remaining:
|
252
|
+
break
|
253
|
+
|
254
|
+
rest = find_valid_combination(pos + 1, remaining - used_length)
|
223
255
|
if rest is not None:
|
224
|
-
dp[(pos, remaining)] = [(
|
256
|
+
dp[(pos, remaining)] = [(repeat_count,) + r for r in rest]
|
225
257
|
return dp[(pos, remaining)]
|
226
258
|
|
227
259
|
dp[(pos, remaining)] = None
|
@@ -262,6 +294,22 @@ def _distribute_length_constraints(
|
|
262
294
|
return result
|
263
295
|
|
264
296
|
|
297
|
+
def _calculate_min_repetition_length(subpattern: list) -> int:
|
298
|
+
"""Calculate minimum length contribution per repetition of a quantified group."""
|
299
|
+
total = 0
|
300
|
+
for op, value in subpattern:
|
301
|
+
if op in [LITERAL, NOT_LITERAL, IN, sre.ANY]:
|
302
|
+
total += 1
|
303
|
+
elif op == sre.SUBPATTERN:
|
304
|
+
_, _, _, inner_pattern = value
|
305
|
+
total += _calculate_min_repetition_length(inner_pattern)
|
306
|
+
elif op in REPEATS:
|
307
|
+
min_repeat, _, inner_pattern = value
|
308
|
+
inner_min = _calculate_min_repetition_length(inner_pattern)
|
309
|
+
total += min_repeat * inner_min
|
310
|
+
return total
|
311
|
+
|
312
|
+
|
265
313
|
def _get_anchor_length(node_type: int) -> int:
|
266
314
|
"""Determine the length of the anchor based on its type."""
|
267
315
|
if node_type in {sre.AT_BEGINNING_STRING, sre.AT_END_STRING, sre.AT_BOUNDARY, sre.AT_NON_BOUNDARY}:
|
@@ -269,15 +317,28 @@ def _get_anchor_length(node_type: int) -> int:
|
|
269
317
|
return 1 # ^ or $ or their multiline/locale/unicode variants
|
270
318
|
|
271
319
|
|
272
|
-
def _update_quantifier(
|
320
|
+
def _update_quantifier(
|
321
|
+
op: int, value: tuple | None, pattern: str, min_length: int | None, max_length: int | None
|
322
|
+
) -> str:
|
273
323
|
"""Update the quantifier based on the operation type and given constraints."""
|
274
|
-
if op in REPEATS:
|
324
|
+
if op in REPEATS and value is not None:
|
275
325
|
return _handle_repeat_quantifier(value, pattern, min_length, max_length)
|
276
|
-
if op in (LITERAL, IN) and max_length != 0:
|
326
|
+
if op in (LITERAL, NOT_LITERAL, IN) and max_length != 0:
|
277
327
|
return _handle_literal_or_in_quantifier(pattern, min_length, max_length)
|
328
|
+
if op == sre.ANY and value is None:
|
329
|
+
# Equivalent to `.` which is in turn is the same as `.{1}`
|
330
|
+
return _handle_repeat_quantifier(
|
331
|
+
SINGLE_ANY,
|
332
|
+
pattern,
|
333
|
+
min_length,
|
334
|
+
max_length,
|
335
|
+
)
|
278
336
|
return pattern
|
279
337
|
|
280
338
|
|
339
|
+
SINGLE_ANY = sre_parse.parse(".{1}")[0][1]
|
340
|
+
|
341
|
+
|
281
342
|
def _handle_repeat_quantifier(
|
282
343
|
value: tuple[int, int, tuple], pattern: str, min_length: int | None, max_length: int | None
|
283
344
|
) -> str:
|
@@ -324,10 +385,12 @@ def _build_size(min_repeat: int, max_repeat: int, min_length: int | None, max_le
|
|
324
385
|
def _strip_quantifier(pattern: str) -> str:
|
325
386
|
"""Remove quantifier from the pattern."""
|
326
387
|
# Lazy & posessive quantifiers
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
388
|
+
for marker in ("*?", "+?", "??", "*+", "?+", "++"):
|
389
|
+
if pattern.endswith(marker) and not pattern.endswith(rf"\{marker}"):
|
390
|
+
return pattern[:-2]
|
391
|
+
for marker in ("?", "*", "+"):
|
392
|
+
if pattern.endswith(marker) and not pattern.endswith(rf"\{marker}"):
|
393
|
+
pattern = pattern[:-1]
|
331
394
|
if pattern.endswith("}") and "{" in pattern:
|
332
395
|
# Find the start of the exact quantifier and drop everything since that index
|
333
396
|
idx = pattern.rfind("{")
|
@@ -1,18 +1,17 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import sys
|
4
|
-
from dataclasses import dataclass
|
5
4
|
from functools import lru_cache
|
6
5
|
from typing import Any, Callable, Dict, Union, overload
|
7
6
|
from urllib.request import urlopen
|
8
7
|
|
9
|
-
import jsonschema
|
10
8
|
import requests
|
11
|
-
from jsonschema.exceptions import RefResolutionError
|
12
9
|
|
13
|
-
from
|
14
|
-
from
|
15
|
-
from
|
10
|
+
from schemathesis.core.compat import RefResolutionError, RefResolver
|
11
|
+
from schemathesis.core.deserialization import deserialize_yaml
|
12
|
+
from schemathesis.core.transforms import deepclone
|
13
|
+
from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
|
14
|
+
|
16
15
|
from .constants import ALL_KEYWORDS
|
17
16
|
from .converter import to_json_schema_recursive
|
18
17
|
from .utils import get_type
|
@@ -24,7 +23,7 @@ RECURSION_DEPTH_LIMIT = 100
|
|
24
23
|
def load_file_impl(location: str, opener: Callable) -> dict[str, Any]:
|
25
24
|
"""Load a schema from the given file."""
|
26
25
|
with opener(location) as fd:
|
27
|
-
return
|
26
|
+
return deserialize_yaml(fd)
|
28
27
|
|
29
28
|
|
30
29
|
@lru_cache
|
@@ -41,14 +40,14 @@ def load_file_uri(location: str) -> dict[str, Any]:
|
|
41
40
|
|
42
41
|
def load_remote_uri(uri: str) -> Any:
|
43
42
|
"""Load the resource and parse it as YAML / JSON."""
|
44
|
-
response = requests.get(uri, timeout=DEFAULT_RESPONSE_TIMEOUT
|
45
|
-
return
|
43
|
+
response = requests.get(uri, timeout=DEFAULT_RESPONSE_TIMEOUT)
|
44
|
+
return deserialize_yaml(response.content)
|
46
45
|
|
47
46
|
|
48
47
|
JSONType = Union[None, bool, float, str, list, Dict[str, Any]]
|
49
48
|
|
50
49
|
|
51
|
-
class InliningResolver(
|
50
|
+
class InliningResolver(RefResolver):
|
52
51
|
"""Inlines resolved schemas."""
|
53
52
|
|
54
53
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
@@ -75,12 +74,10 @@ class InliningResolver(jsonschema.RefResolver):
|
|
75
74
|
raise
|
76
75
|
|
77
76
|
@overload
|
78
|
-
def resolve_all(self, item: dict[str, Any], recursion_level: int = 0) -> dict[str, Any]:
|
79
|
-
pass
|
77
|
+
def resolve_all(self, item: dict[str, Any], recursion_level: int = 0) -> dict[str, Any]: ...
|
80
78
|
|
81
79
|
@overload
|
82
|
-
def resolve_all(self, item: list, recursion_level: int = 0) -> list:
|
83
|
-
pass
|
80
|
+
def resolve_all(self, item: list, recursion_level: int = 0) -> list: ...
|
84
81
|
|
85
82
|
def resolve_all(self, item: JSONType, recursion_level: int = 0) -> JSONType:
|
86
83
|
"""Recursively resolve all references in the given object."""
|
@@ -95,7 +92,7 @@ class InliningResolver(jsonschema.RefResolver):
|
|
95
92
|
# In other cases, this method create new objects for mutable types (dict & list)
|
96
93
|
next_recursion_level = recursion_level + 1
|
97
94
|
if next_recursion_level > RECURSION_DEPTH_LIMIT:
|
98
|
-
copied =
|
95
|
+
copied = deepclone(resolved)
|
99
96
|
remove_optional_references(copied)
|
100
97
|
return copied
|
101
98
|
return resolve(resolved, next_recursion_level)
|
@@ -238,41 +235,3 @@ def remove_optional_references(schema: dict[str, Any]) -> None:
|
|
238
235
|
clean_additional_properties(definition)
|
239
236
|
for k in on_single_item_combinators(definition):
|
240
237
|
del definition[k]
|
241
|
-
|
242
|
-
|
243
|
-
@dataclass
|
244
|
-
class Unresolvable:
|
245
|
-
pass
|
246
|
-
|
247
|
-
|
248
|
-
UNRESOLVABLE = Unresolvable()
|
249
|
-
|
250
|
-
|
251
|
-
def resolve_pointer(document: Any, pointer: str) -> dict | list | str | int | float | None | Unresolvable:
|
252
|
-
"""Implementation is adapted from Rust's `serde-json` crate.
|
253
|
-
|
254
|
-
Ref: https://github.com/serde-rs/json/blob/master/src/value/mod.rs#L751
|
255
|
-
"""
|
256
|
-
if not pointer:
|
257
|
-
return document
|
258
|
-
if not pointer.startswith("/"):
|
259
|
-
return UNRESOLVABLE
|
260
|
-
|
261
|
-
def replace(value: str) -> str:
|
262
|
-
return value.replace("~1", "/").replace("~0", "~")
|
263
|
-
|
264
|
-
tokens = map(replace, pointer.split("/")[1:])
|
265
|
-
target = document
|
266
|
-
for token in tokens:
|
267
|
-
if isinstance(target, dict):
|
268
|
-
target = target.get(token, UNRESOLVABLE)
|
269
|
-
if target is UNRESOLVABLE:
|
270
|
-
return UNRESOLVABLE
|
271
|
-
elif isinstance(target, list):
|
272
|
-
try:
|
273
|
-
target = target[int(token)]
|
274
|
-
except IndexError:
|
275
|
-
return UNRESOLVABLE
|
276
|
-
else:
|
277
|
-
return UNRESOLVABLE
|
278
|
-
return target
|