Skip to content

Commit d90f8d4

Browse files
committed
Apply click-type-test to check CLI annotations
Using click-type-test, we can confirm that the parameter annotations are correct. Most of the annotations line up. The only things which needed special attention were `Literal` deductions from `click.Choice` parameters. A couple of the types are easily tripped up by some of the dynamism at play, so these are solved with `overrides` in the typing test. There's also an adjustment to fix a BinaryIO/IO[bytes] discrepancy, driven mostly to become consistent with the click type annotations.
1 parent 3e16eef commit d90f8d4

File tree

10 files changed

+48
-19
lines changed

10 files changed

+48
-19
lines changed

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ console_scripts =
3939
[options.extras_require]
4040
dev =
4141
pytest<9
42+
click-type-test==0.0.7;python_version>="3.10"
4243
coverage<8
4344
pytest-xdist<4
4445
responses==0.24.1

src/check_jsonschema/cachedownloader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def _download(self) -> str:
127127
return dest
128128

129129
@contextlib.contextmanager
130-
def open(self) -> t.Generator[t.BinaryIO, None, None]:
130+
def open(self) -> t.Iterator[t.IO[bytes]]:
131131
if (not self._cache_dir) or self._disable_cache:
132132
yield io.BytesIO(self._get_request().content)
133133
else:

src/check_jsonschema/cli/main_command.py

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

33
import os
4+
import sys
45
import textwrap
56
import typing as t
67

@@ -23,6 +24,11 @@
2324
from .param_types import CommaDelimitedList, LazyBinaryReadFile, ValidatorClassName
2425
from .parse_result import ParseResult, SchemaLoadingMode
2526

27+
if sys.version_info >= (3, 8):
28+
from typing import Literal
29+
else:
30+
from typing_extensions import Literal
31+
2632
BUILTIN_SCHEMA_NAMES = [f"vendor.{k}" for k in SCHEMA_CATALOG.keys()] + [
2733
f"custom.{k}" for k in CUSTOM_SCHEMA_NAMES
2834
]
@@ -232,16 +238,16 @@ def main(
232238
no_cache: bool,
233239
cache_filename: str | None,
234240
disable_formats: tuple[list[str], ...],
235-
format_regex: str,
236-
default_filetype: str,
237-
traceback_mode: str,
238-
data_transform: str | None,
241+
format_regex: Literal["python", "default"],
242+
default_filetype: Literal["json", "yaml", "toml", "json5"],
243+
traceback_mode: Literal["full", "short"],
244+
data_transform: Literal["azure-pipelines", "gitlab-ci"] | None,
239245
fill_defaults: bool,
240246
validator_class: type[jsonschema.protocols.Validator] | None,
241-
output_format: str,
247+
output_format: Literal["text", "json"],
242248
verbose: int,
243249
quiet: int,
244-
instancefiles: tuple[t.BinaryIO, ...],
250+
instancefiles: tuple[t.IO[bytes], ...],
245251
) -> None:
246252
args = ParseResult()
247253

src/check_jsonschema/cli/parse_result.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def __init__(self) -> None:
2222
self.schema_mode: SchemaLoadingMode = SchemaLoadingMode.filepath
2323
self.schema_path: str | None = None
2424
self.base_uri: str | None = None
25-
self.instancefiles: tuple[t.BinaryIO, ...] = ()
25+
self.instancefiles: tuple[t.IO[bytes], ...] = ()
2626
# cache controls
2727
self.disable_cache: bool = False
2828
self.cache_filename: str | None = None

src/check_jsonschema/instance_loader.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
class InstanceLoader:
1313
def __init__(
1414
self,
15-
files: t.Sequence[t.BinaryIO | CustomLazyFile],
15+
files: t.Sequence[t.IO[bytes] | CustomLazyFile],
1616
default_filetype: str = "json",
1717
data_transform: Transform | None = None,
1818
) -> None:
@@ -40,7 +40,7 @@ def iter_files(self) -> t.Iterator[tuple[str, ParseError | t.Any]]:
4040

4141
try:
4242
if isinstance(file, CustomLazyFile):
43-
stream: t.BinaryIO = t.cast(t.BinaryIO, file.open())
43+
stream: t.IO[bytes] = t.cast(t.IO[bytes], file.open())
4444
else:
4545
stream = file
4646

