Skip to content

Commit

Permalink
Refactored pager support to not rely on pydoc
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko committed May 26, 2014
1 parent c5d53f8 commit aef1650
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 16 deletions.
23 changes: 17 additions & 6 deletions click/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
PY2 = sys.version_info[0] == 2


_ansi_re = re.compile('\033\[((?:\d|;)*)([a-zA-Z])')


def _make_text_stream(stream, encoding, errors):
if encoding is None:
encoding = get_best_encoding(stream)
Expand Down Expand Up @@ -427,7 +430,7 @@ def __repr__(self):
return repr(self._f)


_auto_wrap_for_ansi = lambda x: x
auto_wrap_for_ansi = None


try:
Expand All @@ -438,14 +441,11 @@ def __repr__(self):
from weakref import WeakKeyDictionary
_ansi_stream_wrappers = WeakKeyDictionary()

def _auto_wrap_for_ansi(stream):
def auto_wrap_for_ansi(stream):
cached = _ansi_stream_wrappers.get(stream)
if cached is not None:
return cached
try:
strip = not stream.isatty()
except Exception:
strip = True
strip = not isatty(stream)
rv = colorama.AnsiToWin32(stream, strip=strip).stream
try:
_ansi_stream_wrappers[stream] = rv
Expand All @@ -454,6 +454,17 @@ def _auto_wrap_for_ansi(stream):
return rv


def strip_ansi(value):
return _ansi_re.sub('', value)


def isatty(stream):
try:
return stream.isatty()
except Exception:
return False


binary_streams = {
'stdin': get_binary_stdin,
'stdout': get_binary_stdout,
Expand Down
61 changes: 60 additions & 1 deletion click/_termui_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
:license: BSD, see LICENSE for more details.
"""
import os
import sys
import time
import math
from ._compat import get_text_stdout, range_type, PY2
from ._compat import get_text_stdout, range_type, PY2, isatty, open_stream, \
strip_ansi
from .utils import echo


Expand Down Expand Up @@ -224,3 +226,60 @@ def next(self):
if not PY2:
__next__ = next
del next


def pager(text):
"""Decide what method to use for paging through text."""
stdout = get_text_stdout()
if not isatty(sys.stdin) or not isatty(stdout):
return _nullpager(stdout, text)
if 'PAGER' in os.environ:
if sys.platform == 'win32':
return _tempfilepager(strip_ansi(text), os.environ['PAGER'])
elif os.environ.get('TERM') in ('dumb', 'emacs'):
return _pipepager(strip_ansi(text), os.environ['PAGER'])
else:
return _pipepager(text, os.environ['PAGER'])
if os.environ.get('TERM') in ('dumb', 'emacs'):
return _nullpager(stdout, text)
if sys.platform == 'win32' or sys.platform.startswith('os2'):
return _tempfilepager(strip_ansi(text), 'more <')
if hasattr(os, 'system') and os.system('(less) 2>/dev/null') == 0:
return _pipepager(text, 'less')

import tempfile
fd, filename = tempfile.mkstemp()
os.close(fd)
try:
if hasattr(os, 'system') and os.system('more "%s"' % filename) == 0:
return _pipepager(text, 'more')
return _nullpager(stdout, text)
finally:
os.unlink(filename)


def _pipepager(text, cmd):
"""Page through text by feeding it to another program."""
pipe = os.popen(cmd, 'w')
try:
pipe.write(text)
pipe.close()
except IOError:
pass


def _tempfilepager(text, cmd):
"""Page through text by invoking a program on a temporary file."""
import tempfile
filename = tempfile.mktemp()
with open_stream(filename, 'w')[1] as f:
f.write(text)
try:
os.system(cmd + ' "' + filename + '"')
finally:
os.unlink(filename)


def _nullpager(stream, text):
"""Simply print unformatted text. This is the ultimate fallback."""
stream.write(strip_ansi(text))
8 changes: 2 additions & 6 deletions click/termui.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,8 @@ def echo_via_pager(text):
encoding = get_best_encoding(sys.stdout)
text = text.encode(encoding, 'replace')

# Pydoc's pager is badly broken with LANG=C on Python 3 to the point
# where it will corrupt the terminal. http://bugs.python.org/issue21398
# I don't feel like reimplementing it given that it works on Python 2
# and seems reasonably stable otherwise.
import pydoc
pydoc.pager(text)
from ._termui_impl import pager
return pager(text + '\n')


def progressbar(iterable=None, length=None, label=None, show_eta=True,
Expand Down
13 changes: 11 additions & 2 deletions click/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from ._compat import text_type, open_stream, get_streerror, string_types, \
PY2, get_best_encoding, binary_streams, text_streams, filename_to_ui, \
_auto_wrap_for_ansi
auto_wrap_for_ansi, strip_ansi, isatty

if not PY2:
from ._compat import _find_binary_writer
Expand Down Expand Up @@ -223,7 +223,6 @@ def echo(message=None, file=None, nl=True):
"""
if file is None:
file = sys.stdout
file = _auto_wrap_for_ansi(file)

if message is not None and not isinstance(message, echo_native_types):
message = text_type(message)
Expand All @@ -242,6 +241,16 @@ def echo(message=None, file=None, nl=True):
binary_file.write(b'\n')
binary_file.flush()
return

# If we have colorama support we wrap the stream to handle colors
# for us. In case colorama is not supported and our output stream
# is not a terminal, we strip the ansi codes ourselves.
if auto_wrap_for_ansi is not None:
file = auto_wrap_for_ansi(file)
elif message and not isatty(file):
message = strip_ansi(message)

if message:
file.write(message)
if nl:
file.write('\n')
Expand Down
3 changes: 2 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ def test_echo(runner):
click.echo(b'\x44\x44')
click.echo(42, nl=False)
click.echo(b'a', nl=False)
click.echo('\x1b[31mx\x1b[39m', nl=False)
bytes = out.getvalue()
assert bytes == b'\xe2\x98\x83\nDD\n42a'
assert bytes == b'\xe2\x98\x83\nDD\n42ax'


def test_filename_formatting():
Expand Down

0 comments on commit aef1650

Please sign in to comment.