add new render hooks for transparent progress

This commit is contained in:
Will McGugan 2020-06-12 21:00:56 +01:00
parent 8b5a8ddf07
commit f8b45a87be
4 changed files with 99 additions and 101 deletions

View file

@ -235,6 +235,15 @@ class ConsoleThreadLocals(threading.local):
buffer_index: int = 0 buffer_index: int = 0
class RenderHook:
"""Provides hooks in to render process."""
def process_renderables(
self, renderables: List[ConsoleRenderable]
) -> List[ConsoleRenderable]:
return renderables
def detect_legacy_windows() -> bool: def detect_legacy_windows() -> bool:
"""Detect legacy Windows.""" """Detect legacy Windows."""
return "WINDIR" in os.environ and "WT_SESSION" not in os.environ return "WINDIR" in os.environ and "WT_SESSION" not in os.environ
@ -325,6 +334,7 @@ class Console:
self._record_buffer_lock = threading.RLock() self._record_buffer_lock = threading.RLock()
self._thread_locals = ConsoleThreadLocals() self._thread_locals = ConsoleThreadLocals()
self._record_buffer: List[Segment] = [] self._record_buffer: List[Segment] = []
self._render_hooks: List[RenderHook] = []
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<console width={self.width} {str(self._color_system)}>" return f"<console width={self.width} {str(self._color_system)}>"
@ -368,6 +378,19 @@ class Console:
self._buffer_index -= 1 self._buffer_index -= 1
self._check_buffer() self._check_buffer()
def push_render_hook(self, hook: RenderHook) -> None:
"""Add a new render hook to the stack.
Args:
hook (RenderHook): Render hook instance.
"""
self._render_hooks.append(hook)
def pop_render_hook(self) -> None:
"""Pop the last renderhook from the stack."""
self._render_hooks.pop()
def __enter__(self) -> "Console": def __enter__(self) -> "Console":
"""Own context manager to enter buffer context.""" """Own context manager to enter buffer context."""
self._enter_buffer() self._enter_buffer()
@ -684,14 +707,15 @@ class Console:
if rich_cast: if rich_cast:
renderable = rich_cast() renderable = rich_cast()
if isinstance(renderable, str): if isinstance(renderable, str):
append_text( if renderable:
self.render_str( append_text(
renderable, self.render_str(
emoji=emoji, renderable,
markup=markup, emoji=emoji,
highlighter=_highlighter, markup=markup,
highlighter=_highlighter,
)
) )
)
elif isinstance(renderable, ConsoleRenderable): elif isinstance(renderable, ConsoleRenderable):
check_text() check_text()
append(renderable) append(renderable)
@ -754,7 +778,7 @@ class Console:
end (str, optional): String to write at end of print data. Defaults to "\n". end (str, optional): String to write at end of print data. Defaults to "\n".
style (Union[str, Style], optional): A style to apply to output. Defaults to None. style (Union[str, Style], optional): A style to apply to output. Defaults to None.
justify (str, optional): Justify method: "default", "left", "right", "center", or "full". Defaults to ``None``. justify (str, optional): Justify method: "default", "left", "right", "center", or "full". Defaults to ``None``.
overflow (str, optional): Overflow method: "crop", "fold", or "ellipisis". Defaults to None. overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None.
no_wrap (Optional[bool], optional): Disable word wrapping. Defaults to None. no_wrap (Optional[bool], optional): Disable word wrapping. Defaults to None.
emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to ``None``. emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to ``None``.
markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to ``None``. markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to ``None``.
@ -775,6 +799,8 @@ class Console:
markup=markup, markup=markup,
highlight=highlight, highlight=highlight,
) )
for hook in self._render_hooks:
renderables = hook.process_renderables(renderables)
render_options = self.options.update( render_options = self.options.update(
justify=justify, overflow=overflow, width=width, no_wrap=no_wrap justify=justify, overflow=overflow, width=width, no_wrap=no_wrap
) )
@ -843,35 +869,39 @@ class Console:
if not objects: if not objects:
self.line() self.line()
return return
renderables = self._collect_renderables(
objects,
sep,
end,
justify=justify,
emoji=emoji,
markup=markup,
highlight=highlight,
)
caller = inspect.stack()[_stack_offset]
path = caller.filename.rpartition(os.sep)[-1]
line_no = caller.lineno
if log_locals:
locals_map = {
key: value
for key, value in caller.frame.f_locals.items()
if not key.startswith("__")
}
renderables.append(tabulate_mapping(locals_map, title="Locals"))
with self: with self:
self._buffer.extend( renderables = self._collect_renderables(
self.render( objects,
self._log_render(self, renderables, path=path, line_no=line_no), sep,
self.options, end,
) justify=justify,
emoji=emoji,
markup=markup,
highlight=highlight,
) )
caller = inspect.stack()[_stack_offset]
path = caller.filename.rpartition(os.sep)[-1]
line_no = caller.lineno
if log_locals:
locals_map = {
key: value
for key, value in caller.frame.f_locals.items()
if not key.startswith("__")
}
renderables.append(tabulate_mapping(locals_map, title="Locals"))
renderables = [
self._log_render(self, renderables, path=path, line_no=line_no)
]
for hook in self._render_hooks:
renderables = hook.process_renderables(renderables)
extend = self._buffer.extend
render = self.render
render_options = self.options
for renderable in renderables:
extend(render(renderable, render_options))
def _check_buffer(self) -> None: def _check_buffer(self) -> None:
"""Check if the buffer may be rendered.""" """Check if the buffer may be rendered."""
with self._lock: with self._lock:
@ -883,7 +913,8 @@ class Console:
del self._buffer[:] del self._buffer[:]
else: else:
text = self._render_buffer() text = self._render_buffer()
self.file.write(text) if text:
self.file.write(text)
self.file.flush() self.file.flush()
def _render_buffer(self) -> str: def _render_buffer(self) -> str:

