Skip to content
Open
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
7 changes: 7 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/handling-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you can rephrase this to eliminate the reference to the now private _LazyFile altogether.

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).
Expand Down
27 changes: 0 additions & 27 deletions docs/utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 20 additions & 3 deletions src/click/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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"
Expand All @@ -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.",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This warning is not triggered if someone import get_binary_stream from click.utils.get_binary_stream.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can test it with something like:

@pytest.mark.parametrize("module", [click, click.utils], ids=["click", "click.utils"])
@pytest.mark.parametrize("name", ["get_binary_stream", "get_text_stream"])
def test_stream_helper_deprecated(module, name):
    with pytest.warns(DeprecationWarning, match=name):
        func = getattr(module, name)

You should add that to the test suite BTW.

And maybe extend it to the other functions you are deprecating.

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)
6 changes: 3 additions & 3 deletions src/click/_termui_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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.
"""
Expand All @@ -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:
Expand Down
10 changes: 5 additions & 5 deletions src/click/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = ""

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/click/termui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions src/click/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)

Expand All @@ -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:
Expand Down
69 changes: 56 additions & 13 deletions src/click/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nitpick: first line should be put right after the triple-quote. Like so:

    """Wraps a function so that it swallows exceptions.

    :meta private:
    """


:meta private:
"""

def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None:
try:
Expand All @@ -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:
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: should be move right after the """.

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
Expand Down Expand Up @@ -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__(
Expand All @@ -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
Expand All @@ -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]
Expand All @@ -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__(
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace .. deprecated:: 8.5 by .. deprecated:: 8.5.0.

A nitpick I also proposed to fix in #3697.

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:
Expand All @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace .. deprecated:: 8.5 by .. deprecated:: 8.5.0.

A nitpick I also proposed to fix in #3697.

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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we move that directive at the end of the docstring for consistency like the others?

:param name: the name of the stream to open. Valid names are ``'stdin'``,
``'stdout'`` and ``'stderr'``
:param encoding: overrides the detected default encoding.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deprecation warning is too verbose, just align it to the other: XXX is deprecated and will be removed in Click 9.0. is good enough! :)

" renamed in Click 9.0.",
DeprecationWarning,
stacklevel=2,
)
return globals()[f"_{name}"]

raise AttributeError(name)
Loading