Skip to content

Commit

Permalink
Apply click-type-test to check CLI annotations
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sirosen committed Jan 25, 2024
1 parent c45108a commit 3e245f7
Show file tree
Hide file tree
Showing 10 changed files with 48 additions and 19 deletions.
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ console_scripts =
[options.extras_require]
dev =
pytest<8
click-type-test==0.0.7;python_version>="3.10"
coverage<8
pytest-xdist<4
responses==0.24.1
Expand Down
2 changes: 1 addition & 1 deletion src/check_jsonschema/cachedownloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def _download(self) -> str:
return dest

@contextlib.contextmanager
def open(self) -> t.Generator[t.BinaryIO, None, None]:
def open(self) -> t.Iterator[t.IO[bytes]]:
if (not self._cache_dir) or self._disable_cache:
yield io.BytesIO(self._get_request().content)
else:
Expand Down
18 changes: 12 additions & 6 deletions src/check_jsonschema/cli/main_command.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import os
import sys
import textwrap
import typing as t

Expand All @@ -23,6 +24,11 @@
from .param_types import CommaDelimitedList, LazyBinaryReadFile, ValidatorClassName
from .parse_result import ParseResult, SchemaLoadingMode

if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal

BUILTIN_SCHEMA_NAMES = [f"vendor.{k}" for k in SCHEMA_CATALOG.keys()] + [
f"custom.{k}" for k in CUSTOM_SCHEMA_NAMES
]
Expand Down Expand Up @@ -232,16 +238,16 @@ def main(
no_cache: bool,
cache_filename: str | None,
disable_formats: tuple[list[str], ...],
format_regex: str,
default_filetype: str,
traceback_mode: str,
data_transform: str | None,
format_regex: Literal["python", "default"],
default_filetype: Literal["json", "yaml", "toml", "json5"],
traceback_mode: Literal["full", "short"],
data_transform: Literal["azure-pipelines", "gitlab-ci"] | None,
fill_defaults: bool,
validator_class: type[jsonschema.protocols.Validator] | None,
output_format: str,
output_format: Literal["text", "json"],
verbose: int,
quiet: int,
instancefiles: tuple[t.BinaryIO, ...],
instancefiles: tuple[t.IO[bytes], ...],
) -> None:
args = ParseResult()

Expand Down
2 changes: 1 addition & 1 deletion src/check_jsonschema/cli/parse_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def __init__(self) -> None:
self.schema_mode: SchemaLoadingMode = SchemaLoadingMode.filepath
self.schema_path: str | None = None
self.base_uri: str | None = None
self.instancefiles: tuple[t.BinaryIO, ...] = ()
self.instancefiles: tuple[t.IO[bytes], ...] = ()
# cache controls
self.disable_cache: bool = False
self.cache_filename: str | None = None
Expand Down
4 changes: 2 additions & 2 deletions src/check_jsonschema/instance_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
class InstanceLoader:
def __init__(
self,
files: t.Sequence[t.BinaryIO | CustomLazyFile],
files: t.Sequence[t.IO[bytes] | CustomLazyFile],
default_filetype: str = "json",
data_transform: Transform | None = None,
) -> None:
Expand Down Expand Up @@ -40,7 +40,7 @@ def iter_files(self) -> t.Iterator[tuple[str, ParseError | t.Any]]:

try:
if isinstance(file, CustomLazyFile):
stream: t.BinaryIO = t.cast(t.BinaryIO, file.open())
stream: t.IO[bytes] = t.cast(t.IO[bytes], file.open())
else:
stream = file

Expand Down
6 changes: 3 additions & 3 deletions src/check_jsonschema/parsers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from . import json5, toml, yaml

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

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

if filetype in self._by_tag:
Expand All @@ -84,7 +84,7 @@ def get(
)

def parse_data_with_path(
self, data: t.BinaryIO | bytes, path: pathlib.Path | str, default_filetype: str
self, data: t.IO[bytes] | bytes, path: pathlib.Path | str, default_filetype: str
) -> t.Any:
loadfunc = self.get(path, default_filetype)
try:
Expand Down
4 changes: 2 additions & 2 deletions src/check_jsonschema/parsers/json5.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@
if _load is not None:
_load_concrete: t.Callable = _load

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

else:

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


Expand Down
4 changes: 2 additions & 2 deletions src/check_jsonschema/parsers/toml.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,14 @@ def _normalize(data: t.Any) -> t.Any:
if ENABLED:
ParseError: type[Exception] = toml_implementation.TOMLDecodeError

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

else:
ParseError = ValueError

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


Expand Down
4 changes: 2 additions & 2 deletions src/check_jsonschema/parsers/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ def _normalize(data: t.Any) -> t.Any:

def impl2loader(
primary: ruamel.yaml.YAML, *fallbacks: ruamel.yaml.YAML
) -> t.Callable[[t.BinaryIO], t.Any]:
def load(stream: t.BinaryIO) -> t.Any:
) -> t.Callable[[t.IO[bytes]], t.Any]:
def load(stream: t.IO[bytes]) -> t.Any:
stream_bytes = stream.read()
lasterr: ruamel.yaml.YAMLError | None = None
data: t.Any = _data_sentinel
Expand Down
22 changes: 22 additions & 0 deletions tests/unit/test_cli_annotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import typing as t

import pytest

from check_jsonschema.cli import main as cli_main

click_type_test = pytest.importorskip(
"click_type_test", reason="tests require 'click-type-test'"
)


def test_annotations_match_click_params():
click_type_test.check_param_annotations(
cli_main,
overrides={
# don't bother with a Literal for this, since it's relatively dynamic data
"builtin_schema": str | None,
# force default_filetype to be a Literal including `json5`, which is only
# included in the choices if a parser is installed
"default_filetype": t.Literal["json", "yaml", "toml", "json5"],
},
)

0 comments on commit 3e245f7

Please sign in to comment.