View file

@ -37,7 +37,7 @@ class LiveRender:
if self._shape is not None: if self._shape is not None:
_, height = self._shape _, height = self._shape
if height > 1: if height > 1:
return Control(f"\r\x1b[{height - 1}A\x1b[2K") return Control("\r\x1b[2K" + "\x1b[1A\x1b[2K" * (height - 1))
else: else:
return Control("\r\x1b[2K") return Control("\r\x1b[2K")
return Control("") return Control("")

View file

@ -3,6 +3,7 @@ from datetime import datetime
from logging import Handler, LogRecord from logging import Handler, LogRecord
from pathlib import Path from pathlib import Path
from . import get_console
from rich._log_render import LogRender from rich._log_render import LogRender
from rich.console import Console from rich.console import Console
from rich.highlighter import ReprHighlighter from rich.highlighter import ReprHighlighter
@ -25,7 +26,7 @@ class RichHandler(Handler):
def __init__(self, level: int = logging.NOTSET, console: Console = None) -> None: def __init__(self, level: int = logging.NOTSET, console: Console = None) -> None:
super().__init__(level=level) super().__init__(level=level)
self.console = Console() if console is None else console self.console = get_console() if console is None else console
self.highlighter = ReprHighlighter() self.highlighter = ReprHighlighter()
self._log_render = LogRender(show_level=True) self._log_render = LogRender(show_level=True)

View file

