diff --git a/CHANGES.md b/CHANGES.md index c808c22d1..39ae3fcf0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -32,6 +32,13 @@ Unreleased `flag_value` and `default` keep their unset sentinel at construction (resolved lazily on read) so `is UNSET` reliably tells a user-supplied value from an auto-derived one. Behavior is unchanged. {pr}`3641` +- {func}`get_binary_stream` and {func}`get_text_stream` are deprecated and + will be removed in Click 9.0. {issue}`3481` {pr}`3695` +- The following `click.utils` names were never intentionally public and are + now private (`_`-prefixed). The old names remain available with a + `DeprecationWarning` until Click 9.0: `LazyFile`, `KeepOpenFile`, + `make_default_short_help`, `PacifyFlushWrapper`, and `safecall`. + {issue}`3099` {pr}`3695` ## Version 8.4.2 diff --git a/docs/handling-files.md b/docs/handling-files.md index bdf0a4901..a77c99278 100644 --- a/docs/handling-files.md +++ b/docs/handling-files.md @@ -89,7 +89,7 @@ File open behavior can be controlled by the boolean kwarg `lazy`. If a file is o - A failure at first IO operation will happen by raising an {exc}`FileError`. - It can help minimize resource handling confusion. If a file is opened in lazy mode, it will call - {meth}`LazyFile.close_intelligently` to help figure out if the file needs closing or not. This is not needed for + {meth}`_LazyFile.close_intelligently` to help figure out if the file needs closing or not. This is not needed for parameters, but is necessary for manually prompting. For manual prompts with the {func}`prompt` function you do not know if a stream like stdout was opened (which was already open before) or a real file was opened (that needs closing). diff --git a/docs/utils.md b/docs/utils.md index ca3ddb389..6d7d9e0a3 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -259,33 +259,6 @@ Example: click.echo(f"Path: {click.format_filename(b'foo.txt')}") ``` -## Standard Streams - -For command line utilities, it's very important to get access to input and output streams reliably. Python generally -provides access to these streams through `sys.stdout` and friends, but unfortunately, there are API differences between -2.x and 3.x, especially with regards to how these streams respond to Unicode and binary data. - -Because of this, click provides the {func}`get_binary_stream` and {func}`get_text_stream` functions, which produce -consistent results with different Python versions and for a wide variety of terminal configurations. - -The end result is that these functions will always return a functional stream object (except in very odd cases; see -{doc}`/unicode-support`). - -Example: - -```python -import click - -stdin_text = click.get_text_stream('stdin') -stdout_binary = click.get_binary_stream('stdout') -``` - -```{versionadded} 6.0 -``` - -Click now emulates output streams on Windows to support unicode to the Windows console through separate APIs. For more -information see {doc}`wincmd`. - ## Intelligent File Opening ```{versionadded} 3.0 diff --git a/src/click/__init__.py b/src/click/__init__.py index 2d9918bce..ebc33ae2a 100644 --- a/src/click/__init__.py +++ b/src/click/__init__.py @@ -70,8 +70,6 @@ from .utils import echo as echo from .utils import format_filename as format_filename from .utils import get_app_dir as get_app_dir -from .utils import get_binary_stream as get_binary_stream -from .utils import get_text_stream as get_text_stream from .utils import open_file as open_file @@ -113,7 +111,6 @@ def __getattr__(name: str) -> object: if name == "__version__": import importlib.metadata - import warnings warnings.warn( "The '__version__' attribute is deprecated and will be removed in" @@ -124,4 +121,24 @@ def __getattr__(name: str) -> object: ) return importlib.metadata.version("click") + if name == "get_binary_stream": + from .utils import get_binary_stream + + warnings.warn( + "'get_binary_stream' is deprecated and will be removed in Click 9.0.", + DeprecationWarning, + stacklevel=2, + ) + return get_binary_stream + + if name == "get_text_stream": + from .utils import get_text_stream + + warnings.warn( + "'get_text_stream' is deprecated and will be removed in Click 9.0.", + DeprecationWarning, + stacklevel=2, + ) + return get_text_stream + raise AttributeError(name) diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py index fadae9406..03961a836 100644 --- a/src/click/_termui_impl.py +++ b/src/click/_termui_impl.py @@ -28,8 +28,8 @@ from ._compat import term_len from ._compat import WIN from .exceptions import ClickException +from .utils import _KeepOpenFile from .utils import echo -from .utils import KeepOpenFile V = t.TypeVar("V") @@ -641,7 +641,7 @@ def _nullpager( output stream in this case, since it's coming from elsewhere rather than our internal helpers. - The stream is wrapped in :class:`~click.utils.KeepOpenFile` so that, as a + The stream is wrapped in :class:`~click.utils._KeepOpenFile` so that, as a borrowed stream, it is not closed by a ``with`` block. The wrapper that :func:`get_pager_file` builds around it is detached rather than closed. """ @@ -650,7 +650,7 @@ def _nullpager( if color is None: color = False - yield KeepOpenFile(stream), encoding, color # type: ignore[misc] + yield _KeepOpenFile(stream), encoding, color # type: ignore[misc] class Editor: diff --git a/src/click/core.py b/src/click/core.py index b08f0437e..6f483675d 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -42,10 +42,10 @@ from .termui import style from .utils import _detect_program_name from .utils import _expand_args +from .utils import _make_default_short_help +from .utils import _PacifyFlushWrapper from .utils import echo -from .utils import make_default_short_help from .utils import make_str -from .utils import PacifyFlushWrapper if t.TYPE_CHECKING: from typing_extensions import Self @@ -1239,7 +1239,7 @@ def get_short_help_str(self, limit: int = 45) -> str: if self.short_help: text = inspect.cleandoc(self.short_help) elif self.help: - text = make_default_short_help(self.help, limit) + text = _make_default_short_help(self.help, limit) else: text = "" @@ -1563,8 +1563,8 @@ def main( sys.exit(e.exit_code) except OSError as e: if e.errno == errno.EPIPE: - sys.stdout = t.cast(t.TextIO, PacifyFlushWrapper(sys.stdout)) - sys.stderr = t.cast(t.TextIO, PacifyFlushWrapper(sys.stderr)) + sys.stdout = t.cast(t.TextIO, _PacifyFlushWrapper(sys.stdout)) + sys.stderr = t.cast(t.TextIO, _PacifyFlushWrapper(sys.stderr)) sys.exit(1) else: raise diff --git a/src/click/termui.py b/src/click/termui.py index 4bd74ed19..4dc799c62 100644 --- a/src/click/termui.py +++ b/src/click/termui.py @@ -20,8 +20,8 @@ from .types import Choice from .types import convert_type from .types import ParamType +from .utils import _LazyFile from .utils import echo -from .utils import LazyFile if t.TYPE_CHECKING: from ._termui_impl import ProgressBar @@ -123,7 +123,7 @@ def _build_prompt( def _format_default(default: t.Any) -> t.Any: - if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"): + if isinstance(default, (io.IOBase, _LazyFile)) and hasattr(default, "name"): return default.name return default diff --git a/src/click/types.py b/src/click/types.py index a27983b3a..c7eb9fd25 100644 --- a/src/click/types.py +++ b/src/click/types.py @@ -15,9 +15,9 @@ from ._compat import _get_argv_encoding from ._compat import open_stream from .exceptions import BadParameter +from .utils import _LazyFile +from .utils import _safecall from .utils import format_filename -from .utils import LazyFile -from .utils import safecall if t.TYPE_CHECKING: import typing_extensions as te @@ -936,7 +936,7 @@ def convert( lazy = self.resolve_lazy_flag(value) if lazy: - lf = LazyFile( + lf = _LazyFile( value, self.mode, self.encoding, self.errors, atomic=self.atomic ) @@ -956,9 +956,9 @@ def convert( # type is used with prompts. if ctx is not None: if should_close: - ctx.call_on_close(safecall(f.close)) + ctx.call_on_close(_safecall(f.close)) else: - ctx.call_on_close(safecall(f.flush)) + ctx.call_on_close(_safecall(f.flush)) return f except OSError as e: diff --git a/src/click/utils.py b/src/click/utils.py index 4d4d52ac3..81dd5f82d 100644 --- a/src/click/utils.py +++ b/src/click/utils.py @@ -33,8 +33,12 @@ def _posixify(name: str) -> str: return "-".join(name.split()).lower() -def safecall(func: t.Callable[P, R]) -> t.Callable[P, R | None]: - """Wraps a function so that it swallows exceptions.""" +def _safecall(func: t.Callable[P, R]) -> t.Callable[P, R | None]: + """ + Wraps a function so that it swallows exceptions. + + :meta private: + """ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None: try: @@ -56,7 +60,7 @@ def make_str(value: t.Any) -> str: return str(value) -def make_default_short_help(help: str, max_length: int = 45) -> str: +def _make_default_short_help(help: str, max_length: int = 45) -> str: """Returns a condensed version of help string. :meta private: @@ -109,11 +113,15 @@ def make_default_short_help(help: str, max_length: int = 45) -> str: return " ".join(words[:i]) + "..." -class LazyFile: - """A lazy file works like a regular file but it does not fully open +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 @@ -187,7 +195,7 @@ def close_intelligently(self) -> None: if self.should_close: self.close() - def __enter__(self) -> LazyFile: + def __enter__(self) -> _LazyFile: return self def __exit__( @@ -203,7 +211,7 @@ def __iter__(self) -> cabc.Iterator[t.AnyStr]: return iter(self._f) # type: ignore -class KeepOpenFile: +class _KeepOpenFile: """Proxy a file object but keep it open across a ``with`` block. Wraps a borrowed file (such as ``sys.stdin`` or ``sys.stdout``) so that @@ -214,6 +222,8 @@ class KeepOpenFile: Dunder methods are proxied explicitly: implicit special-method lookups bypass :meth:`__getattr__`, because Python resolves them on the type rather than the instance. + + :meta private: """ _file: t.IO[t.Any] @@ -224,7 +234,7 @@ def __init__(self, file: t.IO[t.Any]) -> None: def __getattr__(self, name: str) -> t.Any: return getattr(self._file, name) - def __enter__(self) -> KeepOpenFile: + def __enter__(self) -> _KeepOpenFile: return self def __exit__( @@ -342,8 +352,13 @@ def echo( def get_binary_stream(name: t.Literal["stdin", "stdout", "stderr"]) -> t.BinaryIO: """Returns a system stream for byte processing. + .. deprecated:: 8.5 + Will be removed in Click 9.0. + :param name: the name of the stream to open. Valid names are ``'stdin'``, ``'stdout'`` and ``'stderr'`` + + :meta private: """ opener = binary_streams.get(name) if opener is None: @@ -356,11 +371,16 @@ def get_text_stream( encoding: str | None = None, errors: str | None = "strict", ) -> t.TextIO: - """Returns a system stream for text processing. This usually returns - a wrapped stream around a binary stream returned from + """Returns a system stream for text processing. + + .. deprecated:: 8.5 + Will be removed in Click 9.0. + + This usually returns a wrapped stream around a binary stream returned from :func:`get_binary_stream` but it also can take shortcuts for already correctly configured streams. + :meta private: :param name: the name of the stream to open. Valid names are ``'stdin'``, ``'stdout'`` and ``'stderr'`` :param encoding: overrides the detected default encoding. @@ -410,13 +430,13 @@ def open_file( """ if lazy: return t.cast( - "t.IO[t.Any]", LazyFile(filename, mode, encoding, errors, atomic=atomic) + "t.IO[t.Any]", _LazyFile(filename, mode, encoding, errors, atomic=atomic) ) f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic) if not should_close: - f = t.cast("t.IO[t.Any]", KeepOpenFile(f)) + f = t.cast("t.IO[t.Any]", _KeepOpenFile(f)) return f @@ -512,13 +532,15 @@ def get_app_dir(app_name: str, roaming: bool = True, force_posix: bool = False) ) -class PacifyFlushWrapper: +class _PacifyFlushWrapper: """This wrapper is used to catch and suppress BrokenPipeErrors resulting from ``.flush()`` being called on broken pipe during the shutdown/final-GC of the Python interpreter. Notably ``.flush()`` is always called on ``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any other cleanup code, and the case where the underlying file is not a broken pipe, all calls and attributes are proxied. + + :meta private: """ wrapped: t.IO[t.Any] @@ -644,3 +666,24 @@ def _expand_args( out.extend(matches) return out + + +def __getattr__(name: str) -> object: + import warnings + + if name in { + "LazyFile", + "KeepOpenFile", + "make_default_short_help", + "PacifyFlushWrapper", + "safecall", + }: + warnings.warn( + f"'click.utils.{name}' was never intentionally public and will be" + " renamed in Click 9.0.", + DeprecationWarning, + stacklevel=2, + ) + return globals()[f"_{name}"] + + raise AttributeError(name) diff --git a/tests/test_testing.py b/tests/test_testing.py index a7508b972..6fae8cb6f 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -10,13 +10,14 @@ import click from click.exceptions import ClickException from click.testing import CliRunner +from click.utils import get_binary_stream def test_runner(): @click.command() def test(): - i = click.get_binary_stream("stdin") - o = click.get_binary_stream("stdout") + i = get_binary_stream("stdin") + o = get_binary_stream("stdout") while True: chunk = i.read(4096) if not chunk: @@ -33,8 +34,8 @@ def test(): def test_echo_stdin_stream(): @click.command() def test(): - i = click.get_binary_stream("stdin") - o = click.get_binary_stream("stdout") + i = get_binary_stream("stdin") + o = get_binary_stream("stdout") while True: chunk = i.read(4096) if not chunk: @@ -91,8 +92,8 @@ def test_multiple_prompts(foo, bar): def test_runner_with_stream(): @click.command() def test(): - i = click.get_binary_stream("stdin") - o = click.get_binary_stream("stdout") + i = get_binary_stream("stdin") + o = get_binary_stream("stdout") while True: chunk = i.read(4096) if not chunk: diff --git a/tests/test_utils/test_KeepOpenFile.py b/tests/test_utils/test_KeepOpenFile.py index 26bfe454c..0ba2cab28 100644 --- a/tests/test_utils/test_KeepOpenFile.py +++ b/tests/test_utils/test_KeepOpenFile.py @@ -6,5 +6,5 @@ def test_iter_keepopenfile(tmpdir): p = tmpdir.mkdir("testdir").join("testfile") p.write("\n".join(expected)) with p.open() as f: - for e_line, a_line in zip(expected, click.utils.KeepOpenFile(f), strict=False): + for e_line, a_line in zip(expected, click.utils._KeepOpenFile(f), strict=False): assert e_line == a_line.strip() diff --git a/tests/test_utils/test_LazyFile.py b/tests/test_utils/test_LazyFile.py index e3dd09b89..8d0775aa8 100644 --- a/tests/test_utils/test_LazyFile.py +++ b/tests/test_utils/test_LazyFile.py @@ -6,6 +6,6 @@ def test_iter_lazyfile(tmpdir): p = tmpdir.mkdir("testdir").join("testfile") p.write("\n".join(expected)) with p.open() as f: - with click.utils.LazyFile(f.name) as lf: + with click.utils._LazyFile(f.name) as lf: for e_line, a_line in zip(expected, lf, strict=False): assert e_line == a_line.strip() diff --git a/tests/test_utils/test_make_default_short_help.py b/tests/test_utils/test_make_default_short_help.py index cb9886cbb..c2a1f8ef0 100644 --- a/tests/test_utils/test_make_default_short_help.py +++ b/tests/test_utils/test_make_default_short_help.py @@ -35,5 +35,5 @@ def test_make_default_short_help(value, max_length, alter, expect): if alter: value = alter(value) - out = click.utils.make_default_short_help(value, max_length) + out = click.utils._make_default_short_help(value, max_length) assert out == expect