Skip to content

Commit ffd764b

Browse files
committed
Support stdin for instancefiles using '-'
- instancefiles are read using `click.File("rb")` - the resulting IO objects are passed around, rather than the filenames or path objects - tests need to account for this change in the InstanceLoader
1 parent be970d8 commit ffd764b

File tree

9 files changed

+131
-71
lines changed

9 files changed

+131
-71
lines changed

src/check_jsonschema/checker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def _fail(self, msg: str, err: Exception | None = None) -> t.NoReturn:
4747
raise _Exit(1)
4848

4949
def get_validator(
50-
self, path: pathlib.Path, doc: dict[str, t.Any]
50+
self, path: pathlib.Path | str, doc: dict[str, t.Any]
5151
) -> jsonschema.protocols.Validator:
5252
try:
5353
return self._schema_loader.get_validator(

src/check_jsonschema/cli/main_command.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import os
44
import textwrap
5+
import typing as t
56

67
import click
78
import jsonschema
@@ -217,7 +218,7 @@ def pretty_helptext_list(values: list[str] | tuple[str, ...]) -> str:
217218
help="Reduce output verbosity",
218219
count=True,
219220
)
220-
@click.argument("instancefiles", required=True, nargs=-1)
221+
@click.argument("instancefiles", required=True, nargs=-1, type=click.File("rb"))
221222
def main(
222223
*,
223224
schemafile: str | None,
@@ -236,7 +237,7 @@ def main(
236237
output_format: str,
237238
verbose: int,
238239
quiet: int,
239-
instancefiles: tuple[str, ...],
240+
instancefiles: tuple[t.BinaryIO, ...],
240241
) -> None:
241242
args = ParseResult()
242243

src/check_jsonschema/cli/parse_result.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import enum
4+
import typing as t
45

56
import click
67
import jsonschema
@@ -21,7 +22,7 @@ def __init__(self) -> None:
2122
self.schema_mode: SchemaLoadingMode = SchemaLoadingMode.filepath
2223
self.schema_path: str | None = None
2324
self.base_uri: str | None = None
24-
self.instancefiles: tuple[str, ...] = ()
25+
self.instancefiles: tuple[t.BinaryIO, ...] = ()
2526
# cache controls
2627
self.disable_cache: bool = False
2728
self.cache_filename: str | None = None
Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import pathlib
43
import typing as t
54

65
from .parsers import ParseError, ParserSet
@@ -10,11 +9,11 @@
109
class InstanceLoader:
1110
def __init__(
1211
self,
13-
filenames: t.Sequence[str],
12+
files: t.Sequence[t.BinaryIO],
1413
default_filetype: str = "json",
1514
data_transform: Transform | None = None,
1615
) -> None:
17-
self._filenames = filenames
16+
self._files = files
1817
self._default_filetype = default_filetype
1918
self._data_transform = (
2019
data_transform if data_transform is not None else Transform()
@@ -24,13 +23,16 @@ def __init__(
2423
modify_yaml_implementation=self._data_transform.modify_yaml_implementation
2524
)
2625

27-
def iter_files(self) -> t.Iterator[tuple[pathlib.Path, ParseError | t.Any]]:
28-
for fn in self._filenames:
29-
path = pathlib.Path(fn)
26+
def iter_files(self) -> t.Iterator[tuple[str, ParseError | t.Any]]:
27+
for file in self._files:
28+
if not hasattr(file, "name"):
29+
raise ValueError(f"File {file} has no name attribute")
3030
try:
31-
data: t.Any = self._parsers.parse_file(path, self._default_filetype)
31+
data: t.Any = self._parsers.parse_data_with_path(
32+
file, file.name, self._default_filetype
33+
)
3234
except ParseError as err:
3335
data = err
3436
else:
3537
data = self._data_transform(data)
36-
yield (path, data)
38+
yield (file.name, data)

src/check_jsonschema/result.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import pathlib
24

35
import jsonschema
@@ -15,18 +17,18 @@ def __init__(self) -> None:
1517
def success(self) -> bool:
1618
return not (bool(self.parse_errors) or bool(self.validation_errors))
1719

18-
def record_validation_success(self, path: pathlib.Path) -> None:
20+
def record_validation_success(self, path: pathlib.Path | str) -> None:
1921
self.successes.append(str(path))
2022

2123
def record_validation_error(
22-
self, path: pathlib.Path, err: jsonschema.ValidationError
24+
self, path: pathlib.Path | str, err: jsonschema.ValidationError
2325
) -> None:
2426
filename = str(path)
2527
if filename not in self.validation_errors:
2628
self.validation_errors[filename] = []
2729
self.validation_errors[filename].append(err)
2830

29-
def record_parse_error(self, path: pathlib.Path, err: ParseError) -> None:
31+
def record_parse_error(self, path: pathlib.Path | str, err: ParseError) -> None:
3032
filename = str(path)
3133
if filename not in self.parse_errors:
3234
self.parse_errors[filename] = []

src/check_jsonschema/schema_loader/main.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def set_defaults_then_validate(
4747
class SchemaLoaderBase:
4848
def get_validator(
4949
self,
50-
path: pathlib.Path,
50+
path: pathlib.Path | str,
5151
instance_doc: dict[str, t.Any],
5252
format_opts: FormatOptions,
5353
fill_defaults: bool,
@@ -117,7 +117,7 @@ def get_schema(self) -> dict[str, t.Any]:
117117

118118
def get_validator(
119119
self,
120-
path: pathlib.Path,
120+
path: pathlib.Path | str,
121121
instance_doc: dict[str, t.Any],
122122
format_opts: FormatOptions,
123123
fill_defaults: bool,
@@ -189,7 +189,7 @@ def __init__(self, base_uri: str | None = None) -> None:
189189

190190
def get_validator(
191191
self,
192-
path: pathlib.Path,
192+
path: pathlib.Path | str,
193193
instance_doc: dict[str, t.Any],
194194
format_opts: FormatOptions,
195195
fill_defaults: bool,

tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
import pathlib
23
import sys
34

@@ -38,3 +39,10 @@ def func(path, text):
3839
for name in all_names_to_clear:
3940
if name in sys.modules:
4041
del sys.modules[name]
42+
43+
44+
@pytest.fixture
45+
def in_tmp_dir(request, tmp_path):
46+
os.chdir(str(tmp_path))
47+
yield
48+
os.chdir(request.config.invocation_dir)

tests/unit/test_cli_parse.py

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

3+
import io
34
from unittest import mock
45

56
import click
@@ -14,6 +15,11 @@ class BoxedContext:
1415
ref = None
1516

1617

18+
def touch_files(dirpath, *filenames):
19+
for fname in filenames:
20+
(dirpath / fname).touch()
21+
22+
1723
@pytest.fixture
1824
def boxed_context():
1925
return BoxedContext()
@@ -73,19 +79,24 @@ def test_requires_some_args(runner):
7379
assert result.exit_code == 2
7480

7581

76-
def test_schemafile_and_instancefile(runner, mock_parse_result):
82+
def test_schemafile_and_instancefile(runner, mock_parse_result, in_tmp_dir, tmp_path):
83+
touch_files(tmp_path, "foo.json")
7784
runner.invoke(cli_main, ["--schemafile", "schema.json", "foo.json"])
7885
assert mock_parse_result.schema_mode == SchemaLoadingMode.filepath
7986
assert mock_parse_result.schema_path == "schema.json"
80-
assert mock_parse_result.instancefiles == ("foo.json",)
87+
assert isinstance(mock_parse_result.instancefiles, tuple)
88+
for f in mock_parse_result.instancefiles:
89+
assert isinstance(f, (io.BytesIO, io.BufferedReader))
90+
assert tuple(f.name for f in mock_parse_result.instancefiles) == ("foo.json",)
8191

8292

8393
def test_requires_at_least_one_instancefile(runner):
8494
result = runner.invoke(cli_main, ["--schemafile", "schema.json"])
8595
assert result.exit_code == 2
8696

8797

88-
def test_requires_schemafile(runner):
98+
def test_requires_schemafile(runner, in_tmp_dir, tmp_path):
99+
touch_files(tmp_path, "foo.json")
89100
result = runner.invoke(cli_main, ["foo.json"])
90101
assert result.exit_code == 2
91102

@@ -95,7 +106,8 @@ def test_no_cache_defaults_false(runner, mock_parse_result):
95106
assert mock_parse_result.disable_cache is False
96107

97108

98-
def test_no_cache_flag_is_true(runner, mock_parse_result):
109+
def test_no_cache_flag_is_true(runner, mock_parse_result, in_tmp_dir, tmp_path):
110+
touch_files(tmp_path, "foo.json")
99111
runner.invoke(cli_main, ["--schemafile", "schema.json", "foo.json", "--no-cache"])
100112
assert mock_parse_result.disable_cache is True
101113

@@ -108,32 +120,29 @@ def test_no_cache_flag_is_true(runner, mock_parse_result):
108120
"x.json",
109121
"--builtin-schema",
110122
"vendor.travis",
111-
"foo.json",
112123
],
113124
[
114125
"--schemafile",
115126
"x.json",
116127
"--builtin-schema",
117128
"vendor.travis",
118129
"--check-metaschema",
119-
"foo.json",
120130
],
121131
[
122132
"--schemafile",
123133
"x.json",
124134
"--check-metaschema",
125-
"foo.json",
126135
],
127136
[
128137
"--builtin-schema",
129138
"vendor.travis",
130139
"--check-metaschema",
131-
"foo.json",
132140
],
133141
],
134142
)
135-
def test_mutex_schema_opts(runner, cmd_args):
136-
result = runner.invoke(cli_main, cmd_args)
143+
def test_mutex_schema_opts(runner, cmd_args, in_tmp_dir, tmp_path):
144+
touch_files(tmp_path, "foo.json")
145+
result = runner.invoke(cli_main, cmd_args + ["foo.json"])
137146
assert result.exit_code == 2
138147
assert "are mutually exclusive" in result.stderr
139148

@@ -154,12 +163,15 @@ def test_supports_common_option(runner, cmd_args):
154163
@pytest.mark.parametrize(
155164
"setting,expect_value", [(None, None), ("1", False), ("0", False)]
156165
)
157-
def test_no_color_env_var(runner, monkeypatch, setting, expect_value, boxed_context):
166+
def test_no_color_env_var(
167+
runner, monkeypatch, setting, expect_value, boxed_context, in_tmp_dir, tmp_path
168+
):
158169
if setting is None:
159170
monkeypatch.delenv("NO_COLOR", raising=False)
160171
else:
161172
monkeypatch.setenv("NO_COLOR", setting)
162173

174+
touch_files(tmp_path, "foo.json")
163175
runner.invoke(cli_main, ["--schemafile", "schema.json", "foo.json"])
164176
assert boxed_context.ref.color == expect_value
165177

@@ -168,18 +180,22 @@ def test_no_color_env_var(runner, monkeypatch, setting, expect_value, boxed_cont
168180
"setting,expected_value",
169181
[(None, None), ("auto", None), ("always", True), ("never", False)],
170182
)
171-
def test_color_cli_option(runner, setting, expected_value, boxed_context):
183+
def test_color_cli_option(
184+
runner, setting, expected_value, boxed_context, in_tmp_dir, tmp_path
185+
):
172186
args = ["--schemafile", "schema.json", "foo.json"]
173187
if setting:
174188
args.extend(("--color", setting))
189+
touch_files(tmp_path, "foo.json")
175190
runner.invoke(cli_main, args)
176191
assert boxed_context.ref.color == expected_value
177192

178193

179194
def test_no_color_env_var_overrides_cli_option(
180-
runner, monkeypatch, mock_cli_exec, boxed_context
195+
runner, monkeypatch, mock_cli_exec, boxed_context, in_tmp_dir, tmp_path
181196
):
182197
monkeypatch.setenv("NO_COLOR", "1")
198+
touch_files(tmp_path, "foo.json")
183199
runner.invoke(
184200
cli_main, ["--color=always", "--schemafile", "schema.json", "foo.json"]
185201
)
@@ -190,16 +206,21 @@ def test_no_color_env_var_overrides_cli_option(
190206
"setting,expected_value",
191207
[("auto", 0), ("always", 0), ("never", 0), ("anything_else", 2)],
192208
)
193-
def test_color_cli_option_is_choice(runner, setting, expected_value):
209+
def test_color_cli_option_is_choice(
210+
runner, setting, expected_value, in_tmp_dir, tmp_path
211+
):
212+
touch_files(tmp_path, "foo.json")
194213
assert (
195214
runner.invoke(
196-
cli_main, ["--color", setting, "--schemafile", "schema.json", "foo.json"]
215+
cli_main,
216+
["--color", setting, "--schemafile", "schema.json", "foo.json"],
197217
).exit_code
198218
== expected_value
199219
)
200220

201221

202-
def test_formats_default_to_enabled(runner, mock_parse_result):
222+
def test_formats_default_to_enabled(runner, mock_parse_result, in_tmp_dir, tmp_path):
223+
touch_files(tmp_path, "foo.json")
203224
runner.invoke(cli_main, ["--schemafile", "schema.json", "foo.json"])
204225
assert mock_parse_result.disable_all_formats is False
205226
assert mock_parse_result.disable_formats == ()
@@ -217,7 +238,10 @@ def test_formats_default_to_enabled(runner, mock_parse_result):
217238
["--disable-formats", "uri-reference,date-time"],
218239
),
219240
)
220-
def test_disable_selected_formats(runner, mock_parse_result, addargs):
241+
def test_disable_selected_formats(
242+
runner, mock_parse_result, addargs, in_tmp_dir, tmp_path
243+
):
244+
touch_files(tmp_path, "foo.json")
221245
runner.invoke(
222246
cli_main,
223247
[
@@ -246,7 +270,8 @@ def test_disable_selected_formats(runner, mock_parse_result, addargs):
246270
["--disable-formats", "*,email"],
247271
),
248272
)
249-
def test_disable_all_formats(runner, mock_parse_result, addargs):
273+
def test_disable_all_formats(runner, mock_parse_result, addargs, in_tmp_dir, tmp_path):
274+
touch_files(tmp_path, "foo.json")
250275
# this should be an override, with or without other args
251276
runner.invoke(
252277
cli_main,
@@ -260,10 +285,13 @@ def test_disable_all_formats(runner, mock_parse_result, addargs):
260285
assert mock_parse_result.disable_all_formats is True
261286

262287

263-
def test_can_specify_custom_validator_class(runner, mock_parse_result, mock_module):
288+
def test_can_specify_custom_validator_class(
289+
runner, mock_parse_result, mock_module, in_tmp_dir, tmp_path
290+
):
264291
mock_module("foo.py", "class MyValidator: pass")
265292
import foo
266293

294+
touch_files(tmp_path, "foo.json")
267295
result = runner.invoke(
268296
cli_main,
269297
[
@@ -281,7 +309,9 @@ def test_can_specify_custom_validator_class(runner, mock_parse_result, mock_modu
281309
@pytest.mark.parametrize(
282310
"failmode", ("syntax", "import", "attr", "function", "non_callable")
283311
)
284-
def test_custom_validator_class_fails(runner, mock_parse_result, mock_module, failmode):
312+
def test_custom_validator_class_fails(
313+
runner, mock_parse_result, mock_module, failmode, in_tmp_dir, tmp_path
314+
):
285315
mock_module(
286316
"foo.py",
287317
"""\
@@ -307,6 +337,7 @@ def validator_func(*args, **kwargs):
307337
else:
308338
raise NotImplementedError
309339

340+
touch_files(tmp_path, "foo.json")
310341
result = runner.invoke(
311342
cli_main,
312343
["--schemafile", "schema.json", "foo.json", "--validator-class", arg],

0 commit comments

Comments
 (0)