src/check_jsonschema/parsers/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from . import json5, toml, yaml
1212

1313
_PARSER_ERRORS: set[type[Exception]] = {json.JSONDecodeError, yaml.ParseError}
14-
DEFAULT_LOAD_FUNC_BY_TAG: dict[str, t.Callable[[t.BinaryIO], t.Any]] = {
14+
DEFAULT_LOAD_FUNC_BY_TAG: dict[str, t.Callable[[t.IO[bytes]], t.Any]] = {
1515
"json": json.load,
1616
}
1717
SUPPORTED_FILE_FORMATS = ["json", "yaml"]
@@ -67,7 +67,7 @@ def __init__(
6767

6868
def get(
6969
self, path: pathlib.Path | str, default_filetype: str
70-
) -> t.Callable[[t.BinaryIO], t.Any]:
70+
) -> t.Callable[[t.IO[bytes]], t.Any]:
7171
filetype = path_to_type(path, default_type=default_filetype)
7272

7373
if filetype in self._by_tag:
@@ -84,7 +84,7 @@ def get(
8484
)
8585

8686
def parse_data_with_path(
87-
self, data: t.BinaryIO | bytes, path: pathlib.Path | str, default_filetype: str
87+
self, data: t.IO[bytes] | bytes, path: pathlib.Path | str, default_filetype: str
8888
) -> t.Any:
8989
loadfunc = self.get(path, default_filetype)
9090
try:

src/check_jsonschema/parsers/json5.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@
2727
if _load is not None:
2828
_load_concrete: t.Callable = _load
2929

30-
def load(stream: t.BinaryIO) -> t.Any:
30+
def load(stream: t.IO[bytes]) -> t.Any:
3131
return _load_concrete(stream)
3232

3333
else:
3434

35-
def load(stream: t.BinaryIO) -> t.Any:
35+
def load(stream: t.IO[bytes]) -> t.Any:
3636
raise NotImplementedError
3737

3838

src/check_jsonschema/parsers/toml.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,14 @@ def _normalize(data: t.Any) -> t.Any:
6262
if ENABLED:
6363
ParseError: type[Exception] = toml_implementation.TOMLDecodeError
6464

65-
def load(stream: t.BinaryIO) -> t.Any:
65+
def load(stream: t.IO[bytes]) -> t.Any:
6666
data = toml_implementation.load(stream)
6767
return _normalize(data)
6868

6969
else:
7070
ParseError = ValueError
7171

72-
def load(stream: t.BinaryIO) -> t.Any:
72+
def load(stream: t.IO[bytes]) -> t.Any:
7373
raise NotImplementedError
7474

7575

src/check_jsonschema/parsers/yaml.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ def _normalize(data: t.Any) -> t.Any:
5353

5454
def impl2loader(
5555
primary: ruamel.yaml.YAML, *fallbacks: ruamel.yaml.YAML
56-
) -> t.Callable[[t.BinaryIO], t.Any]:
57-
def load(stream: t.BinaryIO) -> t.Any:
56+
) -> t.Callable[[t.IO[bytes]], t.Any]:
57+
def load(stream: t.IO[bytes]) -> t.Any:
5858
stream_bytes = stream.read()
5959
lasterr: ruamel.yaml.YAMLError | None = None
6060
data: t.Any = _data_sentinel

tests/unit/test_cli_annotations.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import typing as t
2+
3+
import pytest
4+
5+
from check_jsonschema.cli import main as cli_main
6+
7+
click_type_test = pytest.importorskip(
8+
"click_type_test", reason="tests require 'click-type-test'"
9+
)
10+
11+
12+
def test_annotations_match_click_params():
13+
click_type_test.check_param_annotations(
14+
cli_main,
15+
overrides={
16+
# don't bother with a Literal for this, since it's relatively dynamic data
17+
"builtin_schema": str | None,
18+
# force default_filetype to be a Literal including `json5`, which is only
19+
# included in the choices if a parser is installed
20+
"default_filetype": t.Literal["json", "yaml", "toml", "json5"],
21+
},
22+
)

0 commit comments

Comments
 (0)