Skip to content

Commit

Permalink
Streaming pager: initial commit. Fixes pallets#409
Browse files Browse the repository at this point in the history
The streaming pager works using generators.

Just pass a generator function or object into echo_via_pager:

generator expression:
```
click.echo_via_pager(("{}\n".format(i) for i in range(999999)))
```

generator function / expression:
```
def gen():
    counter = 0
    while True:
        counter++
        yield counter
click.echo_via_pager(gen)
click.echo_via_pager(gen()) # you can pass both
```
  • Loading branch information
stefreak committed Nov 12, 2017
1 parent b471d34 commit 000b0b8
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 7 deletions.
34 changes: 29 additions & 5 deletions click/_termui_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import sys
import time
import math
import inspect
from ._compat import _default_text_stdout, range_type, PY2, isatty, \
open_stream, strip_ansi, term_len, get_best_encoding, WIN, int_types, \
CYGWIN
Expand Down Expand Up @@ -300,6 +301,25 @@ def pager(text, color=None):
os.unlink(filename)


def _pipe_make_gen(text):
"""Converts string or generator of strings into generator"""
if inspect.isgeneratorfunction(text):
for chunk in text(): yield chunk
elif inspect.isgenerator(text):
for chunk in text: yield chunk
else:
yield text


def _pipe_make_str(text):
"""Converts string or generator of strings into string"""
if inspect.isgeneratorfunction(text):
return "".join(text())
if inspect.isgenerator(text):
return "".join(text)
return text


def _pipepager(text, cmd, color):
"""Page through text by feeding it to another program. Invoking a
pager through this might support colors.
Expand All @@ -318,17 +338,19 @@ def _pipepager(text, cmd, color):
elif 'r' in less_flags or 'R' in less_flags:
color = True

if not color:
text = strip_ansi(text)

c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
env=env)
encoding = get_best_encoding(c.stdin)
try:
c.stdin.write(text.encode(encoding, 'replace'))
c.stdin.close()
for chunk in _pipe_make_gen(text):
if not color:
chunk = strip_ansi(chunk)

c.stdin.write(chunk.encode(encoding, 'replace'))
except (IOError, KeyboardInterrupt):
pass
else:
c.stdin.close()

# Less doesn't respect ^C, but catches it for its own UI purposes (aborting
# search or other commands inside less).
Expand All @@ -351,6 +373,7 @@ def _tempfilepager(text, cmd, color):
"""Page through text by invoking a program on a temporary file."""
import tempfile
filename = tempfile.mktemp()
text = _pipe_make_str(text)
if not color:
text = strip_ansi(text)
encoding = get_best_encoding(sys.stdout)
Expand All @@ -364,6 +387,7 @@ def _tempfilepager(text, cmd, color):

def _nullpager(stream, text, color):
"""Simply print unformatted text. This is the ultimate fallback."""
text = _pipe_make_str(text)
if not color:
text = strip_ansi(text)
stream.write(text)
Expand Down
7 changes: 5 additions & 2 deletions click/termui.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import sys
import struct
import inspect

from ._compat import raw_input, text_type, string_types, \
isatty, strip_ansi, get_winterm_size, DEFAULT_COLUMNS, WIN
Expand Down Expand Up @@ -215,10 +216,12 @@ def echo_via_pager(text, color=None):
default is autodetection.
"""
color = resolve_color_default(color)
if not isinstance(text, string_types):
if not inspect.isgenerator(text) \
and not inspect.isgeneratorfunction(text) \
and not isinstance(text, string_types):
text = text_type(text)
from ._termui_impl import pager
return pager(text + '\n', color)
return pager(text, color)


def progressbar(iterable=None, length=None, label=None, show_eta=True,
Expand Down

0 comments on commit 000b0b8

Please sign in to comment.