Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ CHANGELOG
Unreleased
----------

- Add official support for Python 3.14
- Add Citation File Format schema and pre-commit hook. Thanks :user:`edgarrmondragon`! (:issue:`502`)

.. vendor-insert-here

- Update vendored schemas: bitbucket-pipelines, buildkite, circle-ci, compose-spec,
dependabot, gitlab-ci, meltano, mergify, renovate, snapcraft (2025-11-11)
- Add official support for Python 3.14
- Add Citation File Format schema and pre-commit hook. Thanks :user:`edgarrmondragon`! (:issue:`502`)
- Improved default text output when parsing errors are encountered. (:issue:`581`)

0.34.1
------
Expand Down
6 changes: 3 additions & 3 deletions src/check_jsonschema/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import jsonschema
import referencing.exceptions

from . import utils
from . import format_errors
from .formats import FormatOptions
from .instance_loader import InstanceLoader
from .parsers import ParseError
Expand All @@ -31,7 +31,7 @@ def __init__(
*,
format_opts: FormatOptions,
regex_impl: RegexImplementation,
traceback_mode: str = "short",
traceback_mode: t.Literal["minimal", "short", "full"] = "short",
fill_defaults: bool = False,
) -> None:
self._schema_loader = schema_loader
Expand All @@ -46,7 +46,7 @@ def __init__(
def _fail(self, msg: str, err: Exception | None = None) -> t.NoReturn:
click.echo(msg, err=True)
if err is not None:
utils.print_error(err, mode=self._traceback_mode)
format_errors.print_error(err, mode=self._traceback_mode)
raise _Exit(1)

def get_validator(
Expand Down
2 changes: 1 addition & 1 deletion src/check_jsonschema/cli/parse_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __init__(self) -> None:
self.regex_variant: RegexVariantName = RegexVariantName.default
# error and output controls
self.verbosity: int = 1
self.traceback_mode: str = "short"
self.traceback_mode: t.Literal["short", "full"] = "short"
self.output_format: str = "text"

def set_regex_variant(
Expand Down
66 changes: 66 additions & 0 deletions src/check_jsonschema/format_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from __future__ import annotations

import linecache
import textwrap
import traceback
import typing as t

import click


def format_error_message(err: BaseException) -> str:
return f"{type(err).__name__}: {err}"


def format_minimal_error(err: BaseException, *, indent: int = 0) -> str:
lines = [textwrap.indent(str(err), indent * " ")]
if err.__cause__ is not None:
lines.append(
textwrap.indent(format_error_message(err.__cause__), (indent + 2) * " ")
)

return "\n".join(lines)


def format_shortened_error(err: BaseException, *, indent: int = 0) -> str:
lines = []
lines.append(textwrap.indent(format_error_message(err), indent * " "))
if err.__traceback__ is not None:
lineno = err.__traceback__.tb_lineno
tb_frame = err.__traceback__.tb_frame
filename = tb_frame.f_code.co_filename
line = linecache.getline(filename, lineno)
lines.append((indent + 2) * " " + f'in "{filename}", line {lineno}')
lines.append((indent + 2) * " " + ">>> " + line.strip())
return "\n".join(lines)


def format_shortened_trace(caught_err: BaseException) -> str:
err_stack: list[BaseException] = [caught_err]
while err_stack[-1].__context__ is not None:
err_stack.append(err_stack[-1].__context__) # type: ignore[arg-type]

parts = [format_shortened_error(caught_err)]
indent = 0
for err in err_stack[1:]:
indent += 2
parts.append("\n" + indent * " " + "caused by\n")
parts.append(format_shortened_error(err, indent=indent))
return "\n".join(parts)


def format_error(
err: Exception, mode: t.Literal["minimal", "short", "full"] = "short"
) -> str:
if mode == "minimal":
return format_minimal_error(err)
elif mode == "short":
return format_shortened_trace(err)
else:
return "".join(traceback.format_exception(type(err), err, err.__traceback__))


def print_error(
err: Exception, mode: t.Literal["minimal", "short", "full"] = "short"
) -> None:
click.echo(format_error(err, mode=mode), err=True)
10 changes: 6 additions & 4 deletions src/check_jsonschema/reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
import click
import jsonschema

from . import format_errors
from .parsers import ParseError
from .result import CheckResult
from .utils import format_error, iter_validation_error
from .utils import iter_validation_error


class Reporter(abc.ABC):
Expand Down Expand Up @@ -111,11 +112,12 @@ def _show_validation_error(

def _show_parse_error(self, filename: str, err: ParseError) -> None:
if self.verbosity < 2:
self._echo(click.style(str(err), fg="yellow"), indent=2)
mode: t.Literal["minimal", "short", "full"] = "minimal"
elif self.verbosity < 3:
self._echo(textwrap.indent(format_error(err, mode="short"), " "))
mode = "short"
else:
self._echo(textwrap.indent(format_error(err, mode="full"), " "))
mode = "full"
self._echo(textwrap.indent(format_errors.format_error(err, mode=mode), " "))

def report_errors(self, result: CheckResult) -> None:
if self.verbosity < 1:
Expand Down
42 changes: 0 additions & 42 deletions src/check_jsonschema/utils.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
from __future__ import annotations

import linecache
import os
import pathlib
import re
import textwrap
import traceback
import typing as t
import urllib.parse
import urllib.request

import click
import jsonschema

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


def format_shortened_error(err: Exception, *, indent: int = 0) -> str:
lines = []
lines.append(textwrap.indent(f"{type(err).__name__}: {err}", indent * " "))
if err.__traceback__ is not None:
lineno = err.__traceback__.tb_lineno
tb_frame = err.__traceback__.tb_frame
filename = tb_frame.f_code.co_filename
line = linecache.getline(filename, lineno)
lines.append((indent + 2) * " " + f'in "{filename}", line {lineno}')
lines.append((indent + 2) * " " + ">>> " + line.strip())
return "\n".join(lines)


def format_shortened_trace(caught_err: Exception) -> str:
err_stack: list[Exception] = [caught_err]
while err_stack[-1].__context__ is not None:
err_stack.append(err_stack[-1].__context__) # type: ignore[arg-type]

parts = [format_shortened_error(caught_err)]
indent = 0
for err in err_stack[1:]:
indent += 2
parts.append("\n" + indent * " " + "caused by\n")
parts.append(format_shortened_error(err, indent=indent))
return "\n".join(parts)


def format_error(err: Exception, mode: str = "short") -> str:
if mode == "short":
return format_shortened_trace(err)
else:
return "".join(traceback.format_exception(type(err), err, err.__traceback__))


def print_error(err: Exception, mode: str = "short") -> None:
click.echo(format_error(err, mode=mode), err=True)


def iter_validation_error(
err: jsonschema.ValidationError,
) -> t.Iterator[jsonschema.ValidationError]:
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/test_reporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pytest
from jsonschema import Draft7Validator

from check_jsonschema.parsers import ParseError
from check_jsonschema.reporter import JsonReporter, TextReporter
from check_jsonschema.result import CheckResult

Expand Down Expand Up @@ -240,3 +241,30 @@ def test_json_format_validation_error_nested(capsys, pretty_json, verbosity):
assert "{'baz': 'buzz'} is not of type 'string'" in [
item["message"] for item in bar_errors
]


def test_text_print_parse_error_with_cause(capsys):
cause = json.JSONDecodeError("a bad thing happened", "{,}", 1)
error = ParseError("whoopsie during parsing")
error.__cause__ = cause

result = CheckResult()
result.record_parse_error("foo.json", error)

text_reporter = TextReporter(verbosity=1)
text_reporter.report_result(result)
captured = capsys.readouterr()

# nothing to stderr
assert captured.err == ""
# stdout contains a nicely formatted error
assert (
textwrap.dedent(
"""\
Several files failed to parse.
whoopsie during parsing
JSONDecodeError: a bad thing happened: line 1 column 2 (char 1)
"""
)
in captured.out
)
Loading