Skip to content

Commit bd78197

Browse files
committed
Split CLI error controls into validation and subschema modes
1 parent 5823ff8 commit bd78197

File tree

4 files changed

+204
-21
lines changed

4 files changed

+204
-21
lines changed

docs/cli.rst

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ CLI (Command Line Interface)
2323
2424
docker run -v path/to/openapi.yaml:/openapi.yaml --rm pythonopenapi/openapi-spec-validator /openapi.yaml
2525
26+
Show all validation errors:
27+
28+
.. code-block:: bash
29+
30+
docker run -v path/to/openapi.yaml:/openapi.yaml --rm pythonopenapi/openapi-spec-validator --validation-errors all /openapi.yaml
31+
32+
Show all validation errors and all subschema details:
33+
34+
.. code-block:: bash
35+
36+
docker run -v path/to/openapi.yaml:/openapi.yaml --rm pythonopenapi/openapi-spec-validator --validation-errors all --subschema-errors all /openapi.yaml
37+
2638
.. md-tab-item:: Python interpreter
2739

2840
.. code-block:: bash
@@ -31,19 +43,28 @@ CLI (Command Line Interface)
3143
3244
.. code-block:: bash
3345
34-
usage: openapi-spec-validator [-h] [--errors {best-match,all}]
35-
[--schema {2.0,3.0.0,3.1.0,detect}]
36-
filename
46+
usage: openapi-spec-validator [-h] [--subschema-errors {best-match,all}]
47+
[--validation-errors {first,all}]
48+
[--errors {best-match,all}] [--schema {detect,2.0,3.0,3.1}]
49+
[--version] file [file ...]
3750
3851
positional arguments:
39-
filename Absolute or relative path to file
52+
file Validate specified file(s).
4053
4154
options:
4255
-h, --help show this help message and exit
43-
--errors {best-match,all}
44-
Control error reporting. Defaults to "best-
45-
match", use "all" to get all subschema
46-
errors.
47-
--schema {2.0,3.0.0,3.1.0,detect}
48-
OpenAPI schema (default: detect)
49-
56+
--subschema-errors {best-match,all}
57+
Control subschema error details. Defaults to "best-match",
58+
use "all" to get all subschema errors.
59+
--validation-errors {first,all}
60+
Control validation errors count. Defaults to "first",
61+
use "all" to get all validation errors.
62+
--errors {best-match,all}, --error {best-match,all}
63+
Deprecated alias for --subschema-errors.
64+
--schema {detect,2.0,3.0,3.1}
65+
OpenAPI schema version (default: detect).
66+
--version show program's version number and exit
67+
68+
Legacy note:
69+
``--errors`` / ``--error`` are deprecated and emit warnings by default.
70+
Set ``OPENAPI_SPEC_VALIDATOR_WARN_DEPRECATED=0`` to silence warnings.

docs/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ Usage
6363
6464
docker run -v path/to/openapi.yaml:/openapi.yaml --rm pythonopenapi/openapi-spec-validator /openapi.yaml
6565
66+
.. code-block:: bash
67+
68+
docker run -v path/to/openapi.yaml:/openapi.yaml --rm pythonopenapi/openapi-spec-validator --validation-errors all /openapi.yaml
69+
6670
.. md-tab-item:: Python interpreter
6771

6872
.. code-block:: bash

openapi_spec_validator/__main__.py

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import os
23
import sys
34
from argparse import ArgumentParser
45
from collections.abc import Sequence
@@ -9,6 +10,7 @@
910
from openapi_spec_validator import __version__
1011
from openapi_spec_validator.readers import read_from_filename
1112
from openapi_spec_validator.readers import read_from_stdin
13+
from openapi_spec_validator.shortcuts import get_validator_cls
1214
from openapi_spec_validator.shortcuts import validate
1315
from openapi_spec_validator.validation import OpenAPIV2SpecValidator
1416
from openapi_spec_validator.validation import OpenAPIV30SpecValidator
@@ -30,27 +32,42 @@ def print_error(filename: str, exc: Exception) -> None:
3032

3133

