Merge pull request #373 from willmcgugan/pager

pager functionality
This commit is contained in:
Will McGugan 2020-10-11 15:43:39 +01:00 committed by GitHub
commit b49faf271a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 197 additions and 13 deletions

View File

@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added ascii_only to ConsoleOptions
- Addded box.SQUARE_DOUBLE_HEAD
- Added highlighting of EUI-48 and EUI-64 (MAC addresses)
- Added Console.pager
### Changed

View File

@ -210,6 +210,25 @@ An alternative way of capturing output is to set the Console file to a :py:class
console.print("[bold red]Hello[/] World")
str_output = console.file.getvalue()
Paging
------
If you have some long output to present to the user you can use a *pager* to display it. A pager is typically an application on by your operating system which will at least support pressing a key to scroll, but will often support scrolling up and down through the text and other features.
You can page output from a Console by calling :meth:`~rich.console.Console.pager` which returns a context manger. When the pager exits, anything that was printed will be sent to the pager. Here's an example::
from rich.__main__ import make_test_card
from rich.console import Console
console = Console()
with console.pager():
console.print(make_test_card())
Since the default pager on most platforms don't support color, Rich will strip color from the output. If you know that your pager supports color, you can set ``style=True`` when calling the :meth:`~rich.console.Console.pager` method.
.. note::
Rich will use the ``PAGER`` environment variable to get the pager command. On Linux and macOS you can set this to ``less -r`` to enable paging with ANSI styles.
Terminal detection
------------------

View File

