mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 07:31:38 +00:00 
			
		
		
		
	Debian (and derivatives) provide a /usr/bin/pager binary, managed by the alternatives system, that always points to an available pager utility. Allow _pyrepl to use it, to follow system policy. This is a very trivial change, from a patch that Debian has been carrying since 2.7 era. Seems appropriate to upstream. https://bugs.debian.org/799555
		
			
				
	
	
		
			175 lines
		
	
	
	
		
			5.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			175 lines
		
	
	
	
		
			5.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
from __future__ import annotations
 | 
						|
 | 
						|
import io
 | 
						|
import os
 | 
						|
import re
 | 
						|
import sys
 | 
						|
 | 
						|
 | 
						|
# types
 | 
						|
if False:
 | 
						|
    from typing import Protocol
 | 
						|
    class Pager(Protocol):
 | 
						|
        def __call__(self, text: str, title: str = "") -> None:
 | 
						|
            ...
 | 
						|
 | 
						|
 | 
						|
def get_pager() -> Pager:
 | 
						|
    """Decide what method to use for paging through text."""
 | 
						|
    if not hasattr(sys.stdin, "isatty"):
 | 
						|
        return plain_pager
 | 
						|
    if not hasattr(sys.stdout, "isatty"):
 | 
						|
        return plain_pager
 | 
						|
    if not sys.stdin.isatty() or not sys.stdout.isatty():
 | 
						|
        return plain_pager
 | 
						|
    if sys.platform == "emscripten":
 | 
						|
        return plain_pager
 | 
						|
    use_pager = os.environ.get('MANPAGER') or os.environ.get('PAGER')
 | 
						|
    if use_pager:
 | 
						|
        if sys.platform == 'win32': # pipes completely broken in Windows
 | 
						|
            return lambda text, title='': tempfile_pager(plain(text), use_pager)
 | 
						|
        elif os.environ.get('TERM') in ('dumb', 'emacs'):
 | 
						|
            return lambda text, title='': pipe_pager(plain(text), use_pager, title)
 | 
						|
        else:
 | 
						|
            return lambda text, title='': pipe_pager(text, use_pager, title)
 | 
						|
    if os.environ.get('TERM') in ('dumb', 'emacs'):
 | 
						|
        return plain_pager
 | 
						|
    if sys.platform == 'win32':
 | 
						|
        return lambda text, title='': tempfile_pager(plain(text), 'more <')
 | 
						|
    if hasattr(os, 'system') and os.system('(pager) 2>/dev/null') == 0:
 | 
						|
        return lambda text, title='': pipe_pager(text, 'pager', title)
 | 
						|
    if hasattr(os, 'system') and os.system('(less) 2>/dev/null') == 0:
 | 
						|
        return lambda text, title='': pipe_pager(text, 'less', title)
 | 
						|
 | 
						|
    import tempfile
 | 
						|
    (fd, filename) = tempfile.mkstemp()
 | 
						|
    os.close(fd)
 | 
						|
    try:
 | 
						|
        if hasattr(os, 'system') and os.system('more "%s"' % filename) == 0:
 | 
						|
            return lambda text, title='': pipe_pager(text, 'more', title)
 | 
						|
        else:
 | 
						|
            return tty_pager
 | 
						|
    finally:
 | 
						|
        os.unlink(filename)
 | 
						|
 | 
						|
 | 
						|
def escape_stdout(text: str) -> str:
 | 
						|
    # Escape non-encodable characters to avoid encoding errors later
 | 
						|
    encoding = getattr(sys.stdout, 'encoding', None) or 'utf-8'
 | 
						|
    return text.encode(encoding, 'backslashreplace').decode(encoding)
 | 
						|
 | 
						|
 | 
						|
def escape_less(s: str) -> str:
 | 
						|
    return re.sub(r'([?:.%\\])', r'\\\1', s)
 | 
						|
 | 
						|
 | 
						|
def plain(text: str) -> str:
 | 
						|
    """Remove boldface formatting from text."""
 | 
						|
    return re.sub('.\b', '', text)
 | 
						|
 | 
						|
 | 
						|
