diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 000000000..306e1add4 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,13 @@ +name: pre-commit + +on: + pull_request: + push: + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: pre-commit/action@v2.0.0 diff --git a/jsonschema/cli.py b/jsonschema/cli.py index 713193979..94b24cd90 100644 --- a/jsonschema/cli.py +++ b/jsonschema/cli.py @@ -3,7 +3,6 @@ """ from json import JSONDecodeError -from textwrap import dedent import argparse import errno import json @@ -66,21 +65,32 @@ def validation_success(self, **kwargs): self._stdout.write(self._formatter.validation_success(**kwargs)) +def _json_formatter(x): + return json.dumps(x, indent=4, sort_keys=True) + + @attr.s class _PrettyFormatter(object): + _MESSAGE_BAR_CHAR = "═" + _MESSAGE_CORNER_CHARS = "╒", "╕" + _MESSAGE_FORMAT = "{}══[{}]═══({})" + _MESSAGE_MAX_LENGTH = 79 - _ERROR_MSG = dedent( - """\ - ===[{type}]===({path})=== + def _message_line(self, path, type, header=False): + begin_char, end_char = self._MESSAGE_CORNER_CHARS if header \ + else [self._MESSAGE_BAR_CHAR] * 2 + return self._MESSAGE_FORMAT.format(begin_char, type, path) \ + .ljust(self._MESSAGE_MAX_LENGTH - 1, self._MESSAGE_BAR_CHAR) + \ + end_char - {body} - ----------------------------- - """, - ) - _SUCCESS_MSG = "===[SUCCESS]===({path})===\n" + def _error_msg(self, path, type, body): + HEADER = self._message_line(path, type, header=True) + FOOTER = "└" + "─" * (self._MESSAGE_MAX_LENGTH - 2) + "┘" + + return "\n".join((HEADER, body, FOOTER, "\n")) def filenotfound_error(self, path, exc_info): - return self._ERROR_MSG.format( + return self._error_msg( path=path, type="FileNotFoundError", body="{!r} does not exist.".format(path), @@ -91,21 +101,21 @@ def parsing_error(self, path, exc_info): exc_lines = "".join( traceback.format_exception(exc_type, exc_value, exc_traceback), ) - return self._ERROR_MSG.format( + return self._error_msg( path=path, type=exc_type.__name__, body=exc_lines, ) def validation_error(self, instance_path, error): - return self._ERROR_MSG.format( + return self._error_msg( path=instance_path, type=error.__class__.__name__, - body=error, + body=error._formatted_message(formatter=_json_formatter), ) def validation_success(self, instance_path): - return self._SUCCESS_MSG.format(path=instance_path) + return self._message_line(path=instance_path, type="SUCCESS") + "\n\n" @attr.s diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index 46ffceda5..3e5634e2d 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -2,8 +2,9 @@ Validation errors, and some surrounding helpers. """ from collections import defaultdict, deque +from functools import partial +from pprint import pformat import itertools -import pprint import textwrap import attr @@ -60,14 +61,18 @@ def __repr__(self): return "<%s: %r>" % (self.__class__.__name__, self.message) def __str__(self): + return self._formatted_message() + + def _formatted_message(self, formatter=partial(pformat, width=72)): essential_for_verbose = ( self.validator, self.validator_value, self.instance, self.schema, ) if any(m is _unset for m in essential_for_verbose): return self.message - pschema = pprint.pformat(self.schema, width=72) - pinstance = pprint.pformat(self.instance, width=72) + pschema = formatter(self.schema) + + pinstance = pformat(self.instance, width=72) return self.message + textwrap.dedent(""" Failed validating %r in %s%s: @@ -186,8 +191,8 @@ def __init__(self, type, instance, schema): self.schema = schema def __str__(self): - pschema = pprint.pformat(self.schema, width=72) - pinstance = pprint.pformat(self.instance, width=72) + pschema = pformat(self.schema, width=72) + pinstance = pformat(self.instance, width=72) return textwrap.dedent(""" Unknown type %r for validator with schema: %s diff --git a/jsonschema/tests/test_cli.py b/jsonschema/tests/test_cli.py index 42167599f..ec318bd9c 100644 --- a/jsonschema/tests/test_cli.py +++ b/jsonschema/tests/test_cli.py @@ -123,10 +123,10 @@ def test_invalid_instance_pretty_output(self): exit_code=1, stderr="""\ - ===[ValidationError]===(some_instance)=== - + ╒══[ValidationError]═══(some_instance)════════════════════════════════════════╕ I am an error! - ----------------------------- + └─────────────────────────────────────────────────────────────────────────────┘ + """, ), @@ -182,14 +182,14 @@ def test_invalid_instance_multiple_errors_pretty_output(self): exit_code=1, stderr="""\ - ===[ValidationError]===(some_instance)=== - + ╒══[ValidationError]═══(some_instance)════════════════════════════════════════╕ First error - ----------------------------- - ===[ValidationError]===(some_instance)=== + └─────────────────────────────────────────────────────────────────────────────┘ + ╒══[ValidationError]═══(some_instance)════════════════════════════════════════╕ Second error - ----------------------------- + └─────────────────────────────────────────────────────────────────────────────┘ + """, ) @@ -250,18 +250,18 @@ def test_multiple_invalid_instances_pretty_output(self): exit_code=1, stderr="""\ - ===[ValidationError]===(some_first_instance)=== - + ╒══[ValidationError]═══(some_first_instance)══════════════════════════════════╕ An error - ----------------------------- - ===[ValidationError]===(some_first_instance)=== + └─────────────────────────────────────────────────────────────────────────────┘ + ╒══[ValidationError]═══(some_first_instance)══════════════════════════════════╕ Another error - ----------------------------- - ===[ValidationError]===(some_second_instance)=== + └─────────────────────────────────────────────────────────────────────────────┘ + ╒══[ValidationError]═══(some_second_instance)═════════════════════════════════╕ BOOM - ----------------------------- + └─────────────────────────────────────────────────────────────────────────────┘ + """, ) @@ -309,7 +309,10 @@ def test_invalid_schema_pretty_output(self): with self.assertRaises(SchemaError) as e: validate(schema=schema, instance="") - error = str(e.exception) +str( + e.exception._formatted_message(formatter=cli._json_formatter), +)``` + formatter=cli._json_formatter)) self.assertOutputs( files=dict(some_schema=json.dumps(schema)), @@ -317,9 +320,9 @@ def test_invalid_schema_pretty_output(self): exit_code=1, stderr=( - "===[SchemaError]===(some_schema)===\n\n" - + str(error) - + "\n-----------------------------\n" + "╒══[SchemaError]═══(some_schema)══════════════════════════════════════════════╕\n" + + str(error) + + "\n└─────────────────────────────────────────────────────────────────────────────┘\n\n" ), ) @@ -339,7 +342,8 @@ def test_invalid_schema_multiple_errors_pretty_output(self): with self.assertRaises(SchemaError) as e: validate(schema=schema, instance="") - error = str(e.exception) + error = str(e.exception._formatted_message( + formatter=cli._json_formatter)) self.assertOutputs( files=dict(some_schema=json.dumps(schema)), @@ -347,9 +351,9 @@ def test_invalid_schema_multiple_errors_pretty_output(self): exit_code=1, stderr=( - "===[SchemaError]===(some_schema)===\n\n" - + str(error) - + "\n-----------------------------\n" + "╒══[SchemaError]═══(some_schema)══════════════════════════════════════════════╕\n" + + str(error) + + "\n└─────────────────────────────────────────────────────────────────────────────┘\n\n" ), ) @@ -376,7 +380,8 @@ def test_invalid_schema_with_invalid_instance_pretty_output(self): with self.assertRaises(SchemaError) as e: validate(schema=schema, instance=instance) - error = str(e.exception) + error = str(e.exception._formatted_message( + formatter=cli._json_formatter)) self.assertOutputs( files=dict( @@ -387,9 +392,9 @@ def test_invalid_schema_with_invalid_instance_pretty_output(self): exit_code=1, stderr=( - "===[SchemaError]===(some_schema)===\n\n" - + str(error) - + "\n-----------------------------\n" + "╒══[SchemaError]═══(some_schema)══════════════════════════════════════════════╕\n" + + str(error) + + "\n└─────────────────────────────────────────────────────────────────────────────┘\n\n" ), ) @@ -457,7 +462,11 @@ def test_instance_is_invalid_JSON_pretty_output(self): ) self.assertFalse(stdout) self.assertIn( - "(some_instance)===\n\nTraceback (most recent call last):\n", + "(some_instance)═", + stderr, + ) + self.assertIn( + "═╕\nTraceback (most recent call last):\n", stderr, ) self.assertNotIn("some_schema", stderr) @@ -488,7 +497,11 @@ def test_instance_is_invalid_JSON_on_stdin_pretty_output(self): ) self.assertFalse(stdout) self.assertIn( - "()===\n\nTraceback (most recent call last):\n", + "()═", + stderr, + ) + self.assertIn( + "═╕\nTraceback (most recent call last):\n", stderr, ) self.assertNotIn("some_schema", stderr) @@ -517,7 +530,11 @@ def test_schema_is_invalid_JSON_pretty_output(self): ) self.assertFalse(stdout) self.assertIn( - "(some_schema)===\n\nTraceback (most recent call last):\n", + "(some_schema)═", + stderr, + ) + self.assertIn( + "═╕\nTraceback (most recent call last):\n", stderr, ) @@ -553,7 +570,11 @@ def test_schema_and_instance_are_both_invalid_JSON_pretty_output(self): ) self.assertFalse(stdout) self.assertIn( - "(some_schema)===\n\nTraceback (most recent call last):\n", + "(some_schema)═", + stderr, + ) + self.assertIn( + "═╕\nTraceback (most recent call last):\n", stderr, ) self.assertNotIn("some_instance", stderr) @@ -580,10 +601,10 @@ def test_instance_does_not_exist_pretty_output(self): exit_code=1, stderr="""\ - ===[FileNotFoundError]===(nonexisting_instance)=== - + ╒══[FileNotFoundError]═══(nonexisting_instance)═══════════════════════════════╕ 'nonexisting_instance' does not exist. - ----------------------------- + └─────────────────────────────────────────────────────────────────────────────┘ + """, ) @@ -601,10 +622,10 @@ def test_schema_does_not_exist_pretty_output(self): exit_code=1, stderr="""\ - ===[FileNotFoundError]===(nonexisting_schema)=== - + ╒══[FileNotFoundError]═══(nonexisting_schema)═════════════════════════════════╕ 'nonexisting_schema' does not exist. - ----------------------------- + └─────────────────────────────────────────────────────────────────────────────┘ + """, ) @@ -626,10 +647,10 @@ def test_neither_instance_nor_schema_exist_pretty_output(self): exit_code=1, stderr="""\ - ===[FileNotFoundError]===(nonexisting_schema)=== - + ╒══[FileNotFoundError]═══(nonexisting_schema)═════════════════════════════════╕ 'nonexisting_schema' does not exist. - ----------------------------- + └─────────────────────────────────────────────────────────────────────────────┘ + """, ) @@ -645,7 +666,7 @@ def test_successful_validation_pretty_output(self): self.assertOutputs( files=dict(some_schema="{}", some_instance="{}"), argv=["--output", "pretty", "-i", "some_instance", "some_schema"], - stdout="===[SUCCESS]===(some_instance)===\n", + stdout="═══[SUCCESS]═══(some_instance)═════════════════════════════════════════════════\n\n", stderr="", ) @@ -663,7 +684,7 @@ def test_successful_validation_of_stdin_pretty_output(self): files=dict(some_schema="{}"), stdin=StringIO("{}"), argv=["--output", "pretty", "some_schema"], - stdout="===[SUCCESS]===()===\n", + stdout="═══[SUCCESS]═══()═══════════════════════════════════════════════════════\n\n", stderr="", ) @@ -679,7 +700,7 @@ def test_successful_validation_of_just_the_schema_pretty_output(self): self.assertOutputs( files=dict(some_schema="{}", some_instance="{}"), argv=["--output", "pretty", "-i", "some_instance", "some_schema"], - stdout="===[SUCCESS]===(some_instance)===\n", + stdout="═══[SUCCESS]═══(some_instance)═════════════════════════════════════════════════\n\n", stderr="", )