3234
def print_validationerror(
33-
filename: str, exc: ValidationError, errors: str = "best-match"
35+
filename: str,
36+
exc: ValidationError,
37+
subschema_errors: str = "best-match",
38+
index: int | None = None,
3439
) -> None:
35-
print(f"{filename}: Validation Error: {exc}")
40+
if index is None:
41+
print(f"{filename}: Validation Error: {exc}")
42+
else:
43+
print(f"{filename}: Validation Error: [{index}] {exc}")
3644
if exc.cause:
3745
print("\n# Cause\n")
3846
print(exc.cause)
3947
if not exc.context:
4048
return
41-
if errors == "all":
49+
if subschema_errors == "all":
4250
print("\n\n# Due to one of those errors\n")
4351
print("\n\n\n".join("## " + str(e) for e in exc.context))
44-
elif errors == "best-match":
52+
elif subschema_errors == "best-match":
4553
print("\n\n# Probably due to this subschema error\n")
4654
print("## " + str(best_match(exc.context)))
4755
if len(exc.context) > 1:
4856
print(
4957
f"\n({len(exc.context) - 1} more subschemas errors,",
50-
"use --errors=all to see them.)",
58+
"use --subschema-errors=all to see them.)",
5159
)
5260

5361

62+
def should_warn_deprecated() -> bool:
63+
return os.getenv("OPENAPI_SPEC_VALIDATOR_WARN_DEPRECATED", "1") != "0"
64+
65+
66+
def warn_deprecated(message: str) -> None:
67+
if should_warn_deprecated():
68+
print(f"DeprecationWarning: {message}", file=sys.stderr)
69+
70+
5471
def main(args: Sequence[str] | None = None) -> None:
5572
parser = ArgumentParser(prog="openapi-spec-validator")
5673
parser.add_argument(
@@ -59,12 +76,27 @@ def main(args: Sequence[str] | None = None) -> None:
5976
help="Validate specified file(s).",
6077
)
6178
parser.add_argument(
62-
"--errors",
79+
"--subschema-errors",
6380
choices=("best-match", "all"),
64-
default="best-match",
65-
help="""Control error reporting. Defaults to "best-match", """
81+
default=None,
82+
help="""Control subschema error details. Defaults to "best-match", """
6683
"""use "all" to get all subschema errors.""",
6784
)
85+
parser.add_argument(
86+
"--validation-errors",
87+
choices=("first", "all"),
88+
default="first",
89+
help="""Control validation errors count. Defaults to "first", """
90+
"""use "all" to get all validation errors.""",
91+
)
92+
parser.add_argument(
93+
"--errors",
94+
"--error",
95+
dest="deprecated_subschema_errors",
96+
choices=("best-match", "all"),
97+
default=None,
98+
help="Deprecated alias for --subschema-errors.",
99+
)
68100
parser.add_argument(
69101
"--schema",
70102
type=str,
@@ -80,6 +112,22 @@ def main(args: Sequence[str] | None = None) -> None:
80112
)
81113
args_parsed = parser.parse_args(args)
82114

115+
subschema_errors = args_parsed.subschema_errors
116+
if args_parsed.deprecated_subschema_errors is not None:
117+
if args_parsed.subschema_errors is None:
118+
subschema_errors = args_parsed.deprecated_subschema_errors
119+
warn_deprecated(
120+
"--errors/--error is deprecated. "
121+
"Use --subschema-errors instead."
122+
)
123+
else:
124+
warn_deprecated(
125+
"--errors/--error is deprecated and ignored when "
126+
"--subschema-errors is provided."
127+
)
128+
if subschema_errors is None:
129+
subschema_errors = "best-match"
130+
83131
for filename in args_parsed.file:
84132
# choose source
85133
reader = read_from_filename
@@ -108,9 +156,27 @@ def main(args: Sequence[str] | None = None) -> None:
108156

109157
# validate
110158
try:
159+
if args_parsed.validation_errors == "all":
160+
if validator_cls is None:
161+
validator_cls = get_validator_cls(spec)
162+
validator = validator_cls(spec, base_uri=base_uri)
163+
errors = list(validator.iter_errors())
164+
if errors:
165+
for idx, exc in enumerate(errors, start=1):
166+
print_validationerror(
167+
filename,
168+
exc,
169+
subschema_errors,
170+
index=idx,
171+
)
172+
print(f"{filename}: {len(errors)} validation errors found")
173+
sys.exit(1)
174+
print_ok(filename)
175+
continue
176+
111177
validate(spec, base_uri=base_uri, cls=validator_cls)
112178
except ValidationError as exc:
113-
print_validationerror(filename, exc, args_parsed.errors)
179+
print_validationerror(filename, exc, subschema_errors)
114180
sys.exit(1)
115181
except Exception as exc:
116182
print_error(filename, exc)

