Skip to content

Commit

Permalink
python-jsonschema#251 - Initial attempt to support stdin
Browse files Browse the repository at this point in the history
Works through steps of argument parsing and accessing open files.  TODO:  Update schema parser arguments.
  • Loading branch information
sloanlance committed Sep 25, 2023
1 parent be970d8 commit 10d8aaf
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 59 deletions.
118 changes: 68 additions & 50 deletions src/check_jsonschema/cli/main_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import os
import textwrap
from io import TextIOWrapper

import click
import jsonschema
Expand All @@ -26,7 +27,8 @@
f"custom.{k}" for k in CUSTOM_SCHEMA_NAMES
]
BUILTIN_SCHEMA_CHOICES = (
BUILTIN_SCHEMA_NAMES + list(SCHEMA_CATALOG.keys()) + CUSTOM_SCHEMA_NAMES
BUILTIN_SCHEMA_NAMES + list(
SCHEMA_CATALOG.keys()) + CUSTOM_SCHEMA_NAMES
)


Expand Down Expand Up @@ -55,6 +57,18 @@ def pretty_helptext_list(values: list[str] | tuple[str, ...]) -> str:
)


class FilesDefaultStdin(click.Argument):
def __init__(self, *args, **kwargs):
kwargs['nargs'] = -1
kwargs['type'] = click.File('r') # will work with '-' for stdin?
# kwargs['type'] = click.Path() # works with '/dev/stdin'
super().__init__(*args, **kwargs)

def process_value(self, ctx, value):
return super().process_value(ctx, value or ('-',))
# return super().process_value(ctx, value or ('/dev/stdin',))


