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
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Unreleased
- A :class:`Group` with ``invoke_without_command=True`` marks its subcommand as
optional in the usage help, showing ``[COMMAND]`` instead of ``COMMAND``.
:issue:`3059` :pr:`3507`
- ``echo_via_pager`` flushes after each write, so passing a generator streams
output to the pager incrementally instead of staying hidden until the pipe
buffer fills. :issue:`3242` :issue:`2542` :pr:`3534`


Version 8.4.1
Expand Down
4 changes: 4 additions & 0 deletions src/click/termui.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,10 @@ def echo_via_pager(
with get_pager_file(color=color) as pager:
for text in itertools.chain(text_generator, "\n"):
pager.write(text)
# Flush after each write so a slow generator streams to the pager
# incrementally rather than staying invisible until the pipe buffer
# fills (~8 KB).
pager.flush()


@t.overload
Expand Down
38 changes: 38 additions & 0 deletions tests/test_termui.py
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,44 @@ def test_echo_via_pager_real_pager_handles_ansi(monkeypatch, capfd, color, expec
assert out == expected


def test_echo_via_pager_streams_each_write(monkeypatch):
"""Each write is flushed so a slow generator streams to the pager
incrementally instead of buffering until the end (issues #3242, #2542).
"""
calls = []

class RecordingStream(io.StringIO):
def __init__(self):
super().__init__()
self.color = None

def write(self, s):
calls.append("write")
return super().write(s)

def flush(self):
calls.append("flush")

stream = RecordingStream()
monkeypatch.setattr(click._termui_impl, "isatty", lambda _: False)
monkeypatch.setattr(click._termui_impl, "_default_text_stdout", lambda: stream)

def generate():
yield "a\n"
yield "b\n"
yield "c\n"

click.echo_via_pager(generate())

# No two writes are adjacent: every chunk is flushed before the next one,
# so the pager sees output as it is produced.
assert not any(
calls[i] == "write" and calls[i + 1] == "write" for i in range(len(calls) - 1)
)
assert calls.count("write") == 4 # three chunks plus the trailing newline
assert stream.getvalue() == "a\nb\nc\n\n"


def test_get_pager_file_pager_missing_binary_falls_back(monkeypatch, tmp_path):
"""``PAGER`` pointing to a nonexistent binary falls back to the text stdout."""
pager_out = tmp_path / "pager_out.txt"
Expand Down
Loading