Skip to content

Commit 6f2895e

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

File tree

5 files changed

+209
-23
lines changed

5 files changed

+209
-23
lines changed

docs/cli.rst

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,48 @@ 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
2941
3042
python -m openapi_spec_validator openapi.yaml
3143
32-
.. code-block:: bash
44+
.. code-block:: text
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: 77 additions & 10 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,10 +10,12 @@
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
1517
from openapi_spec_validator.validation import OpenAPIV31SpecValidator
18+
from openapi_spec_validator.validation import SpecValidator
1619

1720
logger = logging.getLogger(__name__)
1821
logging.basicConfig(
@@ -30,27 +33,42 @@ def print_error(filename: str, exc: Exception) -> None:
3033

3134

3235
def print_validationerror(
33-
filename: str, exc: ValidationError, errors: str = "best-match"
36+
filename: str,
37+
exc: ValidationError,
38+
subschema_errors: str = "best-match",
39+
index: int | None = None,
3440
) -> None:
35-
print(f"{filename}: Validation Error: {exc}")
41+
if index is None:
42+
print(f"{filename}: Validation Error: {exc}")
43+
else:
44+
print(f"{filename}: Validation Error: [{index}] {exc}")
3645
if exc.cause:
3746
print("\n# Cause\n")
3847
print(exc.cause)
3948
if not exc.context:
4049
return
41-
if errors == "all":
50+
if subschema_errors == "all":
4251
print("\n\n# Due to one of those errors\n")
4352
print("\n\n\n".join("## " + str(e) for e in exc.context))
44-
elif errors == "best-match":
53+
elif subschema_errors == "best-match":
4554
print("\n\n# Probably due to this subschema error\n")
4655
print("## " + str(best_match(exc.context)))
4756
if len(exc.context) > 1:
4857
print(
4958
f"\n({len(exc.context) - 1} more subschemas errors,",
50-
"use --errors=all to see them.)",
59+
"use --subschema-errors=all to see them.)",
5160
)
5261

5362

63+
def should_warn_deprecated() -> bool:
64+
return os.getenv("OPENAPI_SPEC_VALIDATOR_WARN_DEPRECATED", "1") != "0"
65+
66+
67+
def warn_deprecated(message: str) -> None:
68+
if should_warn_deprecated():
69+
print(f"DeprecationWarning: {message}", file=sys.stderr)
70+
71+
5472
def main(args: Sequence[str] | None = None) -> None:
5573
parser = ArgumentParser(prog="openapi-spec-validator")
5674
parser.add_argument(
@@ -59,12 +77,27 @@ def main(args: Sequence[str] | None = None) -> None:
5977
help="Validate specified file(s).",
6078
)
6179
parser.add_argument(
62-
"--errors",
80+
"--subschema-errors",
6381
choices=("best-match", "all"),
64-
default="best-match",
65-
help="""Control error reporting. Defaults to "best-match", """
82+
default=None,
83+
help="""Control subschema error details. Defaults to "best-match", """
6684
"""use "all" to get all subschema errors.""",
6785
)
86+
parser.add_argument(
87+
"--validation-errors",
88+
choices=("first", "all"),
89+
default="first",
90+
help="""Control validation errors count. Defaults to "first", """
91+
"""use "all" to get all validation errors.""",
92+
)
93+
parser.add_argument(
94+
"--errors",
95+
"--error",
96+
dest="deprecated_subschema_errors",
97+
choices=("best-match", "all"),
98+
default=None,
99+
help="Deprecated alias for --subschema-errors.",
100+
)
68101
parser.add_argument(
69102
"--schema",
70103
type=str,
@@ -80,6 +113,22 @@ def main(args: Sequence[str] | None = None) -> None:
80113
)
81114
args_parsed = parser.parse_args(args)
82115

116+
subschema_errors = args_parsed.subschema_errors
117+
if args_parsed.deprecated_subschema_errors is not None:
118+
if args_parsed.subschema_errors is None:
119+
subschema_errors = args_parsed.deprecated_subschema_errors
120+
warn_deprecated(
121+
"--errors/--error is deprecated. "
122+
"Use --subschema-errors instead."
123+
)
124+
else:
125+
warn_deprecated(
126+
"--errors/--error is deprecated and ignored when "
127+
"--subschema-errors is provided."
128+
)
129+
if subschema_errors is None:
130+
subschema_errors = "best-match"
131+
83132
for filename in args_parsed.file:
84133
# choose source
85134
reader = read_from_filename
@@ -95,7 +144,7 @@ def main(args: Sequence[str] | None = None) -> None:
95144
sys.exit(1)
96145

97146
# choose the validator
98-
validators = {
147+
validators: dict[str, type[SpecValidator] | None] = {
99148
"detect": None,
100149
"2.0": OpenAPIV2SpecValidator,
101150
"3.0": OpenAPIV30SpecValidator,
@@ -108,9 +157,27 @@ def main(args: Sequence[str] | None = None) -> None:
108157

109158
# validate
110159
try:
160+
if args_parsed.validation_errors == "all":
161+
if validator_cls is None:
162+
validator_cls = get_validator_cls(spec)
163+
validator = validator_cls(spec, base_uri=base_uri)
164+
errors = list(validator.iter_errors())
165+
if errors:
166+
for idx, err in enumerate(errors, start=1):
167+
print_validationerror(
168+
filename,
169+
err,
170+
subschema_errors,
171+
index=idx,
172+
)
173+
print(f"{filename}: {len(errors)} validation errors found")
174+
sys.exit(1)
175+
print_ok(filename)
176+
continue
177+
111178
validate(spec, base_uri=base_uri, cls=validator_cls)
112179
except ValidationError as exc:
113-
print_validationerror(filename, exc, args_parsed.errors)
180+
print_validationerror(filename, exc, subschema_errors)
114181
sys.exit(1)
115182
except Exception as exc:
116183
print_error(filename, exc)

openapi_spec_validator/validation/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from openapi_spec_validator.validation.validators import (
88
OpenAPIV31SpecValidator,
99
)
10+
from openapi_spec_validator.validation.validators import SpecValidator
1011

1112
__all__ = [
1213
"openapi_v2_spec_validator",
@@ -18,6 +19,7 @@
1819
"OpenAPIV3SpecValidator",
1920
"OpenAPIV30SpecValidator",
2021
"OpenAPIV31SpecValidator",
22+
"SpecValidator",
2123
]
2224

2325
# v2.0 spec

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)