Skip to content

Commit 39e8a67

Browse files
committed
Refactor error formatting, show parse error cause
- Move error formatting into a dedicated module - Add a new 'minimal' output mode to describe how we show detail on a parse error -- and in theory, other well-known errors - Unlike the previous behavior, the 'minimal' format includes presentation of the `__cause__` As a result, the output on a parse error will now include a one-line presentation of the underlying error. Ancillary things: - Fix type annotations for the new interface: use `Literal`s - Fix type annotations caused by `__cause__` handling (use `BaseException`) - Fix some changelog formatting resolves #581
1 parent 7eb53ab commit 39e8a67

File tree

6 files changed

+79
-53
lines changed

6 files changed

+79
-53
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ CHANGELOG
88
Unreleased
99
----------
1010

11-
- Add official support for Python 3.14
12-
- Add Citation File Format schema and pre-commit hook. Thanks :user:`edgarrmondragon`! (:issue:`502`)
13-
1411
.. vendor-insert-here
1512
1613
- Update vendored schemas: bitbucket-pipelines, buildkite, circle-ci, compose-spec,
1714
dependabot, gitlab-ci, meltano, mergify, renovate, snapcraft (2025-11-11)
15+
- Add official support for Python 3.14
16+
- Add Citation File Format schema and pre-commit hook. Thanks :user:`edgarrmondragon`! (:issue:`502`)
17+
- Improved default text output when parsing errors are encountered. (:issue:`581`)
1818

1919
0.34.1
2020
------

src/check_jsonschema/checker.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import jsonschema
88
import referencing.exceptions
99

