'Redirect print and/or logging to Panel

I have made a small application that uses a Rich to show a Live view in several Panels.
Is there a way to put standard (or rich) print statements into a particular panel?
Being able to show logging output in its own dedicated panel would also work.

I feel like this would be a very common use case but I haven't found any documentation. I think the answer could be in using the Console.capture() method but I can't figure it out.



Solution 1:[1]

I figure out how you can manage to that. First, we need to intercept the stdout process of Rich logger. We start with a class:

from collections import deque
class Logger():
    _instance = None
    def __init__(self):
        self.messages = deque(["sa"])
        self.size = 10

    def __new__(class_, *args, **kwargs):
        if not isinstance(class_._instance, class_):
            class_._instance = object.__new__(class_, *args, **kwargs)
        return class_._instance

    def write(self, message):
        self.messages.extend(message.splitlines())
        while len(self.messages) > self.size:
            self.messages.popleft()

    def flush(self):
        pass

which is a Singleton class. We need to pass this class into console API as

from rich.console import Console
c = Console(file=Logger(), width=150)

with some width. Then, we create a logging handler

from rich.logging import RichHandler
r = RichHandler(console=c)

This will be our logging handler as

import logging
logging.basicConfig(
    level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[r]
)
logger = logging.getLogger("rich")

Later on, we need to use our Logger class called somewhere you manage your layout. For me, it is inside a Dashboard class.

class Dashboard:
    def __init__(self):
        self.log_std = Logger()
    def update(self, new_parameters):
        self.layout["logs"].update(Panel(Text(
            "\n".join(self.log_std.messages), justify="right"), padding=(1, 2),
        title="[b red]Logs",
        ))

Each time I call update method, it updates my layout. My layout is more complex, self.layout["logs"] where I show the logs.

Solution 2:[2]

So I took @Mete Yildirim's answer as inspiration and came up with a slight variation that uses an existing logging handler instead of creating a new logger.

The logging.handlers module has a BufferingHandler() that I used for prototyping my solution. By passing the handler into rich, I can snoop its contents and print them into my rich Panel without mods to logging.

LOG_BUFFER_MAX_MSGS = 20

# Set up the main/root logger
main_logger = logging.getLogger()
main_logger.setLevel(logging.DEBUG)

# Instantiate the buffering handler we will watch from within Rich
buffering_handler = BufferingHandler(capacity=LOG_BUFFER_MAX_MSGS)
main_logger.addHandler(buffering_handler)

# Local logger
log = logging.getLogger("rich")


# Create a basic Rich layout
layout = Layout(name="root")

def get_log():
    """
    We call this method from within Rich to snoop the messages
    within the BufferingHandler and put them in a form Rich 
    can display.
    Check the BufferingHandler() source to see how we can access
    its data.
    """
    log_messages = []
    for li in buffering_handler.buffer:
        log_messages.append(li.msg)
    return Panel("\n".join(log_messages))

# Run Rich, displaying each log message to the screen
with Live(layout, refresh_per_second=4) as live:
    while True:
        layout["root"].update(get_log())
        time.sleep(0.25)

To test the above, we can generate some log messages in the background:

def create_log_messages():
    msgs = (
        "Test message 1",
        "Test message 2",
        "Test message 3",
    )
    for li in msgs:
        log.info(li)
        time.sleep(2)

threading.Thread(target=create_log_messages).start()

Shortcomings: The BufferingHandler() will clear it's buffer when the number of lines exceeds the value passed into its capacity argument. I would prefer that it delete old messages instead, but that will require either overloading the existing BufferHandler() implementation or writing a new Handler. The BufferHandler() code is short, so simply writing a new one shouldn't be too much effort.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Mete Yıldırım
Solution 2 JS.