@ -35,6 +35,7 @@ from .control import Control
from .highlighter import NullHighlighter, ReprHighlighter
from .markup import render as render_markup
from .measure import Measurement, measure_renderables
from .pager import Pager, SystemPager
from .pretty import Pretty
from .scope import render_scope
from .segment import Segment
@ -205,6 +206,40 @@ class ThemeContext:
self.console.pop_theme()
class PagerContext:
"""A context manager that 'pages' content. See :meth:`~rich.console.Console.pager` for usage."""
def __init__(
self,
console: "Console",
pager: Pager = None,
styles: bool = False,
links: bool = False,
) -> None:
self._console = console
self.pager = SystemPager() if pager is None else pager
self.styles = styles
self.links = links
def __enter__(self) -> "PagerContext":
self._console._enter_buffer()
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
if exc_type is None:
with self._console._lock:
buffer: List[Segment] = self._console._buffer[:]
del self._console._buffer[:]
segments: Iterable[Segment] = buffer
if not self.styles:
segments = Segment.strip_styles(segments)
elif not self.links:
segments = Segment.strip_links(segments)
content = self._console._render_buffer(segments)
self.pager.show(content)
self._console._exit_buffer()
class RenderGroup:
"""Takes a group of renderables and returns a renderable object that renders the group.
@ -523,7 +558,7 @@ class Console:
Returns:
str: Console output.
"""
render_result = self._render_buffer()
render_result = self._render_buffer(self._buffer)
del self._buffer[:]
self._exit_buffer()
return render_result
@ -667,6 +702,29 @@ class Console:
capture = Capture(self)
return capture
def pager(
self, pager: Pager = None, styles: bool = False, links: bool = False
) -> PagerContext:
"""A context manager to display anything printed within a "pager". The pager used
is defined by the system and will typically support at less pressing a key to scroll.
Args:
pager (Pager, optional): A pager object, or None to use :class:~rich.pager.SystemPager`. Defaults to None.
styles (bool, optional): Show styles in pager. Defaults to False.
links (bool, optional): Show links in pager. Defaults to False.
Example:
>>> from rich.console import Console
>>> from rich.__main__ import make_test_card
>>> console = Console()
>>> with console.pager():
console.print(make_test_card())
Returns:
PagerContext: A context manager.
"""
return PagerContext(self, pager=pager, styles=styles, links=links)
def line(self, count: int = 1) -> None:
"""Write new line(s).
@ -1154,7 +1212,8 @@ class Console:
display(self._buffer)
del self._buffer[:]
else:
text = self._render_buffer()
text = self._render_buffer(self._buffer[:])
del self._buffer[:]
if text:
try:
if WINDOWS: # pragma: no cover
@ -1169,17 +1228,15 @@ class Console:
error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***"
raise
def _render_buffer(self) -> str:
def _render_buffer(self, buffer: Iterable[Segment]) -> str:
"""Render buffered output, and clear buffer."""
output: List[str] = []
append = output.append
color_system = self._color_system
legacy_windows = self.legacy_windows
buffer = self._buffer[:]
if self.record:
with self._record_buffer_lock:
self._record_buffer.extend(buffer)
del self._buffer[:]
not_terminal = not self.is_terminal
for text, style, is_control in buffer:
if style and not is_control:

33
rich/pager.py Normal file
View File

@ -0,0 +1,33 @@
from abc import ABC, abstractmethod
import pydoc
class Pager(ABC):
"""Base class for a pager."""
@abstractmethod
def show(self, content: str) -> None:
"""Show content in pager.
Args:
content (str): Content to be displayed.
"""
class SystemPager(Pager):
"""Uses the pager installed on the system."""
_pager = lambda self, content: pydoc.pager(content)
def show(self, content: str) -> None:
"""Use the same pager used by pydoc."""
self._pager(content)
if __name__ == "__main__": # pragma: no cover
from .__main__ import make_test_card
from .console import Console
console = Console()
with console.pager(styles=True):
console.print(make_test_card())

View File

@ -300,6 +300,30 @@ class Segment(NamedTuple):
last_segment = segment
yield last_segment
@classmethod
def strip_links(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
"""Remove all links from an iterable of styles.
Yields:
Segment: Segments with link removed.
"""
for segment in segments:
if segment.is_control or segment.style is None:
yield segment
else:
text, style, _is_control = segment
yield cls(text, style.update_link(None) if style else None)
@classmethod
def strip_styles(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
"""Remove all styles from an iterable of segments.
Yields:
Segment: Segments with styles replace with None
"""
for text, _style, is_control in segments:
yield cls(text, None, is_control)
if __name__ == "__main__": # pragma: no cover
lines = [[Segment("Hello")]]

View File

@ -522,6 +522,28 @@ class Style:
style._null = False
return style
def update_link(self, link: str = None) -> "Style":
"""Get a copy with a different value for link.
Args:
link (str, optional): New value for link. Defaults to None.
Returns:
Style: A new Style instance.
"""
style = self.__new__(Style)
style._ansi = self._ansi
style._style_definition = self._style_definition
style._color = self._color
style._bgcolor = self._bgcolor
style._attributes = self._attributes
style._set_attributes = self._set_attributes
style._link = link
style._link_id = f"{time()}-{randint(0, 999999)}" if link else ""
style._hash = self._hash
style._null = False
return style
def render(
self,
text: str = "",

View File

@ -171,7 +171,7 @@ class Table(JupyterMixin):
self.footer_style = footer_style
self.border_style = border_style
self.title_style = title_style
self.caption_style = title_style
self.caption_style = caption_style
self.title_justify = title_justify
self.caption_justify = caption_justify
self._row_count = 0
@ -241,12 +241,6 @@ class Table(JupyterMixin):
def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
if self.width is not None:
max_width = self.width
# if self.box:
# max_width -= len(self.columns) - 1
# if self.show_edge:
# max_width -= 2
if max_width < 0:
return Measurement(0, 0)

View File

@ -2,12 +2,14 @@ import io
import os
import sys
import tempfile
from typing import Optional
import pytest
from rich import errors
from rich.color import ColorSystem
from rich.console import CaptureError, Console, ConsoleOptions
from rich import errors
from rich.pager import SystemPager
from rich.panel import Panel
from rich.style import Style
@ -346,3 +348,25 @@ def test_bell() -> None:
console.begin_capture()
console.bell()
assert console.end_capture() == "\x07"
def test_pager() -> None:
console = Console()
pager_content: Optional[str] = None
def mock_pager(content: str) -> None:
nonlocal pager_content
pager_content = content
pager = SystemPager()
pager._pager = mock_pager
with console.pager(pager):
console.print("[bold]Hello World")
assert pager_content == "Hello World\n"
with console.pager(pager, styles=True, links=False):
console.print("[bold link https:/example.org]Hello World")
assert pager_content == "Hello World\n"

View File

@ -86,3 +86,13 @@ def test_filter_control():
assert list(Segment.filter_control(segments, is_control=True)) == [
Segment("bar", is_control=True)
]
def test_strip_styles():
segments = [Segment("foo", Style(bold=True))]
assert list(Segment.strip_styles(segments)) == [Segment("foo", None)]
def test_strip_links():
segments = [Segment("foo", Style(bold=True, link="https://www.example.org"))]
assert list(Segment.strip_links(segments)) == [Segment("foo", Style(bold=True))]