@ -27,7 +27,15 @@ from typing import (
from . import get_console from . import get_console
from .bar import Bar from .bar import Bar
from .console import Console, JustifyMethod, RenderGroup, RenderableType from .console import (
Console,
ConsoleRenderable,
JustifyMethod,
RenderGroup,
RenderHook,
RenderableType,
)
from .control import Control
from .highlighter import Highlighter from .highlighter import Highlighter
from . import filesize from . import filesize
from .live_render import LiveRender from .live_render import LiveRender
@ -368,7 +376,7 @@ class _RefreshThread(Thread):
self.progress.refresh() self.progress.refresh()
class Progress: class Progress(RenderHook):
"""Renders an auto-updating progress bar(s). """Renders an auto-updating progress bar(s).
Args: Args:
@ -410,6 +418,8 @@ class Progress:
self._refresh_thread: Optional[_RefreshThread] = None self._refresh_thread: Optional[_RefreshThread] = None
self._refresh_count = 0 self._refresh_count = 0
self._started = False self._started = False
self.print = self.console.print
self.log = self.console.log
@property @property
def tasks(self) -> List[Task]: def tasks(self) -> List[Task]:
@ -438,6 +448,7 @@ class Progress:
return return
self._started = True self._started = True
self.console.show_cursor(False) self.console.show_cursor(False)
self.console.push_render_hook(self)
self.refresh() self.refresh()
if self.auto_refresh: if self.auto_refresh:
self._refresh_thread = _RefreshThread(self, self.refresh_per_second) self._refresh_thread = _RefreshThread(self, self.refresh_per_second)
@ -457,6 +468,7 @@ class Progress:
self.console.line() self.console.line()
finally: finally:
self.console.show_cursor(True) self.console.show_cursor(True)
self.console.pop_render_hook()
if self._refresh_thread is not None: if self._refresh_thread is not None:
self._refresh_thread.join() self._refresh_thread.join()
self._refresh_thread = None self._refresh_thread = None
@ -598,8 +610,9 @@ class Progress:
self._live_render.set_renderable(self.get_renderable()) self._live_render.set_renderable(self.get_renderable())
if self.console.is_terminal: if self.console.is_terminal:
with self.console: with self.console:
self.console.print(self._live_render.position_cursor()) # self.console.print(self._live_render.position_cursor())
self.console.print(self._live_render) self.console.print(Control(""))
# self.console.print(self._live_render)
self._refresh_count += 1 self._refresh_count += 1
def get_renderable(self) -> RenderableType: def get_renderable(self) -> RenderableType:
@ -693,65 +706,17 @@ class Progress:
with self._lock: with self._lock:
del self._tasks[task_id] del self._tasks[task_id]
def print( def process_renderables(
self, self, renderables: List[ConsoleRenderable]
*objects: Any, ) -> List[ConsoleRenderable]:
sep=" ", """Process renderables to restore cursor and display progress."""
end="\n", if self.console.is_terminal:
style: Union[str, Style] = None, renderables = [
justify: JustifyMethod = None, self._live_render.position_cursor(),
emoji: bool = None, *renderables,
markup: bool = None, self._live_render,
highlight: bool = None, ]
) -> None: return renderables
"""Print to the terminal and preserve progress display. Parameters identical to :class:`~rich.console.Console.print`."""
console = self.console
with console:
if console.is_terminal:
console.print(self._live_render.position_cursor())
console.print(
*objects,
sep=sep,
end=end,
style=style,
justify=justify,
emoji=emoji,
markup=markup,
highlight=highlight,
)
if console.is_terminal:
console.print(self._live_render)
def log(
self,
*objects: Any,
sep=" ",
end="\n",
justify: JustifyMethod = None,
emoji: bool = None,
markup: bool = None,
highlight: bool = None,
log_locals: bool = False,
_stack_offset=1,
) -> None:
"""Log to the terminal and preserve progress display. Parameters identical to :class:`~rich.console.Console.log`."""
console = self.console
with console:
if console.is_terminal:
console.print(self._live_render.position_cursor())
console.log(
*objects,
sep=sep,
end=end,
justify=justify,
emoji=emoji,
markup=markup,
highlight=highlight,
log_locals=log_locals,
_stack_offset=_stack_offset + 1,
)
if console.is_terminal:
console.print(self._live_render)
if __name__ == "__main__": # pragma: no coverage if __name__ == "__main__": # pragma: no coverage
@ -799,7 +764,8 @@ yield True, previous_value''',
examples = cycle(progress_renderables) examples = cycle(progress_renderables)
with Progress(transient=True) as progress: console = Console()
with Progress(console=console, transient=True) as progress:
task1 = progress.add_task(" [red]Downloading", total=1000) task1 = progress.add_task(" [red]Downloading", total=1000)
task2 = progress.add_task(" [green]Processing", total=1000) task2 = progress.add_task(" [green]Processing", total=1000)