Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Unreleased
.. vendor-insert-here

- Update vendored schemas: bitbucket-pipelines, mergify, renovate (2026-08-16)
- Fix a ``DeprecationWarning`` emitted under Click 8.5.0 due to use of the
deprecated ``click.utils.LazyFile``

0.38.0
------
Expand Down
3 changes: 3 additions & 0 deletions src/check_jsonschema/cli/main_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,13 @@ def pretty_helptext_list(values: list[str] | tuple[str, ...]) -> str:
"Instead of validating the instances against a schema, treat each file as a "
"schema and validate them under their matching metaschemas."
),
default=False,
)
@click.option(
"--no-cache",
is_flag=True,
help="Disable schema caching. Always download remote schemas.",
default=False,
)
@click.option(
"--cache-filename", help="Deprecated. This option no longer has any effect."
Expand Down Expand Up @@ -191,6 +193,7 @@ def pretty_helptext_list(values: list[str] | tuple[str, ...]) -> str:
"'--validator-class'"
),
is_flag=True,
default=False,
)
@click.option(
"--validator-class",
Expand Down
100 changes: 99 additions & 1 deletion src/check_jsonschema/cli/param_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import re
import stat
import typing as t
from collections.abc import Iterator
from types import TracebackType

import click
import jsonschema
Expand Down Expand Up @@ -123,7 +125,103 @@ def convert(
return t.cast(t.Type[jsonschema.protocols.Validator], result)


class CustomLazyFile(click.utils.LazyFile):
class _LazyFile:
"""A lazy file works like a regular file but it does not fully open
the file but it does perform some basic checks early to see if the
filename parameter does make sense. This is useful for safely opening
files for writing.

:meta private:
"""

name: str
mode: str
encoding: str | None
errors: str | None
atomic: bool
_f: t.IO[t.Any] | None
should_close: bool

def __init__(
self,
filename: str | os.PathLike[str],
mode: str = "r",
encoding: str | None = None,
errors: str | None = "strict",
atomic: bool = False,
) -> None:
self.name = os.fspath(filename)
self.mode = mode
self.encoding = encoding
self.errors = errors
self.atomic = atomic

if self.name == "-":
self._f, self.should_close = open_stream(filename, mode, encoding, errors)
else:
if "r" in mode:
# Open and close the file in case we're opening it for
# reading so that we can catch at least some errors in
# some cases early.
open(filename, mode).close()
self._f = None
self.should_close = True

def __getattr__(self, name: str) -> t.Any:
return getattr(self.open(), name)

def __repr__(self) -> str:
if self._f is not None:
return repr(self._f)
return f"<unopened file '{click.format_filename(self.name)}' {self.mode}>"

def open(self) -> t.IO[t.Any]:
"""Opens the file if it's not yet open. This call might fail with
a :exc:`FileError`. Not handling this error will produce an error
that Click shows.
"""
if self._f is not None:
return self._f
try:
rv, self.should_close = open_stream(
self.name, self.mode, self.encoding, self.errors, atomic=self.atomic
)
except OSError as e:
from click.exceptions import FileError

raise FileError(self.name, hint=e.strerror) from e
self._f = rv
return rv

def close(self) -> None:
"""Closes the underlying file, no matter what."""
if self._f is not None:
self._f.close()

def close_intelligently(self) -> None:
"""This function only closes the file if it was opened by the lazy
file wrapper. For instance this will never close stdin.
"""
if self.should_close:
self.close()

def __enter__(self) -> _LazyFile: # noqa: PYI034
return self

def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
tb: TracebackType | None,
) -> None:
self.close_intelligently()

def __iter__(self) -> Iterator[t.AnyStr]:
self.open()
return iter(self._f) # type: ignore


class CustomLazyFile(_LazyFile):
def __init__(
self,
filename: str | os.PathLike[str],
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/cli/test_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest

from check_jsonschema import main as cli_main
from check_jsonschema.cli.param_types import _LazyFile
from check_jsonschema.cli.parse_result import ParseResult, SchemaLoadingMode


Expand Down Expand Up @@ -81,7 +82,7 @@ def test_schemafile_and_instancefile(
assert mock_parse_result.schema_path == "schema.json"
assert isinstance(mock_parse_result.instancefiles, tuple)
for f in mock_parse_result.instancefiles:
assert isinstance(f, click.utils.LazyFile)
assert isinstance(f, _LazyFile)
assert tuple(f.name for f in mock_parse_result.instancefiles) == ("foo.json",)


Expand Down
Loading