@click.command(
"check-jsonschema",
help="""\
Expand All @@ -75,30 +89,30 @@ def pretty_helptext_list(values: list[str] | tuple[str, ...]) -> str:
\b
The '--builtin-schema' flag supports the following schema names:
"""
+ pretty_helptext_list(BUILTIN_SCHEMA_NAMES)
+ """\
+ pretty_helptext_list(BUILTIN_SCHEMA_NAMES)
+ """\
\b
The '--disable-formats' flag supports the following formats:
"""
+ pretty_helptext_list(KNOWN_FORMATS),
+ pretty_helptext_list(KNOWN_FORMATS),
)
@click.help_option("-h", "--help")
@click.version_option()
@click.option(
"--schemafile",
help=(
"The path to a file containing the JSON Schema to use or an "
"HTTP(S) URI for the schema. If a remote file is used, "
"it will be downloaded and cached locally based on mtime."
"The path to a file containing the JSON Schema to use or an "
"HTTP(S) URI for the schema. If a remote file is used, "
"it will be downloaded and cached locally based on mtime."
),
)
@click.option(
"--base-uri",
help=(
"Override the base URI for the schema. The default behavior is to "
"follow the behavior specified by the JSON Schema spec, which is to "
"prefer an explicit '$id' and failover to the retrieval URI."
"Override the base URI for the schema. The default behavior is to "
"follow the behavior specified by the JSON Schema spec, which is to "
"prefer an explicit '$id' and failover to the retrieval URI."
),
)
@click.option(
Expand All @@ -111,8 +125,8 @@ def pretty_helptext_list(values: list[str] | tuple[str, ...]) -> str:
"--check-metaschema",
is_flag=True,
help=(
"Instead of validating the instances against a schema, treat each file as a "
"schema and validate them under their matching metaschemas."
"Instead of validating the instances against a schema, treat each file as a "
"schema and validate them under their matching metaschemas."
),
)
@click.option(
Expand All @@ -123,26 +137,27 @@ def pretty_helptext_list(values: list[str] | tuple[str, ...]) -> str:
@click.option(
"--cache-filename",
help=(
"The name to use for caching a remote schema. "
"Defaults to the last slash-delimited part of the URI."
"The name to use for caching a remote schema. "
"Defaults to the last slash-delimited part of the URI."
),
)
@click.option(
"--disable-formats",
multiple=True,
help="Disable specific format checks in the schema. "
"Pass '*' to disable all format checks.",
"Pass '*' to disable all format checks.",
type=CommaDelimitedList(choices=("*", *KNOWN_FORMATS)),
metavar="{*|FORMAT,FORMAT,...}",
)
@click.option(
"--format-regex",
help=(
"Set the mode of format validation for regexes. "
"If `--disable-formats regex` is used, this option has no effect."
"Set the mode of format validation for regexes. "
"If `--disable-formats regex` is used, this option has no effect."
),
default=RegexVariantName.default.value,
type=click.Choice([x.value for x in RegexVariantName], case_sensitive=False),
type=click.Choice([x.value for x in RegexVariantName],
case_sensitive=False),
)
@click.option(
"--default-filetype",
Expand All @@ -154,36 +169,36 @@ def pretty_helptext_list(values: list[str] | tuple[str, ...]) -> str:
@click.option(
"--traceback-mode",
help=(
"Set the mode of presentation for error traces. "
"Defaults to shortened tracebacks."
"Set the mode of presentation for error traces. "
"Defaults to shortened tracebacks."
),
type=click.Choice(("full", "short")),
default="short",
)
@click.option(
"--data-transform",
help=(
"Select a builtin transform which should be applied to instancefiles before "
"they are checked."
"Select a builtin transform which should be applied to instancefiles before "
"they are checked."
),
type=click.Choice(tuple(TRANSFORM_LIBRARY.keys())),
)
@click.option(
"--fill-defaults",
help=(
"Autofill 'default' values prior to validation. "
"This may conflict with certain third-party validators used with "
"'--validator-class'"
"Autofill 'default' values prior to validation. "
"This may conflict with certain third-party validators used with "
"'--validator-class'"
),
is_flag=True,
)
@click.option(
"--validator-class",
help=(
"The fully qualified name of a python validator to use in place of "
"the 'jsonschema' library validators, in the form of '<package>:<class>'. "
"The validator must be importable in the same environment where "
"'check-jsonschema' is run."
"The fully qualified name of a python validator to use in place of "
"the 'jsonschema' library validators, in the form of '<package>:<class>'. "
"The validator must be importable in the same environment where "
"'check-jsonschema' is run."
),
type=ValidatorClassName(),
)
Expand All @@ -206,8 +221,8 @@ def pretty_helptext_list(values: list[str] | tuple[str, ...]) -> str:
"-v",
"--verbose",
help=(
"Increase output verbosity. On validation errors, this may be especially "
"useful when oneOf or anyOf is used in the schema."
"Increase output verbosity. On validation errors, this may be especially "
"useful when oneOf or anyOf is used in the schema."
),
count=True,
)
Expand All @@ -217,26 +232,28 @@ def pretty_helptext_list(values: list[str] | tuple[str, ...]) -> str:
help="Reduce output verbosity",
count=True,
)
@click.argument("instancefiles", required=True, nargs=-1)
# @click.argument("instancefiles", required=True, nargs=-1)
@click.argument("instancefiles", cls=FilesDefaultStdin)
def main(
*,
schemafile: str | None,
builtin_schema: str | None,
base_uri: str | None,
check_metaschema: bool,
no_cache: bool,
cache_filename: str | None,
disable_formats: tuple[list[str], ...],
format_regex: str,
default_filetype: str,
traceback_mode: str,
data_transform: str | None,
fill_defaults: bool,
validator_class: type[jsonschema.protocols.Validator] | None,
output_format: str,
verbose: int,
quiet: int,
instancefiles: tuple[str, ...],
*,
schemafile: str | None,
builtin_schema: str | None,
base_uri: str | None,
check_metaschema: bool,
no_cache: bool,
cache_filename: str | None,
disable_formats: tuple[list[str], ...],
format_regex: str,
default_filetype: str,
traceback_mode: str,
data_transform: str | None,
fill_defaults: bool,
validator_class: type[jsonschema.protocols.Validator] | None,
output_format: str,
verbose: int,
quiet: int,
# instancefiles: tuple[str, ...],
instancefiles: tuple[TextIOWrapper, ...],
) -> None:
args = ParseResult()

Expand All @@ -245,6 +262,7 @@ def main(

args.base_uri = base_uri
args.instancefiles = instancefiles
print(list(i.name for i in instancefiles))

normalized_disable_formats: tuple[str, ...] = tuple(
f for sublist in disable_formats for f in sublist
Expand Down
4 changes: 3 additions & 1 deletion src/check_jsonschema/cli/parse_result.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import enum
from io import TextIOWrapper

import click
import jsonschema
Expand All @@ -21,7 +22,8 @@ def __init__(self) -> None:
self.schema_mode: SchemaLoadingMode = SchemaLoadingMode.filepath
self.schema_path: str | None = None
self.base_uri: str | None = None
self.instancefiles: tuple[str, ...] = ()
# self.instancefiles: tuple[str, ...] = ()
self.instancefiles: tuple[TextIOWrapper, ...] = ()
# cache controls
self.disable_cache: bool = False
self.cache_filename: str | None = None
Expand Down
12 changes: 8 additions & 4 deletions src/check_jsonschema/instance_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pathlib
import typing as t
from io import TextIOWrapper

from .parsers import ParseError, ParserSet
from .transforms import Transform
Expand All @@ -10,7 +11,8 @@
class InstanceLoader:
def __init__(
self,
filenames: t.Sequence[str],
# filenames: t.Sequence[str],
filenames: t.Sequence[TextIOWrapper],
default_filetype: str = "json",
data_transform: Transform | None = None,
) -> None:
Expand All @@ -26,11 +28,13 @@ def __init__(

def iter_files(self) -> t.Iterator[tuple[pathlib.Path, ParseError | t.Any]]:
for fn in self._filenames:
path = pathlib.Path(fn)
# path = pathlib.Path(fn)
try:
data: t.Any = self._parsers.parse_file(path, self._default_filetype)
# data: t.Any = self._parsers.parse_file(path, self._default_filetype)
data: t.Any = self._parsers.parse_file(fn, self._default_filetype)
except ParseError as err:
data = err
else:
data = self._data_transform(data)
yield (path, data)
# yield (path, data)
yield (fn.name, data)
12 changes: 8 additions & 4 deletions src/check_jsonschema/parsers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import json
import pathlib
import typing as t
from io import TextIOWrapper

import ruamel.yaml

Expand Down Expand Up @@ -84,7 +85,8 @@ def get(
)

def parse_data_with_path(
self, data: t.BinaryIO | bytes, path: pathlib.Path | str, default_filetype: str
# self, data: t.BinaryIO | bytes, path: pathlib.Path | str, default_filetype: str
self, data: t.TextIO, path: pathlib.Path | str, default_filetype: str
) -> t.Any:
loadfunc = self.get(path, default_filetype)
try:
Expand All @@ -94,6 +96,8 @@ def parse_data_with_path(
except LOADING_FAILURE_ERROR_TYPES as e:
raise FailedFileLoadError(f"Failed to parse {path}") from e

def parse_file(self, path: pathlib.Path | str, default_filetype: str) -> t.Any:
with open(path, "rb") as fp:
return self.parse_data_with_path(fp, path, default_filetype)
# def parse_file(self, path: pathlib.Path | str, default_filetype: str) -> t.Any:
def parse_file(self, path: TextIOWrapper, default_filetype: str) -> t.Any:
# with open(path, "rb") as fp:
# return self.parse_data_with_path(fp, path, default_filetype)
return self.parse_data_with_path(path, path.name, default_filetype)

0 comments on commit 10d8aaf

Please sign in to comment.