diff --git a/CHANGES.rst b/CHANGES.rst index 0af01f647..129e1e2ca 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 diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py index 1d23026ed..6e68ac142 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: str) -> 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 649db6eda..a7508b972 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()