Skip to content

[CLI] Improve the overall console output (fix #623) #661

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 83 commits into from
Mar 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
40e5ba1
[CLI] Improve the default display of the errors
RomainTT Feb 19, 2020
e7cd69c
[CLI] Remove Linux-specific command for error delimiter
RomainTT Feb 19, 2020
d702f7d
[CLI] Remove incompatible character, replaced by '='
RomainTT Feb 19, 2020
1e51198
[CLI] General improvement of CLI output readability
RomainTT Feb 20, 2020
96c98d5
[CLI] Remove --human and add --output. Add more helper functions.
RomainTT Feb 21, 2020
aa5fafa
[CLI] Improve stdin management and factorize more code
RomainTT Feb 26, 2020
47e460b
[CLI] Add newline at end of oneline error
RomainTT Feb 26, 2020
ccf7027
[tests] Update existing CLI tests for the new implementation.
RomainTT Feb 26, 2020
cdd0dd9
[tests] remove external files for CLI tests
RomainTT Feb 26, 2020
f07ff5b
[CLI] Fix STDIN management.
RomainTT Feb 26, 2020
305a540
[CLI] Replace JSONDecodeError by ValueError for 2.7 compat
RomainTT Feb 28, 2020
155277d
[CLI] Replace FileNotFoundError by IOError for 2.7 compat
RomainTT Feb 28, 2020
18510ad
[tests] Remove unused import SchemaError
RomainTT Feb 28, 2020
e5f12dd
[tests] Remove unused import os
RomainTT Feb 28, 2020
ddc192e
[tests] Finish to fix existing tests.
RomainTT Mar 4, 2020
67444b3
[tests] Add new tests for the new CLI output formatting
RomainTT Mar 4, 2020
0a60d62
[CLI] Add pragma no cover for some safenet code
RomainTT Mar 4, 2020
9d2251b
[tests] Add some test to the cli parser, about None arguments
RomainTT Mar 4, 2020
6e114f1
Fix some flake8 style errors
RomainTT Mar 5, 2020
2e056fe
[CLI] Remove heading-style comments
RomainTT Mar 7, 2020
8575306
[CLI] Improve --help message about --instance
RomainTT Mar 7, 2020
ae0cbf2
[CLI] Change write_valid_* into write_validation_*, less confusing.
RomainTT Mar 7, 2020
3e4d2fc
[tests] replace assertTrue by assertIn for nicer error
RomainTT Mar 7, 2020
712fda9
[CLI] Remove mandatory \n at the end of plain error.
RomainTT Mar 7, 2020
cbf21b9
[tests] Revert test_unsuccessful_validation_multiple_instances to its…
RomainTT Mar 7, 2020
a5c5fd0
[CLI] Split CliOutputWriter to use object polymorphism
RomainTT Mar 7, 2020
859724e
[tests] Fix test_unsuccessful_validation
RomainTT Mar 7, 2020
36adff5
[tests] Simplify check in test_none_instance
RomainTT Mar 7, 2020
038f2e1
[CLI] Fix compatibility with Python2 (about metaclass)
RomainTT Mar 7, 2020
1f7dbaa
[tests] Add test_unknown_output_format
RomainTT Mar 8, 2020
3a87086
[CLI] Forbid --error-format if --output is not plain
RomainTT Mar 8, 2020
e2d67ff
[tests] Add test_schema_validation
RomainTT Mar 8, 2020
cf1e784
[CLI] Switch public methods to private.
RomainTT Mar 8, 2020
ec3603e
[CLI] Remove abstract class for output writers
RomainTT Mar 10, 2020
7d8db25
[CLI] Fix coding style errors in parse_args
RomainTT Mar 10, 2020
40770f0
[tests] Remove use of unittest mocks. Add contextmanager capture_outp…
RomainTT Mar 10, 2020
e5da51d
[tests] Fix some linter errors
RomainTT Mar 10, 2020
2f40f4c
[tests] Improve test_unknown_output and test_useless_error_format
RomainTT Mar 10, 2020
0d1d2a7
Merge remote-tracking branch 'origin/master' into cli-better-error-disp
Julian Mar 10, 2020
40ea62e
Py2 new-style class.
Julian Mar 10, 2020
bb83bef
Share the parsing error message.
Julian Mar 10, 2020
c503761
Minor style.
Julian Mar 10, 2020
c8f1f49
assertTrue -> assertIn.
Julian Mar 10, 2020
6fa5802
attrs'ed.
Julian Mar 10, 2020
8297d76
Bit clearer display of the error message in the actual source via ded…
Julian Mar 15, 2020
bf5e6cf
Specify the type of error in pretty error messages. …
Julian Mar 16, 2020
901690e
Py3.
Julian Mar 16, 2020
4041d57
Merge remote-tracking branch 'origin/master' into cli-better-error-disp
Julian Mar 21, 2020
205b6e1
Test parsing errors for the plain output writer.
Julian Mar 21, 2020
6275e02
Test parsing errors for stdin + the pretty output writer.
Julian Mar 21, 2020
690e583
Move where errors are written outside of cli writers.
Julian Mar 21, 2020
b156260
Only used in one place now.
Julian Mar 21, 2020
53124f4
Follow how other places tend to indicate the path for stdin.
Julian Mar 21, 2020
a1b5229
Call all the args 'error' consistently.
Julian Mar 21, 2020
6f85e51
Clarify what parts of the CLI output are public.
Julian Mar 21, 2020
fd3bb7e
Slightly less exception for flow control.
Julian Mar 21, 2020
fa8cfef
Move compat code to compat.
Julian Mar 21, 2020
cbd356b
Temporarily inline these, there will be a better way to combine them.
Julian Mar 21, 2020
4b2372a
Sigh, remove -q from spelling which is required to see the misspelled…
Julian Mar 21, 2020
53708a6
Spelling.
Julian Mar 21, 2020
c46c214
type=str is the default.
Julian Mar 21, 2020
d099d7e
Move the asserts out of the with block.
Julian Mar 21, 2020
5981bd9
Minor safety guard.
Julian Mar 21, 2020
93815b0
Equivalent but consistent.
Julian Mar 21, 2020
2561726
Don't even bother keeping the whole list of valid/invalid.
Julian Mar 21, 2020
97a697b
Minor style.
Julian Mar 21, 2020
7956b9d
Merge remote-tracking branch 'origin/master' into cli-better-error-disp
Julian Mar 21, 2020
0bdb8e4
Squash.
Julian Mar 22, 2020
47053b5
Show full tracebacks for parsing errors in pretty output.
Julian Mar 22, 2020
4a68917
And now bundle up stdout/stderr into an object.
Julian Mar 22, 2020
c1de16c
The validation already can handle not having instances.
Julian Mar 22, 2020
abb1038
Error rather than fail for miswritten tests.
Julian Mar 22, 2020
6a8bb5a
Document that non-zero error codes are also not public.
Julian Mar 22, 2020
cfa65ee
Make fake_open work the same across both test cases.
Julian Mar 22, 2020
0e8300e
DRY up cli.run calls.
Julian Mar 22, 2020
66fd01f
OK TestParser doesn't actually open files at all...
Julian Mar 22, 2020
1b7867e
Clarify the test case name.
Julian Mar 22, 2020
c92755d
Transparent fixture names.
Julian Mar 22, 2020
77d22db
DRY up asserting about exit codes.
Julian Mar 22, 2020
89450ae
And dry up calling .getvalue.
Julian Mar 22, 2020
2fefc2b
repr paths in plain CLI outputted errors.
Julian Mar 22, 2020
c61b5ee
Note that --error-format is for schema and validation errors specific…
Julian Mar 22, 2020
a2ac5cc
argparse does funny wrapping by default, so no need for continuations.
Julian Mar 22, 2020
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
7 changes: 7 additions & 0 deletions docs/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,13 @@ notice:
* the ``jsonschema.compat`` module, which is for internal
compatibility use

* the specific non-zero error codes presented by the command line
interface

* the exact representation of errors presented by the command line
interface, other than that errors represented by the plain outputter
will be reported one per line

* anything marked private

With the exception of the last two of those, flippant changes are
Expand Down
1 change: 1 addition & 0 deletions docs/spelling-wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jsonschema
majorly
metaschema
online
outputter
pre
programmatically
recurses
Expand Down
222 changes: 187 additions & 35 deletions jsonschema/cli.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,145 @@
"""
The ``jsonschema`` command line.
"""

from __future__ import absolute_import
from textwrap import dedent
import argparse
import json
import sys
import traceback

import attr

from jsonschema import __version__
from jsonschema._reflect import namedAny
from jsonschema.compat import JSONDecodeError
from jsonschema.exceptions import SchemaError
from jsonschema.validators import validator_for


@attr.s
class _Outputter(object):

_formatter = attr.ib()
_stdout = attr.ib()
_stderr = attr.ib()

@classmethod
def from_arguments(cls, arguments, stdout, stderr):
if arguments["output"] == "plain":
formatter = _PlainFormatter(arguments["error_format"])
elif arguments["output"] == "pretty":
formatter = _PrettyFormatter()
return cls(formatter=formatter, stdout=stdout, stderr=stderr)

def parsing_error(self, **kwargs):
self._stderr.write(self._formatter.parsing_error(**kwargs))

def validation_error(self, **kwargs):
self._stderr.write(self._formatter.validation_error(**kwargs))

def validation_success(self, **kwargs):
self._stdout.write(self._formatter.validation_success(**kwargs))


@attr.s
class _PrettyFormatter(object):

_ERROR_MSG = dedent(
"""\
===[{type.__name__}]===({path})===

{body}
-----------------------------
""",
)
_SUCCESS_MSG = "===[SUCCESS]===({path})===\n"

def parsing_error(self, path, exc_info):
exc_type, exc_value, exc_traceback = exc_info
exc_lines = "".join(
traceback.format_exception(exc_type, exc_value, exc_traceback),
)
return self._ERROR_MSG.format(path=path, type=exc_type, body=exc_lines)

def validation_error(self, instance_path, error):
return self._ERROR_MSG.format(
path=instance_path,
type=error.__class__,
body=error,
)

def validation_success(self, instance_path):
return self._SUCCESS_MSG.format(path=instance_path)


@attr.s
class _PlainFormatter(object):

_error_format = attr.ib()

def parsing_error(self, path, exc_info):
return "Failed to parse {}. Got the following error: {}\n".format(
"<stdin>" if path == "<stdin>" else repr(path),
exc_info[1],
)

def validation_error(self, instance_path, error):
return self._error_format.format(file_name=instance_path, error=error)

def validation_success(self, instance_path):
return ""


def _namedAnyWithDefault(name):
if "." not in name:
name = "jsonschema." + name
return namedAny(name)


def _json_file(path):
with open(path) as file:
return json.load(file)


parser = argparse.ArgumentParser(
description="JSON Schema Validation CLI",
)
parser.add_argument(
"-i", "--instance",
action="append",
dest="instances",
type=_json_file,
help=(
"a path to a JSON instance (i.e. filename.json) "
"to validate (may be specified multiple times)"
),
help="""
a path to a JSON instance (i.e. filename.json) to validate (may
be specified multiple times). If no instances are provided via this
option, one will be expected on standard input.
""",
)
parser.add_argument(
"-F", "--error-format",
default="{error.instance}: {error.message}\n",
help=(
"the format to use for each error output message, specified in "
"a form suitable for passing to str.format, which will be called "
"with 'error' for each error"
),
help="""
the format to use for each validation error message, specified
in a form suitable for str.format. This string will be passed
one formatted object named 'error' for each ValidationError.
Only provide this option when using --output=plain, which is the
default. If this argument is unprovided and --output=plain is
used, a simple default representation will be used."
""",
)
parser.add_argument(
"-o", "--output",
choices=["plain", "pretty"],
default="plain",
help="""
an output format to use. 'plain' (default) will produce minimal
text with one line for each error, while 'pretty' will produce
more detailed human-readable output on multiple lines.
""",
)
parser.add_argument(
"-V", "--validator",
type=_namedAnyWithDefault,
help=(
"the fully qualified object name of a validator to use, or, for "
"validators that are registered with jsonschema, simply the name "
"of the class."
),
help="""
the fully qualified object name of a validator to use, or, for
validators that are registered with jsonschema, simply the name
of the class.
""",
)
parser.add_argument(
"--version",
Expand All @@ -60,32 +148,96 @@ def _json_file(path):
)
parser.add_argument(
"schema",
help="the JSON Schema to validate with (i.e. schema.json)",
type=_json_file,
help="the path to a JSON Schema to validate with (i.e. schema.json)",
)


def _load_json_file(path):
with open(path) as file:
return json.load(file)


def parse_args(args):
arguments = vars(parser.parse_args(args=args or ["--help"]))
if arguments["validator"] is None:
arguments["validator"] = validator_for(arguments["schema"])
if arguments["output"] != "plain" and arguments["error_format"]:
raise parser.error(
"--error-format can only be used with --output plain"
)
if arguments["output"] == "plain" and arguments["error_format"] is None:
arguments["error_format"] = "{error.instance}: {error.message}\n"
return arguments


def main(args=sys.argv[1:]):
sys.exit(run(arguments=parse_args(args=args)))
def _make_validator(schema_path, Validator, outputter):
try:
schema_obj = _load_json_file(schema_path)
except (ValueError, IOError) as error:
outputter.parsing_error(path=schema_path, exc_info=sys.exc_info())
raise error

try:
validator = Validator(schema=schema_obj)
validator.check_schema(schema_obj)
except SchemaError as error:
outputter.validation_error(instance_path=schema_path, error=error)
raise error

def run(arguments, stdout=sys.stdout, stderr=sys.stderr, stdin=sys.stdin):
error_format = arguments["error_format"]
validator = arguments["validator"](schema=arguments["schema"])
return validator


def _validate_instance(instance_path, instance, validator, outputter):
invalid = False
for error in validator.iter_errors(instance):
invalid = True
outputter.validation_error(instance_path=instance_path, error=error)

validator.check_schema(arguments["schema"])
if not invalid:
outputter.validation_success(instance_path=instance_path)
return invalid

errored = False
for instance in arguments["instances"] or [json.load(stdin)]:
for error in validator.iter_errors(instance):
stderr.write(error_format.format(error=error))
errored = True

return errored
def main(args=sys.argv[1:]):
sys.exit(run(arguments=parse_args(args=args)))


def run(arguments, stdout=sys.stdout, stderr=sys.stderr, stdin=sys.stdin):
outputter = _Outputter.from_arguments(
arguments=arguments,
stdout=stdout,
stderr=stderr,
)

try:
validator = _make_validator(
schema_path=arguments["schema"],
Validator=arguments["validator"],
outputter=outputter,
)
except (IOError, ValueError, SchemaError):
return 1

if arguments["instances"]:
load, instances = _load_json_file, arguments["instances"]
else:
def load(_):
return json.load(stdin)
instances = ["<stdin>"]

exit_code = 0
for each in instances:
try:
instance = load(each)
except JSONDecodeError:
outputter.parsing_error(path=each, exc_info=sys.exc_info())
exit_code = 1
else:
exit_code |= _validate_instance(
instance_path=each,
instance=instance,
validator=validator,
outputter=outputter,
)

return exit_code
5 changes: 5 additions & 0 deletions jsonschema/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
except ImportError:
from collections import MutableMapping, Sequence # noqa

try:
from json import JSONDecodeError
except ImportError:
JSONDecodeError = ValueError

PY3 = sys.version_info[0] >= 3

if PY3:
Expand Down
17 changes: 17 additions & 0 deletions jsonschema/tests/_helpers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
import sys
from contextlib import contextmanager

from jsonschema.compat import NativeIO


def bug(issue=None):
message = "A known bug."
if issue is not None:
message += " See issue #{issue}.".format(issue=issue)
return message


@contextmanager
def captured_output():
new_out, new_err = NativeIO(), NativeIO()
old_out, old_err = sys.stdout, sys.stderr
try:
sys.stdout, sys.stderr = new_out, new_err
yield sys.stdout, sys.stderr
finally:
sys.stdout, sys.stderr = old_out, old_err
Loading