diff --git a/rich/console.py b/rich/console.py index 5e697c1f..9b047b69 100644 --- a/rich/console.py +++ b/rich/console.py @@ -235,6 +235,15 @@ class ConsoleThreadLocals(threading.local): 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: """Detect legacy Windows.""" 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._thread_locals = ConsoleThreadLocals() self._record_buffer: List[Segment] = [] + self._render_hooks: List[RenderHook] = [] def __repr__(self) -> str: return f"" @@ -368,6 +378,19 @@ class Console: self._buffer_index -= 1 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": """Own context manager to enter buffer context.""" self._enter_buffer() @@ -684,14 +707,15 @@ class Console: if rich_cast: renderable = rich_cast() if isinstance(renderable, str): - append_text( - self.render_str( - renderable, - emoji=emoji, - markup=markup, - highlighter=_highlighter, + if renderable: + append_text( + self.render_str( + renderable, + emoji=emoji, + markup=markup, + highlighter=_highlighter, + ) ) - ) elif isinstance(renderable, ConsoleRenderable): check_text() append(renderable) @@ -754,7 +778,7 @@ class Console: 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. 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. 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``. @@ -775,6 +799,8 @@ class Console: markup=markup, highlight=highlight, ) + for hook in self._render_hooks: + renderables = hook.process_renderables(renderables) render_options = self.options.update( justify=justify, overflow=overflow, width=width, no_wrap=no_wrap ) @@ -843,35 +869,39 @@ class Console: if not objects: self.line() 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: - self._buffer.extend( - self.render( - self._log_render(self, renderables, path=path, line_no=line_no), - self.options, - ) + 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")) + + 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: """Check if the buffer may be rendered.""" with self._lock: @@ -883,7 +913,8 @@ class Console: del self._buffer[:] else: text = self._render_buffer() - self.file.write(text) + if text: + self.file.write(text) self.file.flush() def _render_buffer(self) -> str: diff --git a/rich/live_render.py b/rich/live_render.py index 208d8acf..8c994262 100644 --- a/rich/live_render.py +++ b/rich/live_render.py @@ -37,7 +37,7 @@ class LiveRender: if self._shape is not None: _, height = self._shape 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: return Control("\r\x1b[2K") return Control("") diff --git a/rich/logging.py b/rich/logging.py index 3419825d..68d42b21 100644 --- a/rich/logging.py +++ b/rich/logging.py @@ -3,6 +3,7 @@ from datetime import datetime from logging import Handler, LogRecord from pathlib import Path +from . import get_console from rich._log_render import LogRender from rich.console import Console from rich.highlighter import ReprHighlighter @@ -25,7 +26,7 @@ class RichHandler(Handler): def __init__(self, level: int = logging.NOTSET, console: Console = None) -> None: 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._log_render = LogRender(show_level=True) diff --git a/rich/progress.py b/rich/progress.py index 86544ab7..5e660a21 100644 --- a/rich/progress.py +++ b/rich/progress.py @@ -27,7 +27,15 @@ from typing import ( from . import get_console 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 . import filesize from .live_render import LiveRender @@ -368,7 +376,7 @@ class _RefreshThread(Thread): self.progress.refresh() -class Progress: +class Progress(RenderHook): """Renders an auto-updating progress bar(s). Args: @@ -410,6 +418,8 @@ class Progress: self._refresh_thread: Optional[_RefreshThread] = None self._refresh_count = 0 self._started = False + self.print = self.console.print + self.log = self.console.log @property def tasks(self) -> List[Task]: @@ -438,6 +448,7 @@ class Progress: return self._started = True self.console.show_cursor(False) + self.console.push_render_hook(self) self.refresh() if self.auto_refresh: self._refresh_thread = _RefreshThread(self, self.refresh_per_second) @@ -457,6 +468,7 @@ class Progress: self.console.line() finally: self.console.show_cursor(True) + self.console.pop_render_hook() if self._refresh_thread is not None: self._refresh_thread.join() self._refresh_thread = None @@ -598,8 +610,9 @@ class Progress: self._live_render.set_renderable(self.get_renderable()) if self.console.is_terminal: with self.console: - self.console.print(self._live_render.position_cursor()) - self.console.print(self._live_render) + # self.console.print(self._live_render.position_cursor()) + self.console.print(Control("")) + # self.console.print(self._live_render) self._refresh_count += 1 def get_renderable(self) -> RenderableType: @@ -693,65 +706,17 @@ class Progress: with self._lock: del self._tasks[task_id] - def print( - self, - *objects: Any, - sep=" ", - end="\n", - style: Union[str, Style] = None, - justify: JustifyMethod = None, - emoji: bool = None, - markup: bool = None, - highlight: bool = None, - ) -> None: - """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) + def process_renderables( + self, renderables: List[ConsoleRenderable] + ) -> List[ConsoleRenderable]: + """Process renderables to restore cursor and display progress.""" + if self.console.is_terminal: + renderables = [ + self._live_render.position_cursor(), + *renderables, + self._live_render, + ] + return renderables if __name__ == "__main__": # pragma: no coverage @@ -799,7 +764,8 @@ yield True, previous_value''', 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) task2 = progress.add_task(" [green]Processing", total=1000)