Tables and Live update when contents do not fit the screen

Hello,

I’m playing with Rich and Live update, wrote a small script to create a table with random values in one of the columns; The idea is to start rendering the table as soon data comes in (even before the able has any content).
This code works well as long you have enough space on the terminal to display the contents, otherwise the application freezes and you have to interrupt it (Ctrl-C for example) to make it exit:

#!/usr/bin/env python
import random
import time
from datetime import datetime

from rich.console import Console
from rich.live import Live
from rich.table import Table
from rich.traceback import install

install(show_locals=True)


def get_table():
    table = Table(title="This is a big table", caption="This is a never ending table")
    table.add_column("Timestamp", justify="right", style="cyan", no_wrap=True)
    table.add_column("Random value", style="magenta")
    return table


def generate_values(max_range: int = 100):
    for idx in range(0, max_range):
        now = datetime.now().isoformat()
        my_int = random.Random().randint(0, 1_000)
        value = f"[green]{my_int}" if my_int <= 500 else f"[red]{my_int}"
        time.sleep(0.5)
        yield now, value


if __name__ == "__main__":
    try:
        tbl = get_table()
        console = Console()
        with Live(tbl, console=console, refresh_per_second=10, auto_refresh=True) as live:
            for timestamp, integer in generate_values(20):
                tbl.add_row(timestamp, integer)
                live.update(tbl)
    except KeyboardInterrupt:
        pass

Questions:

  • There is a way to ‘flush’ the table contents so the Elipse (…) style is not used?
  • Will Rich have a Scrollable widget, like textualize or is not needed?
  • If textualize is the way to go (because you need a proper Scrollable view), how you can trigger the update of the table properly, without waiting for the table to have all the column data?

Thanks,

–Jose

1 Like

I gave it a shot with an Textual App using a ScrollView; Contents fit nicely but then I loose all the real time updates (table gets rendered only when all the contents are available):

#!/usr/bin/env python
from __future__ import annotations

import random
import time
from datetime import datetime
from pathlib import Path
from typing import Type

from rich.table import Table

from textual import events
from textual.app import App
from textual.driver import Driver
from textual.widgets import ScrollView


class TableApp(App):

    def __init__(
            self,
            screen: bool = True,
            driver_class: Type[Driver] | None = None,
            log: str = "",
            log_verbosity: int = 1,
            title: str = "Example of a bit table",
    ):
        super().__init__(screen, driver_class, log, log_verbosity, title)
        self.body = None

    @staticmethod
    def get_table():
        table = Table(title="This is a big table", caption="This is a never ending table")
        table.add_column("Timestamp", justify="right", style="cyan", no_wrap=True)
        table.add_column("Random value", style="magenta")
        return table

    @staticmethod
    def generate_values(max_range: int = 50):
        for idx in range(0, max_range):
            now = datetime.now().isoformat()
            my_int = random.Random().randint(0, 1_000)
            value = f"[green]{my_int}" if my_int <= 500 else f"[red]{my_int}"
            time.sleep(0.5)
            yield now, value

    async def on_load(self, event: events.Load) -> None:
        await self.bind("q", "quit", "Quit")

    async def on_mount(self, event: events.Mount) -> None:
        self.body = body = ScrollView(auto_width=True)
        await self.view.dock(body)

        async def add_content():
            tbl = TableApp.get_table()
            await body.update(tbl)
            for timestamp, integer in TableApp.generate_values(100):
                tbl.add_row(timestamp, integer)
                await body.update(tbl)
        
        await self.call_later(add_content)


if __name__ == "__main__":
    TableApp.run(title="Bit table app example", log=Path.home().joinpath("big_table_textual.log"))

Not sure what is being locked until the very end. Any ideas?

Thanks,

–Jose

I have a bit of a confusion myself with regards to main thread and asyncio event loop. Looks like events are processed only when main thread is ‘idle’. In your example you are blocking it with sleep but for some reason even await asyncio.sleep does not help. There are couple of other ways to achieve what your are trying though.

import asyncio
import random
import time
from datetime import datetime
from threading import Thread

from rich.table import Table
from textual.app import App
from textual.widgets import ScrollView


class TableView(ScrollView):

    def __init__( self, *args, **kwargs) -> None:
        super().__init__( *args, **kwargs)

        self.table = Table(title="This is a big table", caption="This is a never ending table")
        self.table.add_column("Timestamp", justify="right", style="cyan", no_wrap=True)
        self.table.add_column("Random value", style="magenta")

    async def on_mount(self) -> None:
        await self.update(self.table)

    def add_row(self):
        now = datetime.now().isoformat()
        my_int = random.Random().randint(0, 1_000)
        value = f"[green]{my_int}" if my_int <= 500 else f"[red]{my_int}"
        self.table.add_row(now, value),
        asyncio.create_task(self.update(self.table))

    def update_table(self):
        asyncio.create_task(self.update(self.table))

class MainApp(App):
    async def on_mount(self) -> None:
        self.body = TableView()
        await self.view.dock(self.body)
        self.set_interval(1, self.body.add_row)

        # 2nd options
        # self.set_interval(1, self.body.update_table)
        # await self.add_vals(10)

    async def add_vals(self, max_range: int = 50):
        t = Thread(target=target, args=(self.body, max_range))
        t.start()

def target(body, max_range: int = 50):
    for idx in range(0, max_range):
        now = datetime.now().isoformat()
        my_int = random.Random().randint(0, 1_000)
        value = f"[green]{my_int}" if my_int <= 500 else f"[red]{my_int}"
        time.sleep(0.5)
        body.table.add_row(now, value),

if __name__ == "__main__":
    MainApp.run(title="Bit table app example", log="tui.log")

1 Like

Hello @daler-rahimov ,

I just tried your code and it works like a charm; I’m not quite familiar with asyncio so looks it is time to brush my skills there.

Thanks a lot for the help, greatly appreciated!

–Jose

1 Like