schemathesis 4.0.0a3__py3-none-any.whl → 4.0.0a5__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/cli/__init__.py +3 -3
- schemathesis/cli/commands/run/__init__.py +159 -135
- schemathesis/cli/commands/run/checks.py +2 -3
- schemathesis/cli/commands/run/context.py +102 -19
- schemathesis/cli/commands/run/executor.py +33 -12
- schemathesis/cli/commands/run/filters.py +1 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +27 -46
- schemathesis/cli/commands/run/handlers/junitxml.py +1 -1
- schemathesis/cli/commands/run/handlers/output.py +238 -102
- schemathesis/cli/commands/run/hypothesis.py +14 -41
- schemathesis/cli/commands/run/reports.py +72 -0
- schemathesis/cli/commands/run/validation.py +18 -12
- schemathesis/cli/ext/groups.py +42 -13
- schemathesis/cli/ext/options.py +15 -8
- schemathesis/core/__init__.py +7 -1
- schemathesis/core/errors.py +79 -11
- schemathesis/core/failures.py +2 -1
- schemathesis/core/transforms.py +1 -1
- schemathesis/engine/config.py +2 -2
- schemathesis/engine/core.py +11 -1
- schemathesis/engine/errors.py +8 -3
- schemathesis/engine/events.py +7 -0
- schemathesis/engine/phases/__init__.py +16 -4
- schemathesis/engine/phases/stateful/_executor.py +1 -1
- schemathesis/engine/phases/unit/__init__.py +77 -53
- schemathesis/engine/phases/unit/_executor.py +28 -23
- schemathesis/engine/phases/unit/_pool.py +8 -0
- schemathesis/errors.py +6 -2
- schemathesis/experimental/__init__.py +0 -6
- schemathesis/filters.py +8 -0
- schemathesis/generation/coverage.py +6 -1
- schemathesis/generation/hypothesis/builder.py +222 -97
- schemathesis/generation/stateful/state_machine.py +49 -3
- schemathesis/openapi/checks.py +3 -1
- schemathesis/pytest/lazy.py +43 -5
- schemathesis/pytest/plugin.py +4 -4
- schemathesis/schemas.py +1 -1
- schemathesis/specs/openapi/checks.py +28 -11
- schemathesis/specs/openapi/examples.py +2 -5
- schemathesis/specs/openapi/expressions/__init__.py +22 -6
- schemathesis/specs/openapi/expressions/nodes.py +15 -21
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/parameters.py +0 -2
- schemathesis/specs/openapi/patterns.py +24 -7
- schemathesis/specs/openapi/schemas.py +13 -13
- schemathesis/specs/openapi/serialization.py +14 -0
- schemathesis/specs/openapi/stateful/__init__.py +96 -23
- schemathesis/specs/openapi/{links.py → stateful/links.py} +60 -16
- {schemathesis-4.0.0a3.dist-info → schemathesis-4.0.0a5.dist-info}/METADATA +7 -26
- {schemathesis-4.0.0a3.dist-info → schemathesis-4.0.0a5.dist-info}/RECORD +53 -52
- {schemathesis-4.0.0a3.dist-info → schemathesis-4.0.0a5.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a3.dist-info → schemathesis-4.0.0a5.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a3.dist-info → schemathesis-4.0.0a5.dist-info}/licenses/LICENSE +0 -0
@@ -134,7 +134,7 @@ def execute_state_machine_loop(
|
|
134
134
|
return result
|
135
135
|
|
136
136
|
def validate_response(
|
137
|
-
self, response: Response, case: Case, additional_checks: tuple[CheckFunction, ...] = ()
|
137
|
+
self, response: Response, case: Case, additional_checks: tuple[CheckFunction, ...] = (), **kwargs: Any
|
138
138
|
) -> None:
|
139
139
|
self.recorder.record_response(case_id=case.id, response=response)
|
140
140
|
ctx.collect_metric(case, response)
|
@@ -11,11 +11,12 @@ import warnings
|
|
11
11
|
from queue import Queue
|
12
12
|
from typing import TYPE_CHECKING, Any
|
13
13
|
|
14
|
+
from schemathesis.core.errors import InvalidSchema
|
14
15
|
from schemathesis.core.result import Ok
|
15
16
|
from schemathesis.engine import Status, events
|
16
17
|
from schemathesis.engine.phases import PhaseName, PhaseSkipReason
|
17
18
|
from schemathesis.engine.recorder import ScenarioRecorder
|
18
|
-
from schemathesis.generation.hypothesis.builder import HypothesisTestConfig
|
19
|
+
from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, HypothesisTestMode
|
19
20
|
from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
|
20
21
|
|
21
22
|
from ._pool import TaskProducer, WorkerPool
|
@@ -34,6 +35,12 @@ def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
|
|
34
35
|
Implemented as a producer-consumer pattern via a task queue.
|
35
36
|
The main thread provides an iterator over API operations and worker threads create test functions and run them.
|
36
37
|
"""
|
38
|
+
if phase.name == PhaseName.EXAMPLES:
|
39
|
+
mode = HypothesisTestMode.EXAMPLES
|
40
|
+
elif phase.name == PhaseName.COVERAGE:
|
41
|
+
mode = HypothesisTestMode.COVERAGE
|
42
|
+
else:
|
43
|
+
mode = HypothesisTestMode.FUZZING
|
37
44
|
producer = TaskProducer(engine)
|
38
45
|
workers_num = engine.config.execution.workers_num
|
39
46
|
|
@@ -45,7 +52,13 @@ def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
|
|
45
52
|
is_executed = False
|
46
53
|
|
47
54
|
with WorkerPool(
|
48
|
-
workers_num=workers_num,
|
55
|
+
workers_num=workers_num,
|
56
|
+
producer=producer,
|
57
|
+
worker_factory=worker_task,
|
58
|
+
ctx=engine,
|
59
|
+
mode=mode,
|
60
|
+
phase=phase.name,
|
61
|
+
suite_id=suite_started.id,
|
49
62
|
) as pool:
|
50
63
|
try:
|
51
64
|
while True:
|
@@ -74,7 +87,7 @@ def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
|
|
74
87
|
except KeyboardInterrupt:
|
75
88
|
engine.stop()
|
76
89
|
status = Status.INTERRUPTED
|
77
|
-
yield events.Interrupted(phase=
|
90
|
+
yield events.Interrupted(phase=phase.name)
|
78
91
|
|
79
92
|
if not is_executed:
|
80
93
|
phase.skip_reason = PhaseSkipReason.NOTHING_TO_TEST
|
@@ -86,13 +99,52 @@ def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
|
|
86
99
|
yield events.PhaseFinished(phase=phase, status=status, payload=None)
|
87
100
|
|
88
101
|
|
89
|
-
def worker_task(
|
90
|
-
|
102
|
+
def worker_task(
|
103
|
+
*,
|
104
|
+
events_queue: Queue,
|
105
|
+
producer: TaskProducer,
|
106
|
+
ctx: EngineContext,
|
107
|
+
mode: HypothesisTestMode,
|
108
|
+
phase: PhaseName,
|
109
|
+
suite_id: uuid.UUID,
|
110
|
+
) -> None:
|
111
|
+
from hypothesis.errors import HypothesisWarning, InvalidArgument
|
91
112
|
|
92
113
|
from schemathesis.generation.hypothesis.builder import create_test
|
93
114
|
|
94
115
|
from ._executor import run_test, test_func
|
95
116
|
|
117
|
+
def on_error(error: Exception, *, method: str | None = None, path: str | None = None) -> None:
|
118
|
+
if method and path:
|
119
|
+
label = f"{method.upper()} {path}"
|
120
|
+
scenario_started = events.ScenarioStarted(label=label, phase=phase, suite_id=suite_id)
|
121
|
+
events_queue.put(scenario_started)
|
122
|
+
|
123
|
+
events_queue.put(events.NonFatalError(error=error, phase=phase, label=label, related_to_operation=True))
|
124
|
+
|
125
|
+
events_queue.put(
|
126
|
+
events.ScenarioFinished(
|
127
|
+
id=scenario_started.id,
|
128
|
+
suite_id=suite_id,
|
129
|
+
phase=phase,
|
130
|
+
label=label,
|
131
|
+
status=Status.ERROR,
|
132
|
+
recorder=ScenarioRecorder(label="Error"),
|
133
|
+
elapsed_time=0.0,
|
134
|
+
skip_reason=None,
|
135
|
+
is_final=True,
|
136
|
+
)
|
137
|
+
)
|
138
|
+
else:
|
139
|
+
events_queue.put(
|
140
|
+
events.NonFatalError(
|
141
|
+
error=error,
|
142
|
+
phase=phase,
|
143
|
+
label=path or "-",
|
144
|
+
related_to_operation=False,
|
145
|
+
)
|
146
|
+
)
|
147
|
+
|
96
148
|
warnings.filterwarnings("ignore", message="The recursion limit will not be reset", category=HypothesisWarning)
|
97
149
|
with ignore_hypothesis_output():
|
98
150
|
try:
|
@@ -104,62 +156,34 @@ def worker_task(*, events_queue: Queue, producer: TaskProducer, ctx: EngineConte
|
|
104
156
|
if isinstance(result, Ok):
|
105
157
|
operation = result.ok()
|
106
158
|
as_strategy_kwargs = get_strategy_kwargs(ctx, operation)
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
159
|
+
try:
|
160
|
+
test_function = create_test(
|
161
|
+
operation=operation,
|
162
|
+
test_func=test_func,
|
163
|
+
config=HypothesisTestConfig(
|
164
|
+
modes=[mode],
|
165
|
+
settings=ctx.config.execution.hypothesis_settings,
|
166
|
+
seed=ctx.config.execution.seed,
|
167
|
+
generation=ctx.config.execution.generation,
|
168
|
+
as_strategy_kwargs=as_strategy_kwargs,
|
169
|
+
),
|
170
|
+
)
|
171
|
+
except (InvalidSchema, InvalidArgument) as exc:
|
172
|
+
on_error(exc, method=operation.method, path=operation.path)
|
173
|
+
continue
|
117
174
|
|
118
175
|
# The test is blocking, meaning that even if CTRL-C comes to the main thread, this tasks will continue
|
119
176
|
# executing. However, as we set a stop event, it will be checked before the next network request.
|
120
177
|
# However, this is still suboptimal, as there could be slow requests and they will block for longer
|
121
|
-
for event in run_test(
|
178
|
+
for event in run_test(
|
179
|
+
operation=operation, test_function=test_function, ctx=ctx, phase=phase, suite_id=suite_id
|
180
|
+
):
|
122
181
|
events_queue.put(event)
|
123
182
|
else:
|
124
183
|
error = result.err()
|
125
|
-
|
126
|
-
label = f"{error.method.upper()} {error.full_path}"
|
127
|
-
scenario_started = events.ScenarioStarted(
|
128
|
-
label=label, phase=PhaseName.UNIT_TESTING, suite_id=suite_id
|
129
|
-
)
|
130
|
-
events_queue.put(scenario_started)
|
131
|
-
|
132
|
-
events_queue.put(
|
133
|
-
events.NonFatalError(
|
134
|
-
error=error, phase=PhaseName.UNIT_TESTING, label=label, related_to_operation=True
|
135
|
-
)
|
136
|
-
)
|
137
|
-
|
138
|
-
events_queue.put(
|
139
|
-
events.ScenarioFinished(
|
140
|
-
id=scenario_started.id,
|
141
|
-
suite_id=suite_id,
|
142
|
-
phase=PhaseName.UNIT_TESTING,
|
143
|
-
label=label,
|
144
|
-
status=Status.ERROR,
|
145
|
-
recorder=ScenarioRecorder(label="Error"),
|
146
|
-
elapsed_time=0.0,
|
147
|
-
skip_reason=None,
|
148
|
-
is_final=True,
|
149
|
-
)
|
150
|
-
)
|
151
|
-
else:
|
152
|
-
assert error.full_path is not None
|
153
|
-
events_queue.put(
|
154
|
-
events.NonFatalError(
|
155
|
-
error=error,
|
156
|
-
phase=PhaseName.UNIT_TESTING,
|
157
|
-
label=error.full_path,
|
158
|
-
related_to_operation=False,
|
159
|
-
)
|
160
|
-
)
|
184
|
+
on_error(error, method=error.method, path=error.path)
|
161
185
|
except KeyboardInterrupt:
|
162
|
-
events_queue.put(events.Interrupted(phase=
|
186
|
+
events_queue.put(events.Interrupted(phase=phase))
|
163
187
|
|
164
188
|
|
165
189
|
def get_strategy_kwargs(ctx: EngineContext, operation: APIOperation) -> dict[str, Any]:
|
@@ -52,13 +52,17 @@ if TYPE_CHECKING:
|
|
52
52
|
|
53
53
|
|
54
54
|
def run_test(
|
55
|
-
*,
|
55
|
+
*,
|
56
|
+
operation: APIOperation,
|
57
|
+
test_function: Callable,
|
58
|
+
ctx: EngineContext,
|
59
|
+
phase: PhaseName,
|
60
|
+
suite_id: uuid.UUID,
|
56
61
|
) -> events.EventGenerator:
|
57
62
|
"""A single test run with all error handling needed."""
|
58
63
|
import hypothesis.errors
|
59
64
|
|
60
|
-
|
61
|
-
scenario_started = events.ScenarioStarted(label=operation.label, phase=PhaseName.UNIT_TESTING, suite_id=suite_id)
|
65
|
+
scenario_started = events.ScenarioStarted(label=operation.label, phase=phase, suite_id=suite_id)
|
62
66
|
yield scenario_started
|
63
67
|
errors: list[Exception] = []
|
64
68
|
skip_reason = None
|
@@ -66,8 +70,19 @@ def run_test(
|
|
66
70
|
recorder = ScenarioRecorder(label=operation.label)
|
67
71
|
|
68
72
|
def non_fatal_error(error: Exception) -> events.NonFatalError:
|
69
|
-
return events.NonFatalError(
|
70
|
-
|
73
|
+
return events.NonFatalError(error=error, phase=phase, label=operation.label, related_to_operation=True)
|
74
|
+
|
75
|
+
def scenario_finished(status: Status) -> events.ScenarioFinished:
|
76
|
+
return events.ScenarioFinished(
|
77
|
+
id=scenario_started.id,
|
78
|
+
suite_id=suite_id,
|
79
|
+
phase=phase,
|
80
|
+
label=operation.label,
|
81
|
+
recorder=recorder,
|
82
|
+
status=status,
|
83
|
+
elapsed_time=time.monotonic() - test_start_time,
|
84
|
+
skip_reason=skip_reason,
|
85
|
+
is_final=False,
|
71
86
|
)
|
72
87
|
|
73
88
|
try:
|
@@ -111,7 +126,8 @@ def run_test(
|
|
111
126
|
status = Status.ERROR
|
112
127
|
yield non_fatal_error(hypothesis.errors.Unsatisfiable("Failed to generate test cases for this API operation"))
|
113
128
|
except KeyboardInterrupt:
|
114
|
-
yield
|
129
|
+
yield scenario_finished(Status.INTERRUPTED)
|
130
|
+
yield events.Interrupted(phase=phase)
|
115
131
|
return
|
116
132
|
except AssertionError as exc: # May come from `hypothesis-jsonschema` or `hypothesis`
|
117
133
|
status = Status.ERROR
|
@@ -131,7 +147,6 @@ def run_test(
|
|
131
147
|
exc,
|
132
148
|
path=operation.path,
|
133
149
|
method=operation.method,
|
134
|
-
full_path=operation.schema.get_full_path(operation.path),
|
135
150
|
)
|
136
151
|
)
|
137
152
|
except HypothesisRefResolutionError:
|
@@ -165,7 +180,7 @@ def run_test(
|
|
165
180
|
yield non_fatal_error(exc)
|
166
181
|
if (
|
167
182
|
status == Status.SUCCESS
|
168
|
-
and ctx.config.execution.
|
183
|
+
and ctx.config.execution.continue_on_failure
|
169
184
|
and any(check.status == Status.FAILURE for checks in recorder.checks.values() for check in checks)
|
170
185
|
):
|
171
186
|
status = Status.FAILURE
|
@@ -194,20 +209,10 @@ def run_test(
|
|
194
209
|
if invalid_headers:
|
195
210
|
status = Status.ERROR
|
196
211
|
yield non_fatal_error(InvalidHeadersExample.from_headers(invalid_headers))
|
197
|
-
test_elapsed_time = time.monotonic() - test_start_time
|
198
212
|
for error in deduplicate_errors(errors):
|
199
213
|
yield non_fatal_error(error)
|
200
|
-
|
201
|
-
|
202
|
-
suite_id=suite_id,
|
203
|
-
phase=PhaseName.UNIT_TESTING,
|
204
|
-
label=operation.label,
|
205
|
-
recorder=recorder,
|
206
|
-
status=status,
|
207
|
-
elapsed_time=test_elapsed_time,
|
208
|
-
skip_reason=skip_reason,
|
209
|
-
is_final=False,
|
210
|
-
)
|
214
|
+
|
215
|
+
yield scenario_finished(status)
|
211
216
|
|
212
217
|
|
213
218
|
def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> None:
|
@@ -280,7 +285,7 @@ def test_func(*, ctx: EngineContext, case: Case, recorder: ScenarioRecorder) ->
|
|
280
285
|
ctx=ctx.get_check_context(recorder),
|
281
286
|
checks=ctx.config.execution.checks,
|
282
287
|
response=response,
|
283
|
-
|
288
|
+
continue_on_failure=ctx.config.execution.continue_on_failure,
|
284
289
|
recorder=recorder,
|
285
290
|
)
|
286
291
|
|
@@ -291,7 +296,7 @@ def validate_response(
|
|
291
296
|
ctx: CheckContext,
|
292
297
|
checks: Iterable[CheckFunction],
|
293
298
|
response: Response,
|
294
|
-
|
299
|
+
continue_on_failure: bool,
|
295
300
|
recorder: ScenarioRecorder,
|
296
301
|
) -> None:
|
297
302
|
failures = set()
|
@@ -318,5 +323,5 @@ def validate_response(
|
|
318
323
|
on_success=on_success,
|
319
324
|
)
|
320
325
|
|
321
|
-
if failures and not
|
326
|
+
if failures and not continue_on_failure:
|
322
327
|
raise FailureGroup(list(failures)) from None
|
@@ -7,9 +7,11 @@ from types import TracebackType
|
|
7
7
|
from typing import TYPE_CHECKING, Callable
|
8
8
|
|
9
9
|
from schemathesis.core.result import Result
|
10
|
+
from schemathesis.engine.phases import PhaseName
|
10
11
|
|
11
12
|
if TYPE_CHECKING:
|
12
13
|
from schemathesis.engine.context import EngineContext
|
14
|
+
from schemathesis.generation.hypothesis.builder import HypothesisTestMode
|
13
15
|
|
14
16
|
|
15
17
|
class TaskProducer:
|
@@ -34,12 +36,16 @@ class WorkerPool:
|
|
34
36
|
producer: TaskProducer,
|
35
37
|
worker_factory: Callable,
|
36
38
|
ctx: EngineContext,
|
39
|
+
mode: HypothesisTestMode,
|
40
|
+
phase: PhaseName,
|
37
41
|
suite_id: uuid.UUID,
|
38
42
|
) -> None:
|
39
43
|
self.workers_num = workers_num
|
40
44
|
self.producer = producer
|
41
45
|
self.worker_factory = worker_factory
|
42
46
|
self.ctx = ctx
|
47
|
+
self.mode = mode
|
48
|
+
self.phase = phase
|
43
49
|
self.suite_id = suite_id
|
44
50
|
self.workers: list[threading.Thread] = []
|
45
51
|
self.events_queue: Queue = Queue()
|
@@ -51,6 +57,8 @@ class WorkerPool:
|
|
51
57
|
target=self.worker_factory,
|
52
58
|
kwargs={
|
53
59
|
"ctx": self.ctx,
|
60
|
+
"mode": self.mode,
|
61
|
+
"phase": self.phase,
|
54
62
|
"events_queue": self.events_queue,
|
55
63
|
"producer": self.producer,
|
56
64
|
"suite_id": self.suite_id,
|
schemathesis/errors.py
CHANGED
@@ -4,17 +4,19 @@ from schemathesis.core.errors import (
|
|
4
4
|
IncorrectUsage,
|
5
5
|
InternalError,
|
6
6
|
InvalidHeadersExample,
|
7
|
-
InvalidLinkDefinition,
|
8
7
|
InvalidRateLimit,
|
9
8
|
InvalidRegexPattern,
|
10
9
|
InvalidRegexType,
|
11
10
|
InvalidSchema,
|
11
|
+
InvalidStateMachine,
|
12
|
+
InvalidTransition,
|
12
13
|
LoaderError,
|
13
14
|
NoLinksFound,
|
14
15
|
OperationNotFound,
|
15
16
|
SchemathesisError,
|
16
17
|
SerializationError,
|
17
18
|
SerializationNotPossible,
|
19
|
+
TransitionValidationError,
|
18
20
|
UnboundPrefix,
|
19
21
|
)
|
20
22
|
|
@@ -22,16 +24,18 @@ __all__ = [
|
|
22
24
|
"IncorrectUsage",
|
23
25
|
"InternalError",
|
24
26
|
"InvalidHeadersExample",
|
25
|
-
"InvalidLinkDefinition",
|
26
27
|
"InvalidRateLimit",
|
27
28
|
"InvalidRegexPattern",
|
28
29
|
"InvalidRegexType",
|
29
30
|
"InvalidSchema",
|
31
|
+
"InvalidStateMachine",
|
32
|
+
"InvalidTransition",
|
30
33
|
"LoaderError",
|
31
34
|
"OperationNotFound",
|
32
35
|
"NoLinksFound",
|
33
36
|
"SchemathesisError",
|
34
37
|
"SerializationError",
|
35
38
|
"SerializationNotPossible",
|
39
|
+
"TransitionValidationError",
|
36
40
|
"UnboundPrefix",
|
37
41
|
]
|
@@ -64,12 +64,6 @@ class ExperimentSet:
|
|
64
64
|
|
65
65
|
ENV_PREFIX = "SCHEMATHESIS_EXPERIMENTAL"
|
66
66
|
GLOBAL_EXPERIMENTS = ExperimentSet()
|
67
|
-
COVERAGE_PHASE = GLOBAL_EXPERIMENTS.create_experiment(
|
68
|
-
name="Coverage phase",
|
69
|
-
env_var="COVERAGE_PHASE",
|
70
|
-
description="Generate covering test cases",
|
71
|
-
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2418",
|
72
|
-
)
|
73
67
|
POSITIVE_DATA_ACCEPTANCE = GLOBAL_EXPERIMENTS.create_experiment(
|
74
68
|
name="Positive Data Acceptance",
|
75
69
|
env_var="POSITIVE_DATA_ACCEPTANCE",
|
schemathesis/filters.py
CHANGED
@@ -268,6 +268,8 @@ class FilterSet:
|
|
268
268
|
# To match anything the regex should match the expected value, hence passing them together is useless
|
269
269
|
raise IncorrectUsage(ERROR_EXPECTED_AND_REGEX)
|
270
270
|
if expected is not None:
|
271
|
+
if attribute == "method":
|
272
|
+
expected = _normalize_method(expected)
|
271
273
|
matchers.append(Matcher.for_value(attribute, expected))
|
272
274
|
if regex is not None:
|
273
275
|
matchers.append(Matcher.for_regex(attribute, regex))
|
@@ -283,6 +285,12 @@ class FilterSet:
|
|
283
285
|
self._excludes.add(filter_)
|
284
286
|
|
285
287
|
|
288
|
+
def _normalize_method(value: FilterValue) -> FilterValue:
|
289
|
+
if isinstance(value, list):
|
290
|
+
return [item.upper() for item in value]
|
291
|
+
return value.upper()
|
292
|
+
|
293
|
+
|
286
294
|
def attach_filter_chain(
|
287
295
|
target: Callable,
|
288
296
|
attribute: str,
|
@@ -437,7 +437,12 @@ def cover_schema_iter(
|
|
437
437
|
elif key == "required":
|
438
438
|
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
439
439
|
yield from _negative_required(ctx, template, value)
|
440
|
-
elif
|
440
|
+
elif (
|
441
|
+
key == "additionalProperties"
|
442
|
+
and not value
|
443
|
+
and "pattern" not in schema
|
444
|
+
and schema.get("type") in ["object", None]
|
445
|
+
):
|
441
446
|
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
442
447
|
yield NegativeValue(
|
443
448
|
{**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE},
|