From ecb99836a44367bad62960045d586e31e4a95766 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 6 Jul 2023 09:42:06 -0700 Subject: [PATCH] echo doesn't fail with no streams --- CHANGES.rst | 2 ++ src/click/_compat.py | 11 ++++++++--- src/click/_termui_impl.py | 14 ++++++++++++++ src/click/utils.py | 5 +++++ tests/test_utils.py | 9 +++++++++ 5 files changed, 38 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index ce2bf8e80..639f2f7c5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -30,6 +30,8 @@ Unreleased bytes with the replacement character (``�``). :issue:`2395` - Remove unnecessary attempt to detect MSYS2 environment. :issue:`2355` - Remove outdated and unnecessary detection of App Engine environment. :pr:`2554` +- ``echo()`` does not fail when no streams are attached, such as with ``pythonw`` on + Windows. :issue:`2415` Version 8.1.3 diff --git a/src/click/_compat.py b/src/click/_compat.py index 17cef0e8b..9153d150c 100644 --- a/src/click/_compat.py +++ b/src/click/_compat.py @@ -576,12 +576,17 @@ def isatty(stream: t.IO[t.Any]) -> bool: def _make_cached_stream_func( - src_func: t.Callable[[], t.TextIO], wrapper_func: t.Callable[[], t.TextIO] -) -> t.Callable[[], t.TextIO]: + src_func: t.Callable[[], t.Optional[t.TextIO]], + wrapper_func: t.Callable[[], t.TextIO], +) -> t.Callable[[], t.Optional[t.TextIO]]: cache: t.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary() - def func() -> t.TextIO: + def func() -> t.Optional[t.TextIO]: stream = src_func() + + if stream is None: + return None + try: rv = cache.get(stream) except Exception: diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py index 38ff17134..f74465775 100644 --- a/src/click/_termui_impl.py +++ b/src/click/_termui_impl.py @@ -10,6 +10,7 @@ import time import typing as t from gettext import gettext as _ +from io import StringIO from types import TracebackType from ._compat import _default_text_stdout @@ -61,8 +62,15 @@ def __init__( self.show_pos = show_pos self.item_show_func = item_show_func self.label: str = label or "" + if file is None: file = _default_text_stdout() + + # There are no standard streams attached to write to. For example, + # pythonw on Windows. + if file is None: + file = StringIO() + self.file = file self.color = color self.update_min_steps = update_min_steps @@ -352,6 +360,12 @@ def generator(self) -> t.Iterator[V]: def pager(generator: t.Iterable[str], color: t.Optional[bool] = None) -> None: """Decide what method to use for paging through text.""" stdout = _default_text_stdout() + + # There are no standard streams attached to write to. For example, + # pythonw on Windows. + if stdout is None: + stdout = StringIO() + if not isatty(sys.stdin) or not isatty(stdout): return _nullpager(stdout, generator, color) pager_cmd = (os.environ.get("PAGER", None) or "").strip() diff --git a/src/click/utils.py b/src/click/utils.py index 3b5d286b6..d536434f0 100644 --- a/src/click/utils.py +++ b/src/click/utils.py @@ -267,6 +267,11 @@ def echo( else: file = _default_text_stdout() + # There are no standard streams attached to write to. For example, + # pythonw on Windows. + if file is None: + return + # Convert non bytes/text into the native string type. if message is not None and not isinstance(message, (str, bytes, bytearray)): out: t.Optional[t.Union[str, bytes]] = str(message) diff --git a/tests/test_utils.py b/tests/test_utils.py index 63668154b..12709a1d8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -43,6 +43,15 @@ def test_echo_custom_file(): assert f.getvalue() == "hello\n" +def test_echo_no_streams(monkeypatch, runner): + """echo should not fail when stdout and stderr are None with pythonw on Windows.""" + with runner.isolation(): + sys.stdout = None + sys.stderr = None + click.echo("test") + click.echo("test", err=True) + + @pytest.mark.parametrize( ("styles", "ref"), [