Files
Barret Rennie 614e74345c Bug 1821243 - Update to jsonschema 4.17.3 r=ahochheiden
We are using features from modern JSONSchema drafts, like dependantRequired, in
the Firefox Messaging System schemas. jsonschema 3.x does not support these
features. Experimenter is also being updated to 4.17.3 and we want to keep our
validation tests in sync with Experimenter.

Differential Revision: https://phabricator.services.mozilla.com/D172457
2023-03-16 20:27:01 +00:00

300 lines
8.3 KiB
Python

"""
The ``jsonschema`` command line.
"""
from json import JSONDecodeError
from textwrap import dedent
import argparse
import json
import sys
import traceback
import warnings
try:
from importlib import metadata
except ImportError:
import importlib_metadata as metadata # type: ignore
try:
from pkgutil import resolve_name
except ImportError:
from pkgutil_resolve_name import resolve_name # type: ignore
import attr
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
@attr.s
class _Outputter:
_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 load(self, path):
try:
file = open(path)
except FileNotFoundError:
self.filenotfound_error(path=path, exc_info=sys.exc_info())
raise _CannotLoadFile()
with file:
try:
return json.load(file)
except JSONDecodeError:
self.parsing_error(path=path, exc_info=sys.exc_info())
raise _CannotLoadFile()
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))
@attr.s
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="{!r} does not exist.".format(path),
)
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)
@attr.s
class _PlainFormatter:
_error_format = attr.ib()
def filenotfound_error(self, path, exc_info):
return "{!r} does not exist.\n".format(path)
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
if arguments["validator"] is None:
arguments["validator"] = validator_for(schema)
try:
arguments["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:
outputter.parsing_error(
path="<stdin>", exc_info=sys.exc_info(),
)
raise _CannotLoadFile()
instances = ["<stdin>"]
resolver = RefResolver(
base_uri=arguments["base_uri"],
referrer=schema,
) if arguments["base_uri"] is not None else None
validator = arguments["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