Skip to content

Commit 57f0e1c

Browse files
authored
Merge pull request #557 from mschoettle/force-parser
Add --force-filetype option to force a filetype for an instance file
2 parents 30599ab + b0c97c9 commit 57f0e1c

File tree

6 files changed

+71
-8
lines changed

6 files changed

+71
-8
lines changed

Diff for: src/check_jsonschema/cli/main_command.py

+8
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ def pretty_helptext_list(values: list[str] | tuple[str, ...]) -> str:
161161
show_default=True,
162162
type=click.Choice(SUPPORTED_FILE_FORMATS, case_sensitive=True),
163163
)
164+
@click.option(
165+
"--force-filetype",
166+
help="Force a file type to use when parsing instance files",
167+
type=click.Choice(SUPPORTED_FILE_FORMATS, case_sensitive=True),
168+
)
164169
@click.option(
165170
"--traceback-mode",
166171
help=(
@@ -242,6 +247,7 @@ def main(
242247
format_regex: t.Literal["python", "nonunicode", "default"] | None,
243248
regex_variant: t.Literal["python", "nonunicode", "default"] | None,
244249
default_filetype: t.Literal["json", "yaml", "toml", "json5"],
250+
force_filetype: t.Literal["json", "yaml", "toml", "json5"] | None,
245251
traceback_mode: t.Literal["full", "short"],
246252
data_transform: t.Literal["azure-pipelines", "gitlab-ci"] | None,
247253
fill_defaults: bool,
@@ -271,6 +277,7 @@ def main(
271277

272278
args.disable_cache = no_cache
273279
args.default_filetype = default_filetype
280+
args.force_filetype = force_filetype
274281
args.fill_defaults = fill_defaults
275282
if data_transform is not None:
276283
args.data_transform = TRANSFORM_LIBRARY[data_transform]
@@ -311,6 +318,7 @@ def build_instance_loader(args: ParseResult) -> InstanceLoader:
311318
return InstanceLoader(
312319
args.instancefiles,
313320
default_filetype=args.default_filetype,
321+
force_filetype=args.force_filetype,
314322
data_transform=args.data_transform,
315323
)
316324

Diff for: src/check_jsonschema/cli/parse_result.py

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def __init__(self) -> None:
2929
self.cache_filename: str | None = None
3030
# filetype detection (JSON, YAML, TOML, etc)
3131
self.default_filetype: str = "json"
32+
self.force_filetype: str | None = None
3233
# data-transform (for Azure Pipelines and potentially future transforms)
3334
self.data_transform: Transform | None = None
3435
# validation behavioral controls

Diff for: src/check_jsonschema/instance_loader.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ def __init__(
1414
self,
1515
files: t.Sequence[t.IO[bytes] | CustomLazyFile],
1616
default_filetype: str = "json",
17+
force_filetype: str | None = None,
1718
data_transform: Transform | None = None,
1819
) -> None:
1920
self._files = files
2021
self._default_filetype = default_filetype
22+
self._force_filetype = force_filetype
2123
self._data_transform = (
2224
data_transform if data_transform is not None else Transform()
2325
)
@@ -46,7 +48,7 @@ def iter_files(self) -> t.Iterator[tuple[str, ParseError | t.Any]]:
4648

4749
try:
4850
data: t.Any = self._parsers.parse_data_with_path(
49-
stream, name, self._default_filetype
51+
stream, name, self._default_filetype, self._force_filetype
5052
)
5153
except ParseError as err:
5254
data = err

Diff for: src/check_jsonschema/parsers/__init__.py

+21-6
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,15 @@ def __init__(
6565
}
6666

6767
def get(
68-
self, path: pathlib.Path | str, default_filetype: str
68+
self,
69+
path: pathlib.Path | str,
70+
default_filetype: str,
71+
force_filetype: str | None = None,
6972
) -> t.Callable[[t.IO[bytes]], t.Any]:
70-
filetype = path_to_type(path, default_type=default_filetype)
73+
if force_filetype:
74+
filetype = force_filetype
75+
else:
76+
filetype = path_to_type(path, default_type=default_filetype)
7177

7278
if filetype in self._by_tag:
7379
return self._by_tag[filetype]
@@ -83,16 +89,25 @@ def get(
8389
)
8490

8591
def parse_data_with_path(
86-
self, data: t.IO[bytes] | bytes, path: pathlib.Path | str, default_filetype: str
92+
self,
93+
data: t.IO[bytes] | bytes,
94+
path: pathlib.Path | str,
95+
default_filetype: str,
96+
force_filetype: str | None = None,
8797
) -> t.Any:
88-
loadfunc = self.get(path, default_filetype)
98+
loadfunc = self.get(path, default_filetype, force_filetype)
8999
try:
90100
if isinstance(data, bytes):
91101
data = io.BytesIO(data)
92102
return loadfunc(data)
93103
except LOADING_FAILURE_ERROR_TYPES as e:
94104
raise FailedFileLoadError(f"Failed to parse {path}") from e
95105

96-
def parse_file(self, path: pathlib.Path | str, default_filetype: str) -> t.Any:
106+
def parse_file(
107+
self,
108+
path: pathlib.Path | str,
109+
default_filetype: str,
110+
force_filetype: str | None = None,
111+
) -> t.Any:
97112
with open(path, "rb") as fp:
98-
return self.parse_data_with_path(fp, path, default_filetype)
113+
return self.parse_data_with_path(fp, path, default_filetype, force_filetype)

Diff for: tests/unit/cli/test_annotations.py

+1
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ def test_annotations_match_click_params():
1818
# force default_filetype to be a Literal including `json5`, which is only
1919
# included in the choices if a parser is installed
2020
"default_filetype": t.Literal["json", "yaml", "toml", "json5"],
21+
"force_filetype": t.Literal["json", "yaml", "toml", "json5"] | None,
2122
},
2223
)

Diff for: tests/unit/test_instance_loader.py

+37-1
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,49 @@ def test_instanceloader_yaml_data(tmp_path, filename, default_filetype, open_wid
7979
],
8080
)
8181
def test_instanceloader_toml_data(tmp_path, filename, default_filetype, open_wide):
82-
f = tmp_path / "foo.toml"
82+
f = tmp_path / filename
8383
f.write_text('[foo]\nbar = "baz"\n')
8484
loader = InstanceLoader(open_wide(f), default_filetype=default_filetype)
8585
data = list(loader.iter_files())
8686
assert data == [(str(f), {"foo": {"bar": "baz"}})]
8787

8888

89+
@pytest.mark.parametrize(
90+
"filename, force_filetype",
91+
[
92+
("foo.test", "toml"),
93+
("foo", "toml"),
94+
],
95+
)
96+
def test_instanceloader_force_filetype_toml(
97+
tmp_path, filename, force_filetype, open_wide
98+
):
99+
f = tmp_path / filename
100+
f.write_text('[foo]\nbar = "baz"\n')
101+
loader = InstanceLoader(open_wide(f), force_filetype=force_filetype)
102+
data = list(loader.iter_files())
103+
assert data == [(str(f), {"foo": {"bar": "baz"}})]
104+
105+
106+
@pytest.mark.skipif(not JSON5_ENABLED, reason="test requires json5")
107+
@pytest.mark.parametrize(
108+
"filename, force_filetype",
109+
[
110+
("foo.test", "json5"),
111+
("foo.json", "json5"),
112+
],
113+
)
114+
def test_instanceloader_force_filetype_json(
115+
tmp_path, filename, force_filetype, open_wide
116+
):
117+
f = tmp_path / filename
118+
f.write_text("// a comment\n{}")
119+
loader = InstanceLoader(open_wide(f), force_filetype=force_filetype)
120+
data = list(loader.iter_files())
121+
print(data)
122+
assert data == [(str(f), {})]
123+
124+
89125
def test_instanceloader_unknown_type_nonjson_content(tmp_path, open_wide):
90126
f = tmp_path / "foo" # no extension here
91127
f.write_text("a:b") # non-json data (cannot be detected as JSON)

0 commit comments

Comments
 (0)