schemathesis 3.13.0__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 -1016
- 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 +683 -247
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +27 -0
- schemathesis/specs/graphql/scalars.py +86 -0
- schemathesis/specs/graphql/schemas.py +395 -123
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +578 -317
- 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 +753 -74
- 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 +117 -68
- schemathesis/specs/openapi/negative/mutations.py +294 -104
- schemathesis/specs/openapi/negative/utils.py +3 -6
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +648 -650
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +404 -69
- 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.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -41
- schemathesis/_hypothesis.py +0 -115
- schemathesis/cli/callbacks.py +0 -188
- schemathesis/cli/cassettes.py +0 -253
- schemathesis/cli/context.py +0 -36
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -51
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -508
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -79
- schemathesis/exceptions.py +0 -207
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -216
- schemathesis/failures.py +0 -131
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/lazy.py +0 -227
- schemathesis/models.py +0 -1041
- schemathesis/parameters.py +0 -88
- schemathesis/runner/__init__.py +0 -460
- schemathesis/runner/events.py +0 -240
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -755
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -189
- schemathesis/serializers.py +0 -233
- schemathesis/service/__init__.py +0 -3
- schemathesis/service/client.py +0 -46
- schemathesis/service/constants.py +0 -12
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -39
- schemathesis/service/models.py +0 -7
- schemathesis/service/serialization.py +0 -153
- schemathesis/service/worker.py +0 -40
- 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 -302
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -413
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -349
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -436
- schemathesis-3.13.0.dist-info/METADATA +0 -202
- schemathesis-3.13.0.dist-info/RECORD +0 -91
- schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
|
|
6
|
+
from schemathesis.core.errors import InternalError
|
|
7
|
+
|
|
8
|
+
try: # pragma: no cover
|
|
9
|
+
import re._constants as sre
|
|
10
|
+
import re._parser as sre_parse
|
|
11
|
+
except ImportError:
|
|
12
|
+
import sre_constants as sre
|
|
13
|
+
import sre_parse
|
|
14
|
+
|
|
15
|
+
ANCHOR = sre.AT
|
|
16
|
+
REPEATS: tuple
|
|
17
|
+
if hasattr(sre, "POSSESSIVE_REPEAT"):
|
|
18
|
+
REPEATS = (sre.MIN_REPEAT, sre.MAX_REPEAT, sre.POSSESSIVE_REPEAT)
|
|
19
|
+
else:
|
|
20
|
+
REPEATS = (sre.MIN_REPEAT, sre.MAX_REPEAT)
|
|
21
|
+
LITERAL = sre.LITERAL
|
|
22
|
+
NOT_LITERAL = sre.NOT_LITERAL
|
|
23
|
+
IN = sre.IN
|
|
24
|
+
MAXREPEAT = sre_parse.MAXREPEAT
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@lru_cache
|
|
28
|
+
def update_quantifier(pattern: str, min_length: int | None, max_length: int | None) -> str:
|
|
29
|
+
"""Update the quantifier of a regular expression based on given min and max lengths."""
|
|
30
|
+
if not pattern or (min_length in (None, 0) and max_length is None):
|
|
31
|
+
return pattern
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
parsed = sre_parse.parse(pattern)
|
|
35
|
+
updated = _handle_parsed_pattern(parsed, pattern, min_length, max_length)
|
|
36
|
+
try:
|
|
37
|
+
re.compile(updated)
|
|
38
|
+
except re.error as exc:
|
|
39
|
+
raise InternalError(
|
|
40
|
+
f"The combination of min_length={min_length} and max_length={max_length} applied to the original pattern '{pattern}' resulted in an invalid regex: '{updated}'. "
|
|
41
|
+
"This indicates a bug in the regex quantifier merging logic"
|
|
42
|
+
) from exc
|
|
43
|
+
return updated
|
|
44
|
+
except re.error:
|
|
45
|
+
# Invalid pattern
|
|
46
|
+
return pattern
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _handle_parsed_pattern(parsed: list, pattern: str, min_length: int | None, max_length: int | None) -> str:
|
|
50
|
+
"""Handle the parsed pattern and update quantifiers based on different cases."""
|
|
51
|
+
if len(parsed) == 1:
|
|
52
|
+
op, value = parsed[0]
|
|
53
|
+
return _update_quantifier(op, value, pattern, min_length, max_length)
|
|
54
|
+
elif len(parsed) == 2:
|
|
55
|
+
if parsed[0][0] == ANCHOR:
|
|
56
|
+
# Starts with an anchor
|
|
57
|
+
op, value = parsed[1]
|
|
58
|
+
anchor_length = _get_anchor_length(parsed[0][1])
|
|
59
|
+
leading_anchor = pattern[:anchor_length]
|
|
60
|
+
return leading_anchor + _update_quantifier(op, value, pattern[anchor_length:], min_length, max_length)
|
|
61
|
+
if parsed[1][0] == ANCHOR:
|
|
62
|
+
# Ends with an anchor
|
|
63
|
+
op, value = parsed[0]
|
|
64
|
+
anchor_length = _get_anchor_length(parsed[1][1])
|
|
65
|
+
trailing_anchor = pattern[-anchor_length:]
|
|
66
|
+
return _update_quantifier(op, value, pattern[:-anchor_length], min_length, max_length) + trailing_anchor
|
|
67
|
+
elif len(parsed) == 3 and parsed[0][0] == ANCHOR and parsed[2][0] == ANCHOR:
|
|
68
|
+
op, value = parsed[1]
|
|
69
|
+
leading_anchor_length = _get_anchor_length(parsed[0][1])
|
|
70
|
+
trailing_anchor_length = _get_anchor_length(parsed[2][1])
|
|
71
|
+
leading_anchor = pattern[:leading_anchor_length]
|
|
72
|
+
trailing_anchor = pattern[-trailing_anchor_length:]
|
|
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
|
+
# Single literal has the length of 1, but quantifiers could be != 1, which means we can't merge them
|
|
85
|
+
if op == LITERAL and (
|
|
86
|
+
(min_length is not None and min_length > 1) or (max_length is not None and max_length < 1)
|
|
87
|
+
):
|
|
88
|
+
return pattern
|
|
89
|
+
return leading_anchor + _update_quantifier(op, value, inner_pattern, min_length, max_length) + trailing_anchor
|
|
90
|
+
elif (
|
|
91
|
+
len(parsed) > 3
|
|
92
|
+
and parsed[0][0] == ANCHOR
|
|
93
|
+
and parsed[-1][0] == ANCHOR
|
|
94
|
+
and all(op == LITERAL or op in REPEATS for op, _ in parsed[1:-1])
|
|
95
|
+
):
|
|
96
|
+
return _handle_anchored_pattern(parsed, pattern, min_length, max_length)
|
|
97
|
+
return pattern
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _matches_anything(value: list) -> bool:
|
|
101
|
+
"""Check if the given pattern is equivalent to '.' (match any character)."""
|
|
102
|
+
# Common forms: [\w\W], [\s\S], etc.
|
|
103
|
+
return value in (
|
|
104
|
+
[(sre.CATEGORY, sre.CATEGORY_WORD), (sre.CATEGORY, sre.CATEGORY_NOT_WORD)],
|
|
105
|
+
[(sre.CATEGORY, sre.CATEGORY_SPACE), (sre.CATEGORY, sre.CATEGORY_NOT_SPACE)],
|
|
106
|
+
[(sre.CATEGORY, sre.CATEGORY_DIGIT), (sre.CATEGORY, sre.CATEGORY_NOT_DIGIT)],
|
|
107
|
+
[(sre.CATEGORY, sre.CATEGORY_NOT_WORD), (sre.CATEGORY, sre.CATEGORY_WORD)],
|
|
108
|
+
[(sre.CATEGORY, sre.CATEGORY_NOT_SPACE), (sre.CATEGORY, sre.CATEGORY_SPACE)],
|
|
109
|
+
[(sre.CATEGORY, sre.CATEGORY_NOT_DIGIT), (sre.CATEGORY, sre.CATEGORY_DIGIT)],
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _handle_anchored_pattern(parsed: list, pattern: str, min_length: int | None, max_length: int | None) -> str:
|
|
114
|
+
"""Update regex pattern with multiple quantified patterns to satisfy length constraints."""
|
|
115
|
+
# Extract anchors
|
|
116
|
+
leading_anchor_length = _get_anchor_length(parsed[0][1])
|
|
117
|
+
trailing_anchor_length = _get_anchor_length(parsed[-1][1])
|
|
118
|
+
leading_anchor = pattern[:leading_anchor_length]
|
|
119
|
+
trailing_anchor = pattern[-trailing_anchor_length:]
|
|
120
|
+
|
|
121
|
+
pattern_parts = parsed[1:-1]
|
|
122
|
+
|
|
123
|
+
# Calculate total fixed length and per-repetition lengths
|
|
124
|
+
fixed_length = 0
|
|
125
|
+
quantifier_bounds = []
|
|
126
|
+
repetition_lengths = []
|
|
127
|
+
|
|
128
|
+
for op, value in pattern_parts:
|
|
129
|
+
if op in (LITERAL, NOT_LITERAL):
|
|
130
|
+
fixed_length += 1
|
|
131
|
+
elif op in REPEATS:
|
|
132
|
+
min_repeat, max_repeat, subpattern = value
|
|
133
|
+
quantifier_bounds.append((min_repeat, max_repeat))
|
|
134
|
+
repetition_lengths.append(_calculate_min_repetition_length(subpattern))
|
|
135
|
+
|
|
136
|
+
# Adjust length constraints by subtracting fixed literals length
|
|
137
|
+
if min_length is not None:
|
|
138
|
+
min_length -= fixed_length
|
|
139
|
+
if min_length < 0:
|
|
140
|
+
return pattern
|
|
141
|
+
if max_length is not None:
|
|
142
|
+
max_length -= fixed_length
|
|
143
|
+
if max_length < 0:
|
|
144
|
+
return pattern
|
|
145
|
+
|
|
146
|
+
if not quantifier_bounds:
|
|
147
|
+
return pattern
|
|
148
|
+
|
|
149
|
+
length_distribution = _distribute_length_constraints(quantifier_bounds, repetition_lengths, min_length, max_length)
|
|
150
|
+
if not length_distribution:
|
|
151
|
+
return pattern
|
|
152
|
+
|
|
153
|
+
# Rebuild pattern with updated quantifiers
|
|
154
|
+
result = leading_anchor
|
|
155
|
+
current_position = leading_anchor_length
|
|
156
|
+
distribution_idx = 0
|
|
157
|
+
|
|
158
|
+
for op, value in pattern_parts:
|
|
159
|
+
if op == LITERAL:
|
|
160
|
+
# Check if the literal comes from a bracketed expression,
|
|
161
|
+
# e.g. Python regex parses "[+]" as a single LITERAL token.
|
|
162
|
+
if pattern[current_position] == "[":
|
|
163
|
+
# Find the matching closing bracket.
|
|
164
|
+
end_idx = current_position + 1
|
|
165
|
+
while end_idx < len(pattern):
|
|
166
|
+
# Check for an unescaped closing bracket.
|
|
167
|
+
if pattern[end_idx] == "]" and (end_idx == current_position + 1 or pattern[end_idx - 1] != "\\"):
|
|
168
|
+
end_idx += 1
|
|
169
|
+
break
|
|
170
|
+
end_idx += 1
|
|
171
|
+
# Append the entire character set.
|
|
172
|
+
result += pattern[current_position:end_idx]
|
|
173
|
+
current_position = end_idx
|
|
174
|
+
continue
|
|
175
|
+
if pattern[current_position] == "\\":
|
|
176
|
+
# Escaped value
|
|
177
|
+
result += "\\"
|
|
178
|
+
# Could be an octal value
|
|
179
|
+
if (
|
|
180
|
+
current_position + 2 < len(pattern)
|
|
181
|
+
and pattern[current_position + 1] == "0"
|
|
182
|
+
and pattern[current_position + 2] in ("0", "1", "2", "3", "4", "5", "6", "7")
|
|
183
|
+
):
|
|
184
|
+
result += pattern[current_position + 1]
|
|
185
|
+
result += pattern[current_position + 2]
|
|
186
|
+
current_position += 3
|
|
187
|
+
continue
|
|
188
|
+
current_position += 2
|
|
189
|
+
else:
|
|
190
|
+
current_position += 1
|
|
191
|
+
result += chr(value)
|
|
192
|
+
else:
|
|
193
|
+
new_min, new_max = length_distribution[distribution_idx]
|
|
194
|
+
next_position = _find_quantified_end(pattern, current_position)
|
|
195
|
+
quantified_segment = pattern[current_position:next_position]
|
|
196
|
+
_, _, subpattern = value
|
|
197
|
+
new_value = (new_min, new_max, subpattern)
|
|
198
|
+
|
|
199
|
+
result += _update_quantifier(op, new_value, quantified_segment, new_min, new_max)
|
|
200
|
+
current_position = next_position
|
|
201
|
+
distribution_idx += 1
|
|
202
|
+
|
|
203
|
+
return result + trailing_anchor
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _find_quantified_end(pattern: str, start: int) -> int:
|
|
207
|
+
"""Find the end position of current quantified part."""
|
|
208
|
+
char_class_level = 0
|
|
209
|
+
group_level = 0
|
|
210
|
+
|
|
211
|
+
for i in range(start, len(pattern)):
|
|
212
|
+
char = pattern[i]
|
|
213
|
+
|
|
214
|
+
# Handle character class nesting
|
|
215
|
+
if char == "[":
|
|
216
|
+
char_class_level += 1
|
|
217
|
+
elif char == "]":
|
|
218
|
+
char_class_level -= 1
|
|
219
|
+
|
|
220
|
+
# Handle group nesting
|
|
221
|
+
elif char == "(":
|
|
222
|
+
group_level += 1
|
|
223
|
+
elif char == ")":
|
|
224
|
+
group_level -= 1
|
|
225
|
+
|
|
226
|
+
# Only process quantifiers when we're not inside any nested structure
|
|
227
|
+
elif char_class_level == 0 and group_level == 0:
|
|
228
|
+
if char in "*+?":
|
|
229
|
+
return i + 1
|
|
230
|
+
elif char == "{":
|
|
231
|
+
# Find matching }
|
|
232
|
+
while i < len(pattern) and pattern[i] != "}":
|
|
233
|
+
i += 1
|
|
234
|
+
return i + 1
|
|
235
|
+
|
|
236
|
+
return len(pattern)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _distribute_length_constraints(
|
|
240
|
+
bounds: list[tuple[int, int]], repetition_lengths: list[int], min_length: int | None, max_length: int | None
|
|
241
|
+
) -> list[tuple[int, int]] | None:
|
|
242
|
+
"""Distribute length constraints among quantified pattern parts."""
|
|
243
|
+
# Handle exact length case with dynamic programming
|
|
244
|
+
if min_length == max_length:
|
|
245
|
+
assert min_length is not None
|
|
246
|
+
target = min_length
|
|
247
|
+
dp: dict[tuple[int, int], list[tuple[int, ...]] | None] = {}
|
|
248
|
+
|
|
249
|
+
def find_valid_combination(pos: int, remaining: int) -> list[tuple[int, ...]] | None:
|
|
250
|
+
if (pos, remaining) in dp:
|
|
251
|
+
return dp[(pos, remaining)]
|
|
252
|
+
|
|
253
|
+
if pos == len(bounds):
|
|
254
|
+
return [()] if remaining == 0 else None
|
|
255
|
+
|
|
256
|
+
max_repeat: int
|
|
257
|
+
min_repeat, max_repeat = bounds[pos]
|
|
258
|
+
repeat_length = repetition_lengths[pos]
|
|
259
|
+
|
|
260
|
+
if max_repeat == MAXREPEAT:
|
|
261
|
+
max_repeat = remaining // repeat_length + 1 if repeat_length > 0 else remaining + 1
|
|
262
|
+
|
|
263
|
+
# Try each possible length for current quantifier
|
|
264
|
+
for repeat_count in range(min_repeat, max_repeat + 1):
|
|
265
|
+
used_length = repeat_count * repeat_length
|
|
266
|
+
if used_length > remaining:
|
|
267
|
+
break
|
|
268
|
+
|
|
269
|
+
rest = find_valid_combination(pos + 1, remaining - used_length)
|
|
270
|
+
if rest is not None:
|
|
271
|
+
dp[(pos, remaining)] = [(repeat_count,) + r for r in rest]
|
|
272
|
+
return dp[(pos, remaining)]
|
|
273
|
+
|
|
274
|
+
dp[(pos, remaining)] = None
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
distribution = find_valid_combination(0, target)
|
|
278
|
+
if distribution:
|
|
279
|
+
return [(length, length) for length in distribution[0]]
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
# Handle range case by distributing min/max bounds
|
|
283
|
+
result = []
|
|
284
|
+
remaining_min = min_length or 0
|
|
285
|
+
remaining_max = max_length or MAXREPEAT
|
|
286
|
+
|
|
287
|
+
for min_repeat, max_repeat in bounds:
|
|
288
|
+
if remaining_min > 0:
|
|
289
|
+
part_min = min(max_repeat, max(min_repeat, remaining_min))
|
|
290
|
+
else:
|
|
291
|
+
part_min = min_repeat
|
|
292
|
+
|
|
293
|
+
if remaining_max < MAXREPEAT:
|
|
294
|
+
part_max = min(max_repeat, remaining_max)
|
|
295
|
+
else:
|
|
296
|
+
part_max = max_repeat
|
|
297
|
+
|
|
298
|
+
if part_min > part_max:
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
result.append((part_min, part_max))
|
|
302
|
+
|
|
303
|
+
remaining_min = max(0, remaining_min - part_min)
|
|
304
|
+
remaining_max -= part_max if part_max != MAXREPEAT else 0
|
|
305
|
+
|
|
306
|
+
if remaining_min > 0 or remaining_max < 0:
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
return result
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _calculate_min_repetition_length(subpattern: list) -> int:
|
|
313
|
+
"""Calculate minimum length contribution per repetition of a quantified group."""
|
|
314
|
+
total = 0
|
|
315
|
+
for op, value in subpattern:
|
|
316
|
+
if op in [LITERAL, NOT_LITERAL, IN, sre.ANY]:
|
|
317
|
+
total += 1
|
|
318
|
+
elif op == sre.SUBPATTERN:
|
|
319
|
+
_, _, _, inner_pattern = value
|
|
320
|
+
total += _calculate_min_repetition_length(inner_pattern)
|
|
321
|
+
elif op in REPEATS:
|
|
322
|
+
min_repeat, _, inner_pattern = value
|
|
323
|
+
inner_min = _calculate_min_repetition_length(inner_pattern)
|
|
324
|
+
total += min_repeat * inner_min
|
|
325
|
+
return total
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _get_anchor_length(node_type: int) -> int:
|
|
329
|
+
"""Determine the length of the anchor based on its type."""
|
|
330
|
+
if node_type in {sre.AT_BEGINNING_STRING, sre.AT_END_STRING, sre.AT_BOUNDARY, sre.AT_NON_BOUNDARY}:
|
|
331
|
+
return 2 # \A, \Z, \b, or \B
|
|
332
|
+
return 1 # ^ or $ or their multiline/locale/unicode variants
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _update_quantifier(
|
|
336
|
+
op: int, value: tuple | None, pattern: str, min_length: int | None, max_length: int | None
|
|
337
|
+
) -> str:
|
|
338
|
+
"""Update the quantifier based on the operation type and given constraints."""
|
|
339
|
+
if op in REPEATS and value is not None:
|
|
340
|
+
return _handle_repeat_quantifier(value, pattern, min_length, max_length)
|
|
341
|
+
if op in (LITERAL, NOT_LITERAL, IN) and max_length != 0:
|
|
342
|
+
return _handle_literal_or_in_quantifier(pattern, min_length, max_length)
|
|
343
|
+
if op == sre.ANY and value is None:
|
|
344
|
+
# Equivalent to `.` which is in turn is the same as `.{1}`
|
|
345
|
+
return _handle_repeat_quantifier(
|
|
346
|
+
SINGLE_ANY,
|
|
347
|
+
pattern,
|
|
348
|
+
min_length,
|
|
349
|
+
max_length,
|
|
350
|
+
)
|
|
351
|
+
return pattern
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
SINGLE_ANY = sre_parse.parse(".{1}")[0][1]
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _handle_repeat_quantifier(
|
|
358
|
+
value: tuple[int, int, tuple], pattern: str, min_length: int | None, max_length: int | None
|
|
359
|
+
) -> str:
|
|
360
|
+
"""Handle repeat quantifiers (e.g., '+', '*', '?')."""
|
|
361
|
+
min_repeat, max_repeat, _ = value
|
|
362
|
+
|
|
363
|
+
# First, analyze the inner pattern
|
|
364
|
+
inner = _strip_quantifier(pattern)
|
|
365
|
+
if inner.startswith("(") and inner.endswith(")"):
|
|
366
|
+
inner = inner[1:-1]
|
|
367
|
+
|
|
368
|
+
# Determine the length of the inner pattern
|
|
369
|
+
inner_length = 1 # default assumption for non-literal patterns
|
|
370
|
+
try:
|
|
371
|
+
parsed = sre_parse.parse(inner)
|
|
372
|
+
if all(item[0] == LITERAL for item in parsed):
|
|
373
|
+
inner_length = len(parsed)
|
|
374
|
+
if max_length and max_length > 0 and inner_length > max_length:
|
|
375
|
+
return pattern
|
|
376
|
+
except re.error:
|
|
377
|
+
pass
|
|
378
|
+
|
|
379
|
+
if inner_length == 0:
|
|
380
|
+
# Empty pattern contributes 0 chars regardless of repetitions
|
|
381
|
+
# For length constraints, only 0 repetitions make sense
|
|
382
|
+
if min_length is not None and min_length > 0:
|
|
383
|
+
return pattern # Can't satisfy positive length with empty pattern
|
|
384
|
+
return f"({inner})" + _build_quantifier(0, 0)
|
|
385
|
+
|
|
386
|
+
# Convert external length constraints to repetition constraints
|
|
387
|
+
external_min_repeat = None
|
|
388
|
+
external_max_repeat = None
|
|
389
|
+
|
|
390
|
+
if min_length is not None:
|
|
391
|
+
# Need at least ceil(min_length / inner_length) repetitions
|
|
392
|
+
external_min_repeat = (min_length + inner_length - 1) // inner_length
|
|
393
|
+
|
|
394
|
+
if max_length is not None:
|
|
395
|
+
# Can have at most floor(max_length / inner_length) repetitions
|
|
396
|
+
external_max_repeat = max_length // inner_length
|
|
397
|
+
|
|
398
|
+
# Merge original repetition constraints with external constraints
|
|
399
|
+
final_min_repeat = min_repeat
|
|
400
|
+
if external_min_repeat is not None:
|
|
401
|
+
final_min_repeat = max(min_repeat, external_min_repeat)
|
|
402
|
+
|
|
403
|
+
final_max_repeat = max_repeat
|
|
404
|
+
if external_max_repeat is not None:
|
|
405
|
+
if max_repeat == MAXREPEAT:
|
|
406
|
+
final_max_repeat = external_max_repeat
|
|
407
|
+
else:
|
|
408
|
+
final_max_repeat = min(max_repeat, external_max_repeat)
|
|
409
|
+
|
|
410
|
+
if final_min_repeat > final_max_repeat:
|
|
411
|
+
return pattern
|
|
412
|
+
|
|
413
|
+
return f"({inner})" + _build_quantifier(final_min_repeat, final_max_repeat)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _handle_literal_or_in_quantifier(pattern: str, min_length: int | None, max_length: int | None) -> str:
|
|
417
|
+
"""Handle literal or character class quantifiers."""
|
|
418
|
+
min_length = 1 if min_length is None else max(min_length, 1)
|
|
419
|
+
if pattern.startswith("(") and pattern.endswith(")"):
|
|
420
|
+
pattern = pattern[1:-1]
|
|
421
|
+
return f"({pattern})" + _build_quantifier(min_length, max_length)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _build_quantifier(minimum: int | None, maximum: int | None) -> str:
|
|
425
|
+
"""Construct a quantifier string based on min and max values."""
|
|
426
|
+
if maximum == MAXREPEAT or maximum is None:
|
|
427
|
+
return f"{{{minimum or 0},}}"
|
|
428
|
+
if minimum == maximum:
|
|
429
|
+
return f"{{{minimum}}}"
|
|
430
|
+
return f"{{{minimum or 0},{maximum}}}"
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _build_size(min_repeat: int, max_repeat: int, min_length: int | None, max_length: int | None) -> tuple[int, int]:
|
|
434
|
+
"""Merge the current repetition constraints with the provided min and max lengths."""
|
|
435
|
+
if min_length is not None:
|
|
436
|
+
min_repeat = max(min_repeat, min_length)
|
|
437
|
+
if max_length is not None:
|
|
438
|
+
if max_repeat == MAXREPEAT:
|
|
439
|
+
max_repeat = max_length
|
|
440
|
+
else:
|
|
441
|
+
max_repeat = min(max_repeat, max_length)
|
|
442
|
+
return min_repeat, max_repeat
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _strip_quantifier(pattern: str) -> str:
|
|
446
|
+
"""Remove quantifier from the pattern."""
|
|
447
|
+
# Lazy & posessive quantifiers
|
|
448
|
+
for marker in ("*?", "+?", "??", "*+", "?+", "++"):
|
|
449
|
+
if pattern.endswith(marker) and not pattern.endswith(rf"\{marker}"):
|
|
450
|
+
return pattern[:-2]
|
|
451
|
+
for marker in ("?", "*", "+"):
|
|
452
|
+
if pattern.endswith(marker) and not pattern.endswith(rf"\{marker}"):
|
|
453
|
+
pattern = pattern[:-1]
|
|
454
|
+
if pattern.endswith("}") and "{" in pattern:
|
|
455
|
+
# Find the start of the exact quantifier and drop everything since that index
|
|
456
|
+
idx = pattern.rfind("{")
|
|
457
|
+
pattern = pattern[:idx]
|
|
458
|
+
return pattern
|
|
@@ -1,115 +1,94 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
2
4
|
from functools import lru_cache
|
|
3
|
-
from typing import Any, Callable, Dict,
|
|
5
|
+
from typing import Any, Callable, Dict, Union
|
|
4
6
|
from urllib.request import urlopen
|
|
5
7
|
|
|
6
|
-
import jsonschema
|
|
7
8
|
import requests
|
|
8
|
-
import yaml
|
|
9
|
-
|
|
10
|
-
from ...constants import DEFAULT_RESPONSE_TIMEOUT
|
|
11
|
-
from ...utils import StringDatesYAMLLoader
|
|
12
|
-
from .converter import to_json_schema_recursive
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
from schemathesis.core.compat import RefResolutionError, RefResolver
|
|
11
|
+
from schemathesis.core.deserialization import deserialize_yaml
|
|
12
|
+
from schemathesis.core.errors import RemoteDocumentError
|
|
13
|
+
from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
|
|
16
14
|
|
|
17
15
|
|
|
18
|
-
def load_file_impl(location: str, opener: Callable) ->
|
|
16
|
+
def load_file_impl(location: str, opener: Callable) -> dict[str, Any]:
|
|
19
17
|
"""Load a schema from the given file."""
|
|
20
18
|
with opener(location) as fd:
|
|
21
|
-
return
|
|
19
|
+
return deserialize_yaml(fd)
|
|
22
20
|
|
|
23
21
|
|
|
24
|
-
@lru_cache
|
|
25
|
-
def load_file(location: str) ->
|
|
22
|
+
@lru_cache
|
|
23
|
+
def load_file(location: str) -> dict[str, Any]:
|
|
26
24
|
"""Load a schema from the given file."""
|
|
27
25
|
return load_file_impl(location, open)
|
|
28
26
|
|
|
29
27
|
|
|
30
|
-
@lru_cache
|
|
31
|
-
def load_file_uri(location: str) ->
|
|
28
|
+
@lru_cache
|
|
29
|
+
def load_file_uri(location: str) -> dict[str, Any]:
|
|
32
30
|
"""Load a schema from the given file uri."""
|
|
33
31
|
return load_file_impl(location, urlopen)
|
|
34
32
|
|
|
35
33
|
|
|
34
|
+
_HTML_MARKERS = (b"<!doctype", b"<html", b"<head", b"<body")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _looks_like_html(content_type: str | None, body: bytes) -> bool:
|
|
38
|
+
if content_type and "html" in content_type.lower():
|
|
39
|
+
return True
|
|
40
|
+
head = body.lstrip()[:64].lower()
|
|
41
|
+
return any(head.startswith(m) for m in _HTML_MARKERS)
|
|
42
|
+
|
|
43
|
+
|
|
36
44
|
def load_remote_uri(uri: str) -> Any:
|
|
37
45
|
"""Load the resource and parse it as YAML / JSON."""
|
|
38
|
-
response = requests.get(uri, timeout=DEFAULT_RESPONSE_TIMEOUT
|
|
39
|
-
|
|
46
|
+
response = requests.get(uri, timeout=DEFAULT_RESPONSE_TIMEOUT)
|
|
47
|
+
content_type = response.headers.get("Content-Type", "")
|
|
48
|
+
body = response.content or b""
|
|
40
49
|
|
|
50
|
+
def _suffix() -> str:
|
|
51
|
+
return f"(HTTP {response.status_code}, Content-Type={content_type}, size={len(body)})"
|
|
41
52
|
|
|
42
|
-
|
|
53
|
+
if not (200 <= response.status_code < 300):
|
|
54
|
+
raise RemoteDocumentError(f"Failed to fetch {_suffix()}")
|
|
43
55
|
|
|
56
|
+
if _looks_like_html(content_type, body):
|
|
57
|
+
raise RemoteDocumentError(f"Expected YAML/JSON, got HTML {_suffix()}")
|
|
44
58
|
|
|
45
|
-
|
|
46
|
-
"""Inlines resolved schemas."""
|
|
59
|
+
document = deserialize_yaml(response.content)
|
|
47
60
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
61
|
+
if not isinstance(document, (dict, list)):
|
|
62
|
+
raise RemoteDocumentError(
|
|
63
|
+
f"Remote document is parsed as {type(document).__name__}, but an object/array is expected {_suffix()}"
|
|
51
64
|
)
|
|
52
|
-
super().__init__(*args, **kwargs)
|
|
53
65
|
|
|
54
|
-
|
|
55
|
-
def resolve_all(
|
|
56
|
-
self, item: Dict[str, Any], recursion_level: int = 0
|
|
57
|
-
) -> Dict[str, Any]: # pylint: disable=function-redefined
|
|
58
|
-
pass
|
|
59
|
-
|
|
60
|
-
@overload # pragma: no mutate
|
|
61
|
-
def resolve_all(self, item: List, recursion_level: int = 0) -> List: # pylint: disable=function-redefined
|
|
62
|
-
pass
|
|
63
|
-
|
|
64
|
-
# pylint: disable=function-redefined
|
|
65
|
-
def resolve_all(self, item: JSONType, recursion_level: int = 0) -> JSONType:
|
|
66
|
-
"""Recursively resolve all references in the given object."""
|
|
67
|
-
if recursion_level > RECURSION_DEPTH_LIMIT:
|
|
68
|
-
return item
|
|
69
|
-
if isinstance(item, dict):
|
|
70
|
-
ref = item.get("$ref")
|
|
71
|
-
if ref is not None and isinstance(ref, str):
|
|
72
|
-
with self.resolving(ref) as resolved:
|
|
73
|
-
# If the next level of recursion exceeds the limit, then we need to copy it explicitly
|
|
74
|
-
# In other cases, this method create new objects for mutable types (dict & list)
|
|
75
|
-
next_recursion_level = recursion_level + 1
|
|
76
|
-
if next_recursion_level > RECURSION_DEPTH_LIMIT:
|
|
77
|
-
return deepcopy(resolved)
|
|
78
|
-
return self.resolve_all(resolved, next_recursion_level)
|
|
79
|
-
return {key: self.resolve_all(sub_item, recursion_level) for key, sub_item in item.items()}
|
|
80
|
-
if isinstance(item, list):
|
|
81
|
-
return [self.resolve_all(sub_item, recursion_level) for sub_item in item]
|
|
82
|
-
return item
|
|
83
|
-
|
|
84
|
-
def resolve_in_scope(self, definition: Dict[str, Any], scope: str) -> Tuple[List[str], Dict[str, Any]]:
|
|
85
|
-
scopes = [scope]
|
|
86
|
-
# if there is `$ref` then we have a scope change that should be used during validation later to
|
|
87
|
-
# resolve nested references correctly
|
|
88
|
-
if "$ref" in definition:
|
|
89
|
-
self.push_scope(scope)
|
|
90
|
-
try:
|
|
91
|
-
new_scope, definition = deepcopy(self.resolve(definition["$ref"]))
|
|
92
|
-
finally:
|
|
93
|
-
self.pop_scope()
|
|
94
|
-
scopes.append(new_scope)
|
|
95
|
-
return scopes, definition
|
|
66
|
+
return document
|
|
96
67
|
|
|
97
68
|
|
|
98
|
-
|
|
99
|
-
"""Convert resolved OpenAPI schemas to JSON Schema.
|
|
69
|
+
JSONType = Union[None, bool, float, str, list, Dict[str, Any]]
|
|
100
70
|
|
|
101
|
-
When recursive schemas are validated we need to have resolved documents properly converted.
|
|
102
|
-
This approach is the simplest one, since this logic isolated in a single place.
|
|
103
|
-
"""
|
|
104
71
|
|
|
105
|
-
|
|
72
|
+
class ReferenceResolver(RefResolver):
|
|
73
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
74
|
+
kwargs.setdefault(
|
|
75
|
+
"handlers", {"file": load_file_uri, "": load_file, "http": load_remote_uri, "https": load_remote_uri}
|
|
76
|
+
)
|
|
106
77
|
super().__init__(*args, **kwargs)
|
|
107
|
-
self.nullable_name = nullable_name
|
|
108
|
-
self.is_response_schema = is_response_schema
|
|
109
78
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
79
|
+
if sys.version_info >= (3, 11):
|
|
80
|
+
|
|
81
|
+
def resolve(self, ref: str) -> tuple[str, Any]:
|
|
82
|
+
try:
|
|
83
|
+
return super().resolve(ref)
|
|
84
|
+
except RefResolutionError as exc:
|
|
85
|
+
exc.add_note(ref)
|
|
86
|
+
raise
|
|
87
|
+
else:
|
|
88
|
+
|
|
89
|
+
def resolve(self, ref: str) -> tuple[str, Any]:
|
|
90
|
+
try:
|
|
91
|
+
return super().resolve(ref)
|
|
92
|
+
except RefResolutionError as exc:
|
|
93
|
+
exc.__notes__ = [ref]
|
|
94
|
+
raise
|