From 0875c93c856c84162e8bad21776e9d34a028f90c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= Date: Tue, 8 Sep 2026 22:39:24 -0600 Subject: [PATCH] Vendor `click.utils._Lazy` to fix deprecation warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Edgar Ramírez Mondragón --- CHANGELOG.rst | 2 + src/check_jsonschema/cli/main_command.py | 3 + src/check_jsonschema/cli/param_types.py | 100 ++++++++++++++++++++++- tests/unit/cli/test_parse.py | 3 +- 4 files changed, 106 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9d030bcd9..fc8a6ca9c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 ------ diff --git a/src/check_jsonschema/cli/main_command.py b/src/check_jsonschema/cli/main_command.py index 62d79bb35..672502c75 100644 --- a/src/check_jsonschema/cli/main_command.py +++ b/src/check_jsonschema/cli/main_command.py @@ -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." @@ -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", diff --git a/src/check_jsonschema/cli/param_types.py b/src/check_jsonschema/cli/param_types.py index 66bce92de..29cb78482 100644 --- a/src/check_jsonschema/cli/param_types.py +++ b/src/check_jsonschema/cli/param_types.py @@ -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 @@ -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"" + + 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], diff --git a/tests/unit/cli/test_parse.py b/tests/unit/cli/test_parse.py index e7846220c..11bfea806 100644 --- a/tests/unit/cli/test_parse.py +++ b/tests/unit/cli/test_parse.py @@ -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 @@ -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",)