def tty_pager(text: str, title: str = '') -> None:
 | 
						|
    """Page through text on a text terminal."""
 | 
						|
    lines = plain(escape_stdout(text)).split('\n')
 | 
						|
    has_tty = False
 | 
						|
    try:
 | 
						|
        import tty
 | 
						|
        import termios
 | 
						|
        fd = sys.stdin.fileno()
 | 
						|
        old = termios.tcgetattr(fd)
 | 
						|
        tty.setcbreak(fd)
 | 
						|
        has_tty = True
 | 
						|
 | 
						|
        def getchar() -> str:
 | 
						|
            return sys.stdin.read(1)
 | 
						|
 | 
						|
    except (ImportError, AttributeError, io.UnsupportedOperation):
 | 
						|
        def getchar() -> str:
 | 
						|
            return sys.stdin.readline()[:-1][:1]
 | 
						|
 | 
						|
    try:
 | 
						|
        try:
 | 
						|
            h = int(os.environ.get('LINES', 0))
 | 
						|
        except ValueError:
 | 
						|
            h = 0
 | 
						|
        if h <= 1:
 | 
						|
            h = 25
 | 
						|
        r = inc = h - 1
 | 
						|
        sys.stdout.write('\n'.join(lines[:inc]) + '\n')
 | 
						|
        while lines[r:]:
 | 
						|
            sys.stdout.write('-- more --')
 | 
						|
            sys.stdout.flush()
 | 
						|
            c = getchar()
 | 
						|
 | 
						|
            if c in ('q', 'Q'):
 | 
						|
                sys.stdout.write('\r          \r')
 | 
						|
                break
 | 
						|
            elif c in ('\r', '\n'):
 | 
						|
                sys.stdout.write('\r          \r' + lines[r] + '\n')
 | 
						|
                r = r + 1
 | 
						|
                continue
 | 
						|
            if c in ('b', 'B', '\x1b'):
 | 
						|
                r = r - inc - inc
 | 
						|
                if r < 0: r = 0
 | 
						|
            sys.stdout.write('\n' + '\n'.join(lines[r:r+inc]) + '\n')
 | 
						|
            r = r + inc
 | 
						|
 | 
						|
    finally:
 | 
						|
        if has_tty:
 | 
						|
            termios.tcsetattr(fd, termios.TCSAFLUSH, old)
 | 
						|
 | 
						|
 | 
						|
def plain_pager(text: str, title: str = '') -> None:
 | 
						|
    """Simply print unformatted text.  This is the ultimate fallback."""
 | 
						|
    sys.stdout.write(plain(escape_stdout(text)))
 | 
						|
 | 
						|
 | 
						|
def pipe_pager(text: str, cmd: str, title: str = '') -> None:
 | 
						|
    """Page through text by feeding it to another program."""
 | 
						|
    import subprocess
 | 
						|
    env = os.environ.copy()
 | 
						|
    if title:
 | 
						|
        title += ' '
 | 
						|
    esc_title = escape_less(title)
 | 
						|
    prompt_string = (
 | 
						|
        f' {esc_title}' +
 | 
						|
        '?ltline %lt?L/%L.'
 | 
						|
        ':byte %bB?s/%s.'
 | 
						|
        '.'
 | 
						|
        '?e (END):?pB %pB\\%..'
 | 
						|
        ' (press h for help or q to quit)')
 | 
						|
    env['LESS'] = '-RmPm{0}$PM{0}$'.format(prompt_string)
 | 
						|
    proc = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
 | 
						|
                            errors='backslashreplace', env=env)
 | 
						|
    assert proc.stdin is not None
 | 
						|
    try:
 | 
						|
        with proc.stdin as pipe:
 | 
						|
            try:
 | 
						|
                pipe.write(text)
 | 
						|
            except KeyboardInterrupt:
 | 
						|
                # We've hereby abandoned whatever text hasn't been written,
 | 
						|
                # but the pager is still in control of the terminal.
 | 
						|
                pass
 | 
						|
    except OSError:
 | 
						|
        pass # Ignore broken pipes caused by quitting the pager program.
 | 
						|
    while True:
 | 
						|
        try:
 | 
						|
            proc.wait()
 | 
						|
            break
 | 
						|
        except KeyboardInterrupt:
 | 
						|
            # Ignore ctl-c like the pager itself does.  Otherwise the pager is
 | 
						|
            # left running and the terminal is in raw mode and unusable.
 | 
						|
            pass
 | 
						|
 | 
						|
 | 
						|
def tempfile_pager(text: str, cmd: str, title: str = '') -> None:
 | 
						|
    """Page through text by invoking a program on a temporary file."""
 | 
						|
    import tempfile
 | 
						|
    with tempfile.TemporaryDirectory() as tempdir:
 | 
						|
        filename = os.path.join(tempdir, 'pydoc.out')
 | 
						|
        with open(filename, 'w', errors='backslashreplace',
 | 
						|
                  encoding=os.device_encoding(0) if
 | 
						|
                  sys.platform == 'win32' else None
 | 
						|
                  ) as file:
 | 
						|
            file.write(text)
 | 
						|
        os.system(cmd + ' "' + filename + '"')
 |