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
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 26 additions & 4 deletions src/click/_termui_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
11 changes: 11 additions & 0 deletions tests/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading