Skip to content

Commit

Permalink
Add file-like pager: click.get_pager_file()
Browse files Browse the repository at this point in the history
  • Loading branch information
craigds committed Jun 5, 2020
1 parent 9de62f3 commit 1b378dd
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 11 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Unreleased
parameter. :issue:`1264`, :pr:`1329`
- Add an optional parameter to ``ProgressBar.update`` to set the
``current_item``. :issue:`1226`, :pr:`1332`
- Add ``click.get_pager_file`` for file-like access to an output
pager. :pr:`1572`


Version 7.1.2
Expand Down
2 changes: 2 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ Utilities

.. autofunction:: echo_via_pager

.. autofunction:: get_pager_file

.. autofunction:: prompt

.. autofunction:: confirm
Expand Down
12 changes: 12 additions & 0 deletions docs/utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,18 @@ If you want to use the pager for a lot of text, especially if generating everyth
click.echo_via_pager(_generate_output())


For more complex programs, which can't easily use a simple generator, you
can get access to a writable file-like object for the pager, and write to
that instead:

.. click:example::
@click.command()
def less():
with click.get_pager_file() as pager:
for idx in range(50000):
print(idx, file=pager)


Screen Clearing
---------------

Expand Down
1 change: 1 addition & 0 deletions src/click/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from .termui import confirm
from .termui import echo_via_pager
from .termui import edit
from .termui import get_pager_file
from .termui import get_terminal_size
from .termui import getchar
from .termui import launch
Expand Down
37 changes: 30 additions & 7 deletions src/click/_termui_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
placed in this module and only imported as needed.
"""
import contextlib
import io
import math
import os
import sys
Expand Down Expand Up @@ -328,6 +329,24 @@ def generator(self):
self.render_progress()


class StripAnsi(io.TextIOWrapper):
@classmethod
def maybe(cls, stream, *, color, encoding):
if not getattr(stream, "encoding", None):
if color:
# just wrap the byte stream in a text stream
stream = io.TextIOWrapper(stream, encoding=encoding)
else:
# wrap in a text stream *and* strip ansi chars
stream = cls(stream, encoding=encoding)
# stream is already a text stream, can't do anything.
return stream

def write(self, text):
text = strip_ansi(text)
return super().write(text)


def _pager_contextmanager(color=None):
"""Decide what method to use for paging through text."""
stdout = _default_text_stdout()
Expand Down Expand Up @@ -357,13 +376,17 @@ def _pager_contextmanager(color=None):
os.unlink(filename)


def pager(generator, color=None):
"""Given an iterable of text, write it all to an output pager."""
with _pager_contextmanager(color=color) as (pager_file, encoding, color):
for text in generator:
if not color:
text = strip_ansi(text)
pager_file.write(text.encode(encoding, "replace"))
@contextlib.contextmanager
def get_pager_file(color=None):
"""Context manager.
Yields a writable file-like object which can be used as an output pager.
.. versionadded:: 8.0
:param color: controls if the pager supports ANSI colors or not. The
default is autodetection.
"""
with _pager_contextmanager(color=color) as (stream, encoding, color):
with StripAnsi.maybe(stream, color=color, encoding=encoding) as text_stream:
yield text_stream


@contextlib.contextmanager
Expand Down
24 changes: 20 additions & 4 deletions src/click/termui.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,23 @@ def ioctl_gwinsz(fd):
return int(cr[1]), int(cr[0])


def get_pager_file(color=None):
"""Context manager.
Yields a writable file-like object which can be used as an output pager.
.. versionadded:: 8.0
:param color: controls if the pager supports ANSI colors or not. The
default is autodetection.
"""
from ._termui_impl import get_pager_file

color = resolve_color_default(color)

return get_pager_file(color=color)


def echo_via_pager(text_or_generator, color=None):
"""This function takes a text and shows it via an environment specific
pager on stdout.
Expand All @@ -267,7 +284,6 @@ def echo_via_pager(text_or_generator, color=None):
:param color: controls if the pager supports ANSI colors or not. The
default is autodetection.
"""
color = resolve_color_default(color)

if inspect.isgeneratorfunction(text_or_generator):
i = text_or_generator()
Expand All @@ -279,9 +295,9 @@ def echo_via_pager(text_or_generator, color=None):
# convert every element of i to a text type if necessary
text_generator = (el if isinstance(el, str) else str(el) for el in i)

from ._termui_impl import pager

return pager(itertools.chain(text_generator, "\n"), color)
with get_pager_file(color=color) as pager:
for text in itertools.chain(text_generator, "\n"):
pager.write(text)


def progressbar(
Expand Down

0 comments on commit 1b378dd

Please sign in to comment.