|
""" |
|
The ``jsonschema`` command line. |
|
""" |
|
|
|
from importlib import metadata |
|
from json import JSONDecodeError |
|
from textwrap import dedent |
|
import argparse |
|
import json |
|
import sys |
|
import traceback |
|
import warnings |
|
|
|
try: |
|
from pkgutil import resolve_name |
|
except ImportError: |
|
from pkgutil_resolve_name import resolve_name |
|
|
|
from attrs import define, field |
|
|
|
from jsonschema.exceptions import SchemaError |
|
from jsonschema.validators import _RefResolver, validator_for |
|
|
|
warnings.warn( |
|
( |
|
"The jsonschema CLI is deprecated and will be removed in a future " |
|
"version. Please use check-jsonschema instead, which can be installed " |
|
"from https://pypi.org/project/check-jsonschema/" |
|
), |
|
DeprecationWarning, |
|
stacklevel=2, |
|
) |
|
|
|
|
|
class _CannotLoadFile(Exception): |
|
pass |
|
|
|
|
|
@define |
|
class _Outputter: |
|
|
|
_formatter = field() |
|
_stdout = field() |
|
_stderr = field() |
|
|
|
@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 load(self, path): |
|
try: |
|
file = open(path) |
|
except FileNotFoundError as error: |
|
self.filenotfound_error(path=path, exc_info=sys.exc_info()) |
|
raise _CannotLoadFile() from error |
|
|
|
with file: |
|
try: |
|
return json.load(file) |
|
except JSONDecodeError as error: |
|
self.parsing_error(path=path, exc_info=sys.exc_info()) |
|
raise _CannotLoadFile() from error |
|
|
|
def filenotfound_error(self, **kwargs): |
|
self._stderr.write(self._formatter.filenotfound_error(**kwargs)) |
|
|
|
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)) |
|
|
|
|
|
@define |
|
class _PrettyFormatter: |
|
|
|
_ERROR_MSG = dedent( |
|
"""\ |
|
===[{type}]===({path})=== |
|
|
|
{body} |
|
----------------------------- |
|
""", |
|
) |
|
_SUCCESS_MSG = "===[SUCCESS]===({path})===\n" |
|
|
|
def filenotfound_error(self, path, exc_info): |
|
return self._ERROR_MSG.format( |
|
path=path, |
|
type="FileNotFoundError", |
|
body=f"{path!r} does not exist.", |
|
) |
|
|
|
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.__name__, |
|
body=exc_lines, |
|
) |
|
|
|
def validation_error(self, instance_path, error): |
|
return self._ERROR_MSG.format( |
|
path=instance_path, |
|
type=error.__class__.__name__, |
|
body=error, |
|
) |
|
|
|
def validation_success(self, instance_path): |
|
return self._SUCCESS_MSG.format(path=instance_path) |
|
|
|
|
|
@define |
|
class _PlainFormatter: |
|
|
|
_error_format = field() |
|
|
|
def filenotfound_error(self, path, exc_info): |
|
return f"{path!r} does not exist.\n" |
|
|
|
def parsing_error(self, path, exc_info): |
|
return "Failed to parse {}: {}\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 _resolve_name_with_default(name): |
|
if "." not in name: |
|
name = "jsonschema." + name |
|
return resolve_name(name) |
|
|
|
|
|
parser = argparse.ArgumentParser( |
|
description="JSON Schema Validation CLI", |
|
) |
|
parser.add_argument( |
|
"-i", "--instance", |
|
action="append", |
|
dest="instances", |
|
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", |
|
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=_resolve_name_with_default, |
|
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( |
|
"--base-uri", |
|
help=""" |
|
a base URI to assign to the provided schema, even if it does not |
|
declare one (via e.g. $id). This option can be used if you wish to |
|
resolve relative references to a particular URI (or local path) |
|
""", |
|
) |
|
parser.add_argument( |
|
"--version", |
|
action="version", |
|
version=metadata.version("jsonschema"), |
|
) |
|
parser.add_argument( |
|
"schema", |
|
help="the path to a JSON Schema to validate with (i.e. schema.json)", |
|
) |
|
|
|
|
|
def parse_args(args): |
|
arguments = vars(parser.parse_args(args=args or ["--help"])) |
|
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 _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) |
|
|
|
if not invalid: |
|
outputter.validation_success(instance_path=instance_path) |
|
return invalid |
|
|
|
|
|
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: |
|
schema = outputter.load(arguments["schema"]) |
|
except _CannotLoadFile: |
|
return 1 |
|
|
|
Validator = arguments["validator"] |
|
if Validator is None: |
|
Validator = validator_for(schema) |
|
|
|
try: |
|
Validator.check_schema(schema) |
|
except SchemaError as error: |
|
outputter.validation_error( |
|
instance_path=arguments["schema"], |
|
error=error, |
|
) |
|
return 1 |
|
|
|
if arguments["instances"]: |
|
load, instances = outputter.load, arguments["instances"] |
|
else: |
|
def load(_): |
|
try: |
|
return json.load(stdin) |
|
except JSONDecodeError as error: |
|
outputter.parsing_error( |
|
path="<stdin>", exc_info=sys.exc_info(), |
|
) |
|
raise _CannotLoadFile() from error |
|
instances = ["<stdin>"] |
|
|
|
resolver = _RefResolver( |
|
base_uri=arguments["base_uri"], |
|
referrer=schema, |
|
) if arguments["base_uri"] is not None else None |
|
|
|
validator = Validator(schema, resolver=resolver) |
|
exit_code = 0 |
|
for each in instances: |
|
try: |
|
instance = load(each) |
|
except _CannotLoadFile: |
|
exit_code = 1 |
|
else: |
|
exit_code |= _validate_instance( |
|
instance_path=each, |
|
instance=instance, |
|
validator=validator, |
|
outputter=outputter, |
|
) |
|
|
|
return exit_code |
|
|