10-
from . import utils
10+
from . import format_errors
1111
from .formats import FormatOptions
1212
from .instance_loader import InstanceLoader
1313
from .parsers import ParseError
@@ -31,7 +31,7 @@ def __init__(
3131
*,
3232
format_opts: FormatOptions,
3333
regex_impl: RegexImplementation,
34-
traceback_mode: str = "short",
34+
traceback_mode: t.Literal["minimal", "short", "full"] = "short",
3535
fill_defaults: bool = False,
3636
) -> None:
3737
self._schema_loader = schema_loader
@@ -46,7 +46,7 @@ def __init__(
4646
def _fail(self, msg: str, err: Exception | None = None) -> t.NoReturn:
4747
click.echo(msg, err=True)
4848
if err is not None:
49-
utils.print_error(err, mode=self._traceback_mode)
49+
format_errors.print_error(err, mode=self._traceback_mode)
5050
raise _Exit(1)
5151

5252
def get_validator(

src/check_jsonschema/cli/parse_result.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def __init__(self) -> None:
4141
self.regex_variant: RegexVariantName = RegexVariantName.default
4242
# error and output controls
4343
self.verbosity: int = 1
44-
self.traceback_mode: str = "short"
44+
self.traceback_mode: t.Literal["short", "full"] = "short"
4545
self.output_format: str = "text"
4646

4747
def set_regex_variant(
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from __future__ import annotations
2+
3+
import linecache
4+
import textwrap
5+
import traceback
6+
import typing as t
7+
8+
import click
9+
10+
11+
def format_error_message(err: BaseException) -> str:
12+
return f"{type(err).__name__}: {err}"
13+
14+
15+
def format_minimal_error(err: BaseException, *, indent: int = 0) -> str:
16+
lines = [textwrap.indent(str(err), indent * " ")]
17+
if err.__cause__ is not None:
18+
lines.append(
19+
textwrap.indent(format_error_message(err.__cause__), (indent + 2) * " ")
20+
)
21+
22+
return "\n".join(lines)
23+
24+
25+
def format_shortened_error(err: BaseException, *, indent: int = 0) -> str:
26+
lines = []
27+
lines.append(textwrap.indent(format_error_message(err), indent * " "))
28+
if err.__traceback__ is not None:
29+
lineno = err.__traceback__.tb_lineno
30+
tb_frame = err.__traceback__.tb_frame
31+
filename = tb_frame.f_code.co_filename
32+
line = linecache.getline(filename, lineno)
33+
lines.append((indent + 2) * " " + f'in "{filename}", line {lineno}')
34+
lines.append((indent + 2) * " " + ">>> " + line.strip())
35+
return "\n".join(lines)
36+
37+
38+
def format_shortened_trace(caught_err: BaseException) -> str:
39+
err_stack: list[BaseException] = [caught_err]
40+
while err_stack[-1].__context__ is not None:
41+
err_stack.append(err_stack[-1].__context__) # type: ignore[arg-type]
42+
43+
parts = [format_shortened_error(caught_err)]
44+
indent = 0
45+
for err in err_stack[1:]:
46+
indent += 2
47+
parts.append("\n" + indent * " " + "caused by\n")
48+
parts.append(format_shortened_error(err, indent=indent))
49+
return "\n".join(parts)
50+
51+
52+
def format_error(
53+
err: Exception, mode: t.Literal["minimal", "short", "full"] = "short"
54+
) -> str:
55+
if mode == "minimal":
56+
return format_minimal_error(err)
57+
elif mode == "short":
58+
return format_shortened_trace(err)
59+
else:
60+
return "".join(traceback.format_exception(type(err), err, err.__traceback__))
61+
62+
63+
def print_error(
64+
err: Exception, mode: t.Literal["minimal", "short", "full"] = "short"
65+
) -> None:
66+
click.echo(format_error(err, mode=mode), err=True)

src/check_jsonschema/reporter.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
import click
1414
import jsonschema
1515

16+
from . import format_errors
1617
from .parsers import ParseError
1718
from .result import CheckResult
18-
from .utils import format_error, iter_validation_error
19+
from .utils import iter_validation_error
1920

2021

2122
class Reporter(abc.ABC):
@@ -111,11 +112,12 @@ def _show_validation_error(
111112

112113
def _show_parse_error(self, filename: str, err: ParseError) -> None:
113114
if self.verbosity < 2:
114-
self._echo(click.style(str(err), fg="yellow"), indent=2)
115+
mode: t.Literal["minimal", "short", "full"] = "minimal"
115116
elif self.verbosity < 3:
116-
self._echo(textwrap.indent(format_error(err, mode="short"), " "))
117+
mode = "short"
117118
else:
118-
self._echo(textwrap.indent(format_error(err, mode="full"), " "))
119+
mode = "full"
120+
self._echo(textwrap.indent(format_errors.format_error(err, mode=mode), " "))
119121

120122
def report_errors(self, result: CheckResult) -> None:
121123
if self.verbosity < 1:

src/check_jsonschema/utils.py

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
from __future__ import annotations
22

3-
import linecache
43
import os
54
import pathlib
65
import re
7-
import textwrap
8-
import traceback
96
import typing as t
107
import urllib.parse
118
import urllib.request
129

13-
import click
1410
import jsonschema
1511

1612
WINDOWS = os.name == "nt"
@@ -95,44 +91,6 @@ def filename2path(filename: str) -> pathlib.Path:
9591
return p.resolve()
9692

9793

98-
def format_shortened_error(err: Exception, *, indent: int = 0) -> str:
99-
lines = []
100-
lines.append(textwrap.indent(f"{type(err).__name__}: {err}", indent * " "))
101-
if err.__traceback__ is not None:
102-
lineno = err.__traceback__.tb_lineno
103-
tb_frame = err.__traceback__.tb_frame
104-
filename = tb_frame.f_code.co_filename
105-
line = linecache.getline(filename, lineno)
106-
lines.append((indent + 2) * " " + f'in "{filename}", line {lineno}')
107-
lines.append((indent + 2) * " " + ">>> " + line.strip())
108-
return "\n".join(lines)
109-
110-
111-
def format_shortened_trace(caught_err: Exception) -> str:
112-
err_stack: list[Exception] = [caught_err]
113-
while err_stack[-1].__context__ is not None:
114-
err_stack.append(err_stack[-1].__context__) # type: ignore[arg-type]
115-
116-
parts = [format_shortened_error(caught_err)]
117-
indent = 0
118-
for err in err_stack[1:]:
119-
indent += 2
120-
parts.append("\n" + indent * " " + "caused by\n")
121-
parts.append(format_shortened_error(err, indent=indent))
122-
return "\n".join(parts)
123-
124-
125-
def format_error(err: Exception, mode: str = "short") -> str:
126-
if mode == "short":
127-
return format_shortened_trace(err)
128-
else:
129-
return "".join(traceback.format_exception(type(err), err, err.__traceback__))
130-
131-
132-
def print_error(err: Exception, mode: str = "short") -> None:
133-
click.echo(format_error(err, mode=mode), err=True)
134-
135-
13694
def iter_validation_error(
13795
err: jsonschema.ValidationError,
13896
) -> t.Iterator[jsonschema.ValidationError]:

0 commit comments

Comments
 (0)