From f57d4b58668b7de0572c5197ba25c6c42e907032 Mon Sep 17 00:00:00 2001 From: Chris Pappalardo Date: Tue, 19 May 2026 12:37:31 -0700 Subject: [PATCH 1/3] add maybe strip ansi thin to handle stdout and stderr situations --- src/click/_termui_impl.py | 30 ++++++++++++++++++++++++++---- tests/test_testing.py | 11 +++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py index 1d23026ed4..28faa5eb21 100644 --- a/src/click/_termui_impl.py +++ b/src/click/_termui_impl.py @@ -385,6 +385,24 @@ def generator(self) -> cabc.Iterator[V]: self.render_progress() +class _MaybeStripAnsiThin: + def __init__(self, stream: t.TextIO, color: bool): + self.color = color + self.stream = stream + + def write(self, text: str) -> int: + if not self.color: + text = strip_ansi(text) + return self.stream.write(text) + + def flush(self) -> None: + return self.stream.flush() + + def __getattr__(self, name) -> t.Any: + # Forward other attributes (e.g., fileno, isatty) + return getattr(self.stream, name) + + class MaybeStripAnsi(io.TextIOWrapper): def __init__(self, stream: t.IO[bytes], *, color: bool, **kwargs: t.Any): super().__init__(stream, **kwargs) @@ -436,10 +454,14 @@ def get_pager_file(color: bool | None = None) -> t.Generator[t.TextIO, None, Non default is autodetection. """ with _pager_contextmanager(color=color) as (stream, encoding, color): - # Split streams by capabilities rather than the abstract TextIO / - # BinaryIO annotations: buffered text streams can be unwrapped to bytes, - # while text-only streams are yielded as-is. - if _has_binary_buffer(stream): + # route stdout and stderr through a thinner wrapper + is_std = stream is sys.stdout or stream is sys.stderr + if not isinstance(stream, t.BinaryIO) and is_std: + stream = _MaybeStripAnsiThin(stream, color=color) + # otherwise, split streams by capabilities rather than the abstract + # TextIO / BinaryIO annotations: buffered text streams can be unwrapped + # to bytes, while text-only streams are yielded as-is. + elif _has_binary_buffer(stream): # Text stream backed by a binary buffer. stream = MaybeStripAnsi(stream.buffer, color=color, encoding=encoding) elif isinstance(stream, t.BinaryIO): diff --git a/tests/test_testing.py b/tests/test_testing.py index 649db6eda7..a7508b9724 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -256,6 +256,17 @@ def cli(): assert result.output == "" +def test_with_echo_via_pager(): + @click.command() + def cli(): + click.echo_via_pager("Hello, Click!") + + runner = CliRunner() + result = runner.invoke(cli) + assert not result.exception + assert result.output == "Hello, Click!\n" + + def test_exit_code_and_output_from_sys_exit(): # See issue #362 @click.command() From 6c6316e50f808bf1fd8e906015eb2b589bd75309 Mon Sep 17 00:00:00 2001 From: Chris Pappalardo Date: Tue, 19 May 2026 12:45:14 -0700 Subject: [PATCH 2/3] update changes --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index 0af01f6476..129e1e2ca0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,7 @@ Unreleased - Zsh completion scripts parse correctly on Windows. :issue:`3277` - Shell completion of `Choice` `Enum` values produces a valid completion result. :issue:`3015` +- Fix I/O operation on closed file error with `echo_via_pager`. :issue:`3449` Version 8.4.0 From 2cf8e0143878ccf055547f463b32459a502bfaac Mon Sep 17 00:00:00 2001 From: Chris Pappalardo Date: Tue, 19 May 2026 12:47:28 -0700 Subject: [PATCH 3/3] fix linting --- src/click/_termui_impl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py index 28faa5eb21..6e68ac142c 100644 --- a/src/click/_termui_impl.py +++ b/src/click/_termui_impl.py @@ -398,7 +398,7 @@ def write(self, text: str) -> int: def flush(self) -> None: return self.stream.flush() - def __getattr__(self, name) -> t.Any: + def __getattr__(self, name: str) -> t.Any: # Forward other attributes (e.g., fileno, isatty) return getattr(self.stream, name)