You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			293 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			293 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Python
		
	
"""
 | 
						|
The ``jsonschema`` command line.
 | 
						|
"""
 | 
						|
 | 
						|
from importlib import metadata
 | 
						|
from json import JSONDecodeError
 | 
						|
from pkgutil import resolve_name
 | 
						|
from textwrap import dedent
 | 
						|
import argparse
 | 
						|
import json
 | 
						|
import sys
 | 
						|
import traceback
 | 
						|
import warnings
 | 
						|
 | 
						|
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)  # noqa: SIM115, PTH123
 | 
						|
        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):  # noqa: D103
 | 
						|
    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:]):  # noqa: D103
 | 
						|
    sys.exit(run(arguments=parse_args(args=args)))
 | 
						|
 | 
						|
 | 
						|
def run(arguments, stdout=sys.stdout, stderr=sys.stderr, stdin=sys.stdin):  # noqa: D103
 | 
						|
    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
 |