Search code examples
pythonrich

How to get Rich to print out final table without cutting off the bottom?


I am using the "Rich" printing tool in Python. It's working great - but I have a problem. When I run it with more content than fits on the terminal window, it cuts everything off at the bottom (as expected). But when it finishes, I want to allow the entire table to print out. Here's a repro:

import asyncio
import logging
import random
import time
import uuid

from rich.console import Console
from rich.layout import Layout
from rich.live import Live
from rich.panel import Panel
from rich.progress import BarColumn, Progress, TextColumn
from rich.table import Table

all_statuses = {}
task_total = 30

console = Console()

table_update_event = asyncio.Event()
table_update_running = False

events_to_progress = []

max_time = 5  # Changed to 5 seconds

logging.basicConfig(level=logging.INFO)


class InstanceStatus:
    def __init__(self, region, zone):
        # Generate a unique ID for each instance - maximum 6 characters
        self.id = f"{region}-{zone}-{uuid.uuid4()}"[:6]
        self.region = region
        self.zone = zone
        self.status = "Initializing"
        self.detailed_status = "Initializing"
        self.elapsed_time = 0
        self.instance_id = None
        self.public_ip = None
        self.private_ip = None
        self.vpc_id = None

    def combined_status(self):
        return f"{self.status} ({self.detailed_status})"


def make_progress_table():
    table = Table(show_header=True, header_style="bold magenta", show_lines=False)
    table.add_column("ID", width=8, style="cyan", no_wrap=True)
    table.add_column("Region", width=8, style="green", no_wrap=True)
    table.add_column("Zone", width=8, style="green", no_wrap=True)
    table.add_column("Status", width=15, style="yellow", no_wrap=True)
    table.add_column("Elapsed", width=8, justify="right", style="magenta", no_wrap=True)
    table.add_column("Instance ID", width=15, style="blue", no_wrap=True)
    table.add_column("Public IP", width=15, style="blue", no_wrap=True)
    table.add_column("Private IP", width=15, style="blue", no_wrap=True)

    sorted_statuses = sorted(all_statuses.values(), key=lambda x: (x.region, x.zone))
    for status in sorted_statuses:
        table.add_row(
            status.id[:8],
            status.region[:8],
            status.zone[:8],
            status.combined_status()[:15],
            f"{status.elapsed_time:.1f}s",
            (status.instance_id or "")[:15],
            (status.public_ip or "")[:15],
            (status.private_ip or "")[:15],
        )
    return table


def create_layout(progress, table):
    layout = Layout()
    progress_panel = Panel(
        progress,
        title="Progress",
        border_style="green",
        padding=(1, 1),
    )
    layout.split(
        Layout(progress_panel, size=5),
        Layout(table),
    )
    return layout


async def update_table(live):
    global table_update_running, events_to_progress, all_statuses, console
    if table_update_running:
        logging.debug("Table update already running. Exiting.")
        return

    logging.debug("Starting table update.")

    try:
        table_update_running = True
        progress = Progress(
            TextColumn("[progress.description]{task.description}"),
            BarColumn(bar_width=None),
            TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
            TextColumn("[progress.completed]{task.completed} of {task.total}"),
            TextColumn("[progress.elapsed]{task.elapsed:>3.0f}s"),
            expand=True,
        )
        task = progress.add_task("Creating Instances", total=task_total)

        while not table_update_event.is_set() or events_to_progress:
            while events_to_progress:
                event = events_to_progress.pop(0)
                all_statuses[event.id] = event
                progress.update(task, completed=len(all_statuses))

            table = make_progress_table()
            layout = create_layout(progress, table)
            live.update(layout)

            await asyncio.sleep(0.05)  # Reduce sleep time for more frequent updates

    except Exception as e:
        logging.error(f"Error in update_table: {str(e)}")
    finally:
        table_update_running = False
        logging.debug("Table update finished.")


async def main():
    global events_to_progress, all_statuses

    start_time = time.time()
    end_time = start_time + 4  # Set to 4 seconds

    statuses_to_create = [
        InstanceStatus(str(random.randint(1, 100)), str(random.randint(1, 1000)))
        for _ in range(task_total)
    ]

    with Live(console=console, refresh_per_second=20) as live:
        update_table_task = asyncio.create_task(update_table(live))

        # Distribute status creation over 4 seconds
        for i in range(task_total):
            events_to_progress.append(statuses_to_create[i])
            if (i + 1) % 10 == 0:
                await asyncio.sleep(0.4)

        # Ensure all statuses are processed
        all_statuses.update({status.id: status for status in statuses_to_create})

        # If we finished early, wait until 4 seconds have passed
        time_elapsed = time.time() - start_time
        if time_elapsed < 4:
            await asyncio.sleep(4 - time_elapsed)

        table_update_event.set()


if __name__ == "__main__":
    asyncio.run(main())

If task_total = 30 is less than the height of your terminal, it works perfectly. If it's greater (e.g. 100), then it cuts off the table and never prints the final result.


Solution

  • Edit:

    After re-reading your question, it seems you are just asking how to print out the table after processing is complete. If that's the case, I would have update_table return the final table. Then add the following at the end of main.

            table = await update_table_task
            live.update(table)
    

    Original Answer:

    You have a couple of problems. First, your Live is not set to show overflow. This one is a simple fix.

    with Live(console=console, refresh_per_second=20, vertical_overflow='visible') as live:
    

    Second, the Layout class will based on the terminal size, not the content size. You would have to measure the size of the table with all of its borders and headers, then you would need to explicitly set the size of the Layouts (outer and inner). Since you are only splitting vertically, however, there is a much simpler solution. Instead of using a Layout, create a Group.

    Replace create_layout with the following (requires you to add Group to the list of imports from rich.console).

    def create_group(progress, table):
        progress_panel = Panel(
            progress,
            title="Progress",
            border_style="green",
            padding=(1, 1),
            height=5,
        )
        return Group(table, progress_panel)
    

    Then you probably want to change the variable name layout in the following (or just get rid of the assignment and do live.update(create_group(progress, table)))

                layout = create_layout(progress, table)
                live.update(layout)
    

    If you don't want to switch the order of the progress panel and the table, another option would be to render the Layout while processing so the progress bar is visible, then show the Group when processing is complete.