codekingpro/portable-devtools
115k
1"""2The ``jsonschema`` command line.3"""4 5from importlib import metadata6from json import JSONDecodeError7from pkgutil import resolve_name8from textwrap import dedent9import argparse10import json11import sys12import traceback13import warnings14 15from attrs import define, field16 17from jsonschema.exceptions import SchemaError18from jsonschema.validators import _RefResolver, validator_for19 20warnings.warn(21 (22 "The jsonschema CLI is deprecated and will be removed in a future "23 "version. Please use check-jsonschema instead, which can be installed "24 "from https://pypi.org/project/check-jsonschema/"25 ),26 DeprecationWarning,27 stacklevel=2,28)29 30 31class _CannotLoadFile(Exception):32 pass33 34 35@define36class _Outputter:37 38 _formatter = field()39 _stdout = field()40 _stderr = field()41 42 @classmethod43 def from_arguments(cls, arguments, stdout, stderr):44 if arguments["output"] == "plain":45 formatter = _PlainFormatter(arguments["error_format"])46 elif arguments["output"] == "pretty":47 formatter = _PrettyFormatter()48 return cls(formatter=formatter, stdout=stdout, stderr=stderr)49 50 def load(self, path):51 try:52 file = open(path) # noqa: SIM115, PTH12353 except FileNotFoundError as error:54 self.filenotfound_error(path=path, exc_info=sys.exc_info())55 raise _CannotLoadFile() from error56 57 with file:58 try:59 return json.load(file)60 except JSONDecodeError as error:61 self.parsing_error(path=path, exc_info=sys.exc_info())62 raise _CannotLoadFile() from error63 64 def filenotfound_error(self, **kwargs):65 self._stderr.write(self._formatter.filenotfound_error(**kwargs))66 67 def parsing_error(self, **kwargs):68 self._stderr.write(self._formatter.parsing_error(**kwargs))69 70 def validation_error(self, **kwargs):71 self._stderr.write(self._formatter.validation_error(**kwargs))72 73 def validation_success(self, **kwargs):74 self._stdout.write(self._formatter.validation_success(**kwargs))75 76 77@define78class _PrettyFormatter:79 80 _ERROR_MSG = dedent(81 """\82 ===[{type}]===({path})===83 84 {body}85 -----------------------------86 """,87 )88 _SUCCESS_MSG = "===[SUCCESS]===({path})===\n"89 90 def filenotfound_error(self, path, exc_info):91 return self._ERROR_MSG.format(92 path=path,93 type="FileNotFoundError",94 body=f"{path!r} does not exist.",95 )96 97 def parsing_error(self, path, exc_info):98 exc_type, exc_value, exc_traceback = exc_info99 exc_lines = "".join(100 traceback.format_exception(exc_type, exc_value, exc_traceback),101 )102 return self._ERROR_MSG.format(103 path=path,104 type=exc_type.__name__,105 body=exc_lines,106 )107 108 def validation_error(self, instance_path, error):109 return self._ERROR_MSG.format(110 path=instance_path,111 type=error.__class__.__name__,112 body=error,113 )114 115 def validation_success(self, instance_path):116 return self._SUCCESS_MSG.format(path=instance_path)117 118 119@define120class _PlainFormatter:121 122 _error_format = field()123 124 def filenotfound_error(self, path, exc_info):125 return f"{path!r} does not exist.\n"126 127 def parsing_error(self, path, exc_info):128 return "Failed to parse {}: {}\n".format(129 "<stdin>" if path == "<stdin>" else repr(path),130 exc_info[1],131 )132 133 def validation_error(self, instance_path, error):134 return self._error_format.format(file_name=instance_path, error=error)135 136 def validation_success(self, instance_path):137 return ""138 139 140def _resolve_name_with_default(name):141 if "." not in name:142 name = "jsonschema." + name143 return resolve_name(name)144 145 146parser = argparse.ArgumentParser(147 description="JSON Schema Validation CLI",148)149parser.add_argument(150 "-i", "--instance",151 action="append",152 dest="instances",153 help="""154 a path to a JSON instance (i.e. filename.json) to validate (may155 be specified multiple times). If no instances are provided via this156 option, one will be expected on standard input.157 """,158)159parser.add_argument(160 "-F", "--error-format",161 help="""162 the format to use for each validation error message, specified163 in a form suitable for str.format. This string will be passed164 one formatted object named 'error' for each ValidationError.165 Only provide this option when using --output=plain, which is the166 default. If this argument is unprovided and --output=plain is167 used, a simple default representation will be used.168 """,169)170parser.add_argument(171 "-o", "--output",172 choices=["plain", "pretty"],173 default="plain",174 help="""175 an output format to use. 'plain' (default) will produce minimal176 text with one line for each error, while 'pretty' will produce177 more detailed human-readable output on multiple lines.178 """,179)180parser.add_argument(181 "-V", "--validator",182 type=_resolve_name_with_default,183 help="""184 the fully qualified object name of a validator to use, or, for185 validators that are registered with jsonschema, simply the name186 of the class.187 """,188)189parser.add_argument(190 "--base-uri",191 help="""192 a base URI to assign to the provided schema, even if it does not193 declare one (via e.g. $id). This option can be used if you wish to194 resolve relative references to a particular URI (or local path)195 """,196)197parser.add_argument(198 "--version",199 action="version",200 version=metadata.version("jsonschema"),201)202parser.add_argument(203 "schema",204 help="the path to a JSON Schema to validate with (i.e. schema.json)",205)206 207 208def parse_args(args): # noqa: D103209 arguments = vars(parser.parse_args(args=args or ["--help"]))210 if arguments["output"] != "plain" and arguments["error_format"]:211 raise parser.error(212 "--error-format can only be used with --output plain",213 )214 if arguments["output"] == "plain" and arguments["error_format"] is None:215 arguments["error_format"] = "{error.instance}: {error.message}\n"216 return arguments217 218 219def _validate_instance(instance_path, instance, validator, outputter):220 invalid = False221 for error in validator.iter_errors(instance):222 invalid = True223 outputter.validation_error(instance_path=instance_path, error=error)224 225 if not invalid:226 outputter.validation_success(instance_path=instance_path)227 return invalid228 229 230def main(args=sys.argv[1:]): # noqa: D103231 sys.exit(run(arguments=parse_args(args=args)))232 233 234def run(arguments, stdout=sys.stdout, stderr=sys.stderr, stdin=sys.stdin): # noqa: D103235 outputter = _Outputter.from_arguments(236 arguments=arguments,237 stdout=stdout,238 stderr=stderr,239 )240 241 try:242 schema = outputter.load(arguments["schema"])243 except _CannotLoadFile:244 return 1245 246 Validator = arguments["validator"]247 if Validator is None:248 Validator = validator_for(schema)249 250 try:251 Validator.check_schema(schema)252 except SchemaError as error:253 outputter.validation_error(254 instance_path=arguments["schema"],255 error=error,256 )257 return 1258 259 if arguments["instances"]:260 load, instances = outputter.load, arguments["instances"]261 else:262 def load(_):263 try:264 return json.load(stdin)265 except JSONDecodeError as error:266 outputter.parsing_error(267 path="<stdin>", exc_info=sys.exc_info(),268 )269 raise _CannotLoadFile() from error270 instances = ["<stdin>"]271 272 resolver = _RefResolver(273 base_uri=arguments["base_uri"],274 referrer=schema,275 ) if arguments["base_uri"] is not None else None276 277 validator = Validator(schema, resolver=resolver)278 exit_code = 0279 for each in instances:280 try:281 instance = load(each)282 except _CannotLoadFile:283 exit_code = 1284 else:285 exit_code |= _validate_instance(286 instance_path=each,287 instance=instance,288 validator=validator,289 outputter=outputter,290 )291 292 return exit_code293 