tests/integration/test_main.py

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def test_errors_on_missing_description_full(capsys):
101101
"""An error is obviously printed given an empty schema."""
102102
testargs = [
103103
"./tests/integration/data/v3.0/missing-description.yaml",
104-
"--errors=all",
104+
"--subschema-errors=all",
105105
"--schema=3.0.0",
106106
]
107107
with pytest.raises(SystemExit):
@@ -221,6 +221,98 @@ def test_malformed_schema_stdin(capsys):
221221
assert "stdin: OK" not in out
222222

223223

224+
def test_errors_all_lists_all_validation_errors(capsys):
225+
spec_io = StringIO(
226+
"""
227+
openapi: 3.0.0
228+
"""
229+
)
230+
231+
testargs = ["--validation-errors", "all", "--schema", "3.0.0", "-"]
232+
with mock.patch("openapi_spec_validator.__main__.sys.stdin", spec_io):
233+
with pytest.raises(SystemExit):
234+
main(testargs)
235+
236+
out, err = capsys.readouterr()
237+
assert not err
238+
assert "stdin: Validation Error: [1]" in out
239+
assert "stdin: Validation Error: [2]" in out
240+
assert "'info' is a required property" in out
241+
assert "'paths' is a required property" in out
242+
assert "stdin: 2 validation errors found" in out
243+
244+
245+
def test_error_alias_controls_subschema_errors_and_warns(capsys):
246+
testargs = [
247+
"./tests/integration/data/v3.0/missing-description.yaml",
248+
"--error",
249+
"all",
250+
"--schema=3.0.0",
251+
]
252+
with pytest.raises(SystemExit):
253+
main(testargs)
254+
255+
out, err = capsys.readouterr()
256+
assert "'$ref' is a required property" in out
257+
assert "validation errors found" not in out
258+
assert (
259+
"DeprecationWarning: --errors/--error is deprecated. "
260+
"Use --subschema-errors instead."
261+
) in err
262+
263+
264+
def test_error_alias_warning_can_be_disabled(capsys):
265+
testargs = [
266+
"./tests/integration/data/v3.0/missing-description.yaml",
267+
"--error",
268+
"all",
269+
"--schema=3.0.0",
270+
]
271+
with mock.patch.dict(
272+
"openapi_spec_validator.__main__.os.environ",
273+
{"OPENAPI_SPEC_VALIDATOR_WARN_DEPRECATED": "0"},
274+
clear=False,
275+
):
276+
with pytest.raises(SystemExit):
277+
main(testargs)
278+
279+
out, err = capsys.readouterr()
280+
assert "'$ref' is a required property" in out
281+
assert not err
282+
283+
284+
def test_deprecated_error_ignored_when_new_flag_used(capsys):
285+
spec_io = StringIO(
286+
"""
287+
openapi: 3.0.0
288+
"""
289+
)
290+
291+
testargs = [
292+
"--error",
293+
"all",
294+
"--subschema-errors",
295+
"best-match",
296+
"--validation-errors",
297+
"all",
298+
"--schema",
299+
"3.0.0",
300+
"-",
301+
]
302+
with mock.patch("openapi_spec_validator.__main__.sys.stdin", spec_io):
303+
with pytest.raises(SystemExit):
304+
main(testargs)
305+
306+
out, err = capsys.readouterr()
307+
assert "stdin: Validation Error: [1]" in out
308+
assert "# Probably due to this subschema error" not in out
309+
assert (
310+
"DeprecationWarning: --errors/--error is deprecated and ignored when "
311+
"--subschema-errors is provided."
312+
) in err
313+
assert "stdin: 2 validation errors found" in out
314+
315+
224316
def test_version(capsys):
225317
"""Test --version flag outputs correct version."""
226318
testargs = ["--version"]

0 commit comments

Comments
 (0)