Skip to content

Commit ac5ad1d

Browse files
committed
Add support for passing --schemafile '-' for stdin
1 parent ffd764b commit ac5ad1d

File tree

5 files changed

+78
-11
lines changed

5 files changed

+78
-11
lines changed

src/check_jsonschema/cli/main_command.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,10 @@ def pretty_helptext_list(values: list[str] | tuple[str, ...]) -> str:
9191
help=(
9292
"The path to a file containing the JSON Schema to use or an "
9393
"HTTP(S) URI for the schema. If a remote file is used, "
94-
"it will be downloaded and cached locally based on mtime."
94+
"it will be downloaded and cached locally based on mtime. "
95+
"Use '-' for stdin."
9596
),
97+
metavar="[PATH|URI]",
9698
)
9799
@click.option(
98100
"--base-uri",

src/check_jsonschema/instance_loader.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import io
34
import typing as t
45

56
from .parsers import ParseError, ParserSet
@@ -25,14 +26,21 @@ def __init__(
2526

2627
def iter_files(self) -> t.Iterator[tuple[str, ParseError | t.Any]]:
2728
for file in self._files:
28-
if not hasattr(file, "name"):
29+
if hasattr(file, "name"):
30+
name = file.name
31+
# allowing for BytesIO to be special-cased here is useful for
32+
# simpler test setup, since this is what tests will pass and we naturally
33+
# support it here
34+
elif isinstance(file, io.BytesIO) or file.fileno() == 0:
35+
name = "<stdin>"
36+
else:
2937
raise ValueError(f"File {file} has no name attribute")
3038
try:
3139
data: t.Any = self._parsers.parse_data_with_path(
32-
file, file.name, self._default_filetype
40+
file, name, self._default_filetype
3341
)
3442
except ParseError as err:
3543
data = err
3644
else:
3745
data = self._data_transform(data)
38-
yield (file.name, data)
46+
yield (name, data)

src/check_jsonschema/schema_loader/main.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from ..parsers import ParserSet
1313
from ..utils import is_url_ish
1414
from .errors import UnsupportedUrlScheme
15-
from .readers import HttpSchemaReader, LocalSchemaReader
15+
from .readers import HttpSchemaReader, LocalSchemaReader, StdinSchemaReader
1616
from .resolver import make_reference_registry
1717

1818

@@ -82,15 +82,22 @@ def __init__(
8282
self._parsers = ParserSet()
8383

8484
# setup a schema reader lazily, when needed
85-
self._reader: LocalSchemaReader | HttpSchemaReader | None = None
85+
self._reader: LocalSchemaReader | HttpSchemaReader | StdinSchemaReader | None = (
86+
None
87+
)
8688

8789
@property
88-
def reader(self) -> LocalSchemaReader | HttpSchemaReader:
90+
def reader(self) -> LocalSchemaReader | HttpSchemaReader | StdinSchemaReader:
8991
if self._reader is None:
9092
self._reader = self._get_schema_reader()
9193
return self._reader
9294

93-
def _get_schema_reader(self) -> LocalSchemaReader | HttpSchemaReader:
95+
def _get_schema_reader(
96+
self,
97+
) -> LocalSchemaReader | HttpSchemaReader | StdinSchemaReader:
98+
if self.schemafile == "-":
99+
return StdinSchemaReader()
100+
94101
if self.url_info is None or self.url_info.scheme in ("file", ""):
95102
return LocalSchemaReader(self.schemafile)
96103

src/check_jsonschema/schema_loader/readers.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from __future__ import annotations
22

33
import io
4+
import json
5+
import sys
46
import typing as t
57

68
import ruamel.yaml
79

810
from ..cachedownloader import CacheDownloader
9-
from ..parsers import ParserSet
11+
from ..parsers import ParseError, ParserSet
1012
from ..utils import filename2path
1113
from .errors import SchemaParseError
1214

@@ -30,7 +32,7 @@ def __init__(self, filename: str) -> None:
3032
self.filename = str(self.path)
3133
self.parsers = ParserSet()
3234

33-
def get_retrieval_uri(self) -> str:
35+
def get_retrieval_uri(self) -> str | None:
3436
return self.path.as_uri()
3537

3638
def _read_impl(self) -> t.Any:
@@ -40,6 +42,20 @@ def read_schema(self) -> dict:
4042
return _run_load_callback(self.filename, self._read_impl)
4143

4244

45+
class StdinSchemaReader:
46+
def __init__(self) -> None:
47+
self.parsers = ParserSet()
48+
49+
def get_retrieval_uri(self) -> str | None:
50+
return None
51+
52+
def read_schema(self) -> dict:
53+
try:
54+
return json.load(sys.stdin)
55+
except ValueError as e:
56+
raise ParseError("Failed to parse JSON from stdin") from e
57+
58+
4359
class HttpSchemaReader:
4460
def __init__(
4561
self,
@@ -64,7 +80,7 @@ def _parse(self, schema_bytes: bytes) -> t.Any:
6480
)
6581
return self._parsed_schema
6682

67-
def get_retrieval_uri(self) -> str:
83+
def get_retrieval_uri(self) -> str | None:
6884
return self.url
6985

7086
def _read_impl(self) -> t.Any:

tests/acceptance/test_special_filetypes.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,37 @@ def _fake_compute_default_cache_dir(self):
114114
assert result.exit_code == 0
115115
else:
116116
assert result.exit_code == 1
117+
118+
119+
@pytest.mark.parametrize("check_passes", (True, False))
120+
@pytest.mark.parametrize("using_stdin", ("schema", "instance"))
121+
def test_schema_or_instance_from_stdin(
122+
run_line, check_passes, tmp_path, monkeypatch, using_stdin
123+
):
124+
"""
125+
a "remote schema" (meaning HTTPS) with bad data, therefore requiring that a retry
126+
fires in order to parse
127+
"""
128+
if using_stdin == "schema":
129+
instance_path = tmp_path / "instance.json"
130+
instance_path.write_text("42" if check_passes else '"foo"')
131+
132+
result = run_line(
133+
["check-jsonschema", "--schemafile", "-", str(instance_path)],
134+
input='{"type": "integer"}',
135+
)
136+
elif using_stdin == "instance":
137+
schema_path = tmp_path / "schema.json"
138+
schema_path.write_text('{"type": "integer"}')
139+
instance = "42" if check_passes else '"foo"'
140+
141+
result = run_line(
142+
["check-jsonschema", "--schemafile", schema_path, "-"],
143+
input=instance,
144+
)
145+
else:
146+
raise NotImplementedError
147+
if check_passes:
148+
assert result.exit_code == 0
149+
else:
150+
assert result.exit_code == 1

0 commit comments

Comments
 (0)