Skip to content

Commit 475e0a0

Browse files
committed
Add force-filetype option to force a filetype for a file
1 parent 30599ab commit 475e0a0

File tree

8 files changed

+82
-12
lines changed

8 files changed

+82
-12
lines changed

src/check_jsonschema/cli/main_command.py

Lines changed: 8 additions & 0 deletions
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 typr to use for the file",
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

src/check_jsonschema/cli/parse_result.py

Lines changed: 1 addition & 0 deletions
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

src/check_jsonschema/instance_loader.py

Lines changed: 3 additions & 1 deletion
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

src/check_jsonschema/parsers/__init__.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,16 @@ 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,
6972
) -> t.Callable[[t.IO[bytes]], t.Any]:
7073
filetype = path_to_type(path, default_type=default_filetype)
7174

7275
if filetype in self._by_tag:
76+
filetype = force_filetype or filetype
77+
7378
return self._by_tag[filetype]
7479

7580
if filetype in MISSING_SUPPORT_MESSAGES:
@@ -83,16 +88,25 @@ def get(
8388
)
8489

8590
def parse_data_with_path(
86-
self, data: t.IO[bytes] | bytes, path: pathlib.Path | str, default_filetype: str
91+
self,
92+
data: t.IO[bytes] | bytes,
93+
path: pathlib.Path | str,
94+
default_filetype: str,
95+
force_filetype: str | None,
8796
) -> t.Any:
88-
loadfunc = self.get(path, default_filetype)
97+
loadfunc = self.get(path, default_filetype, force_filetype)
8998
try:
9099
if isinstance(data, bytes):
91100
data = io.BytesIO(data)
92101
return loadfunc(data)
93102
except LOADING_FAILURE_ERROR_TYPES as e:
94103
raise FailedFileLoadError(f"Failed to parse {path}") from e
95104

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

src/check_jsonschema/schema_loader/readers.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ def get_retrieval_uri(self) -> str | None:
4444
return self.path.as_uri()
4545

4646
def _read_impl(self) -> t.Any:
47-
return self.parsers.parse_file(self.path, default_filetype="json")
47+
return self.parsers.parse_file(
48+
self.path, default_filetype="json", force_filetype=None
49+
)
4850

4951
def read_schema(self) -> dict:
5052
if self._parsed_schema is _UNSET:
@@ -84,7 +86,10 @@ def __init__(
8486

8587
def _parse(self, schema_bytes: bytes) -> t.Any:
8688
return self.parsers.parse_data_with_path(
87-
io.BytesIO(schema_bytes), self.url, default_filetype="json"
89+
io.BytesIO(schema_bytes),
90+
self.url,
91+
default_filetype="json",
92+
force_filetype=None,
8893
)
8994

9095
def get_retrieval_uri(self) -> str | None:

src/check_jsonschema/schema_loader/resolver.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def create_retrieve_callable(
5454

5555
def get_local_file(uri: str) -> t.Any:
5656
path = filename2path(uri)
57-
return parser_set.parse_file(path, "json")
57+
return parser_set.parse_file(path, "json", None)
5858

5959
def retrieve_reference(uri: str) -> referencing.Resource[Schema]:
6060
scheme = urllib.parse.urlsplit(uri).scheme
@@ -70,15 +70,17 @@ def retrieve_reference(uri: str) -> referencing.Resource[Schema]:
7070
if full_uri_scheme in ("http", "https"):
7171

7272
def validation_callback(content: bytes) -> None:
73-
parser_set.parse_data_with_path(content, full_uri, "json")
73+
parser_set.parse_data_with_path(content, full_uri, "json", None)
7474

7575
bound_downloader = downloader.bind(
7676
full_uri, validation_callback=validation_callback
7777
)
7878
with bound_downloader.open() as fp:
7979
data = fp.read()
8080

81-
parsed_object = parser_set.parse_data_with_path(data, full_uri, "json")
81+
parsed_object = parser_set.parse_data_with_path(
82+
data, full_uri, "json", None
83+
)
8284
else:
8385
parsed_object = get_local_file(full_uri)
8486

tests/unit/cli/test_annotations.py

Lines changed: 1 addition & 0 deletions
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
)

tests/unit/test_instance_loader.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,50 @@ 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.parametrize(
107+
"filename, force_filetype",
108+
[
109+
("foo.test", "json5"),
110+
("foo.json", "json5"),
111+
],
112+
)
113+
def test_instanceloader_force_filetype_json(
114+
tmp_path, filename, force_filetype, open_wide
115+
):
116+
if not JSON5_ENABLED:
117+
pytest.skip("test requires json5")
118+
f = tmp_path / filename
119+
f.write_text("// a comment\n{}")
120+
loader = InstanceLoader(open_wide(f), force_filetype=force_filetype)
121+
data = list(loader.iter_files())
122+
print(data)
123+
assert data == [(str(f), {})]
124+
125+
89126
def test_instanceloader_unknown_type_nonjson_content(tmp_path, open_wide):
90127
f = tmp_path / "foo" # no extension here
91128
f.write_text("a:b") # non-json data (cannot be detected as JSON)

0 commit comments

Comments
 (0)