Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[REQUEST] Support for deleting/removing rows/columns from table #1331

Closed
cbrxyz opened this issue Jul 7, 2021 · 2 comments
Closed

[REQUEST] Support for deleting/removing rows/columns from table #1331

cbrxyz opened this issue Jul 7, 2021 · 2 comments

Comments

@cbrxyz
Copy link

cbrxyz commented Jul 7, 2021

Hi! I'm trying to build a UI through the Table class, but I couldn't find a way to remove rows or columns from the table, only add. How I'm getting around it now is by just constantly re-building the table, aka:

from rich.table import Table
from rich.console import Console

console = Console()

rows = []

while True:
    user_input = input("Would you like to add or delete?")
    if user_input == "add":
        rows.append("Cell")
    else:
        if len(rows) > 0:
            rows = rows[:-1]
    table = Table("Cells")
    for row in rows:
        table.add_row(row)
    console.print(table)

This works fine, but I wanted to use the Live class, but to do so, I believe there would have to be a remove_row method for the table. I could use the update() method, but I believe I would still have to store the rows in an external array (outside that of the rows array inside the Table class). I would hope remove_row would help to reduce this need.

How would you improve Rich?

I would hope that the remove methods for tables could help to add more flexibility to their generation. Two methods of implementation that I had in mind include:

import time

from rich.table import Table
from rich.live import Live

table = Table("Cells")
with Live(table, refresh_per_second=4):
    time.sleep(0.4)
    user_input = input("Would you like to add or delete?")
    if user_input == "add":
        table.add_row("Cell")
    else:
        table.remove_row() # by default removes the last row, but if index is passed in, removes that row
import time

from rich.table import table
from rich.live import live

table = table("cells")
with live(table, refresh_per_second=4):
    time.sleep(0.4)
    user_input = input("would you like to add or delete?")
    if user_input == "add":
        table.add_row("cell")
    else:
        last_row = table.rows[-1] # gets last row of table
        last_row.remove() # now removes it

What problem does it solve for you?

I believe implementing this function would allow me to remove the need to manage my table data in an external array that I know I can add to and delete from. This would allow me to keep the data inside the Table class and only use the class' methods to manipulate data.

Did I help

If I was able to resolve your problem, consider sponsoring my work on Rich, or buy me a coffee to say thanks.

@willmcgugan
Copy link
Collaborator

The Table class (and all other Rich renderables) exist purely to draw things to the terminal. It leaves the responsibility for storing the row data up to the developer.

Your original solution is absolutely fine. Store your data in a convenient format, and generate a Table when you want to print it. Think of it more like HTML. The table is the finished product you create from your data, but its not how you store the data.

@Landcruiser87
Copy link

Landcruiser87 commented May 23, 2023

First. @willmcgugan. Rich is my favorite python library. Thank you for all your hard work keeping it up date! It does everything that I've always wanted a terminal to be able to do. (I've yet to delve into textual yet but soon!) I found a workaround for the above request for deleting rows. In my case, i wanted to send log messages to a separate part of the Live TUI while my algorithm runs in the background. Most attempts at this try to tail a log file over time, but as those logs grow, that feasibility decreases with reading massive logs every loop. So instead I built a Handler class to the logging routine that would dump any messages bound for logger into a temp list. Then when the templist gets to more than half the terminal height, I pop off the first item in the templist and redraw the table to get a rolling list of log values. Still cleaning up the code a bit but as an example the below works wonderfully!

import logging
import numpy as np
import os
from pathlib import Path
from datetime import datetime
from time import sleep
from rich.layout import Layout
from rich.console import Console
from rich.logging import RichHandler
from rich.panel import Panel
from rich.live import Live
from rich.table import Table
from rich.progress import (
	Progress,
	BarColumn,
	SpinnerColumn,
	TextColumn,
	TimeRemainingColumn,
	MofNCompleteColumn,
	TimeElapsedColumn,
)


def get_file_handler(log_dir: Path) -> logging:
	log_format = "[%(asctime)s]-[%(funcName)s(%(lineno)d)]-[%(levelname)s]-[%(message)s]"
	log_file = log_dir / "test.log"
	file_handler = logging.FileHandler(log_file)
	file_handler.setFormatter(logging.Formatter(log_format))
	return file_handler


def get_logger(log_dir: Path, console: Console) -> logging:
	logger = logging.getLogger(__name__)
	logger.setLevel(logging.INFO)
	logger.addHandler(get_file_handler(log_dir))
	return logger


def make_layout() -> Layout:
	layout = Layout(name="root")
	layout.split(
		Layout(name="header", size=3), 
		Layout(name="main")
	)
	layout["main"].split_row(
		Layout(name="leftside"), 
		Layout(name="termoutput")
	)
	layout["leftside"].split_column(
		Layout(name="stats", ratio=2), 
		Layout(name="progbar")
	)
	return layout



def get_stats() -> Table:
	rand_arr = np.random.randint(low=2, high=10, size=(10, 3)) + np.random.random((10, 3))
	stats_table = Table(
		expand=True,
		show_header=True,
		header_style="bold",
		title="[magenta][b]Hot Stats![/b]",
		highlight=True,
	)
	stats_table.add_column("Column", justify="right")
	stats_table.add_column("Mean", justify="center")
	stats_table.add_column("Std", justify="center")
	stats_table.add_column("Max", justify="center")
	stats_table.add_column("Min", justify="center")

	for col in range(rand_arr.shape[1]):
		stats_table.add_row(
			f"Col {col}",
			f"{rand_arr[:, col].mean():.2f}",
			f"{rand_arr[:, col].std():.2f}",
			f"{rand_arr[:, col].max():.2f}",
			f"{rand_arr[:, col].min():.2f}",
		)

	return stats_table

#Old code for reading a the last line of a log file. 
# def get_last_log(logger) -> str:
# 	log_file_path = logger.handlers[0].baseFilename
# 	with open(log_file_path, "r") as file:
# 		log_entries = file.readlines()
# 		last_entry = log_entries[-1].strip() if log_entries else ""

# 	return last_entry


class make_header:
	"""Display header with clock."""

	def __rich__(self) -> Panel:
		grid = Table.grid(expand=True)
		grid.add_column(justify="center", ratio=1)
		grid.add_column(justify="right")
		grid.add_row(
			"[b]Super[/b] cool application",
			datetime.now().ctime().replace(":", "[blink]:[/]"),
		)
		return Panel(grid, style="red on black")


class MainTableHandler(logging.Handler):
	def __init__(self, main_table: Table, layout: Layout, log_level: str):
		super().__init__()
		self.main_table = main_table
		self.log_list = []
		self.layout = layout
		self.log_format = "%(asctime)s-(%(funcName)s)-%(lineno)d-%(levelname)s-[%(message)s]"
		self.setLevel(log_level)
		#Could set colors for levels here. 

	def emit(self, record):
		record.asctime = record.asctime.split(",")[0]
		#msg = self.format(record) #if you want just the message info switch comment lines
		msg = self.log_format % record.__dict__
		tsize = os.get_terminal_size().lines // 2
		if len(self.log_list) > tsize:
			self.log_list.append(msg)
			self.log_list.pop(0)
			self.main_table = redraw_main_table(self.log_list)
			self.layout["termoutput"].update(Panel(self.main_table, border_style="red"))
		else:
			self.main_table.add_row(msg)
			self.log_list.append(msg)


def redraw_main_table(temp_list: list) -> Table:
	main_table = Table(
		expand=True,
		show_header=False,
		header_style="bold",
		title="[blue][b]Log Entries[/b]",
		highlight=True,
	)
	main_table.add_column("Log Entries")
	for row in temp_list:
		main_table.add_row(row)

	return main_table

def main():
	console = Console(color_system="truecolor")
	logger = get_logger(Path.cwd(), console=console)

	main_table = Table(
		expand=True,
		show_header=False,
		header_style="bold",
		title="[blue][b]Log Messages[/b]",
		highlight=True,
	)
	main_table.add_column("Log Output")

	my_progress_bar = Progress(
		SpinnerColumn(),
		TextColumn("{task.description}"),
		BarColumn(),
		"time remain:",
		TimeRemainingColumn(),
		TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
	)
	my_counter = range(30)
	my_task = my_progress_bar.add_task("The jobs", total=int(len(my_counter)))

	progress_table = Table.grid(expand=True)
	progress_table.add_row(
		Panel(
			my_progress_bar,
			title="Super awesome progress bar",
			border_style="green",
			padding=(1, 1),
		)
	)

	stats_table = get_stats()
	layout = make_layout()
	layout["header"].update(make_header())
	layout["progbar"].update(Panel(progress_table, border_style="green"))
	layout["termoutput"].update(Panel(main_table, border_style="blue"))
	layout["stats"].update(Panel(stats_table, border_style="magenta"))

	with Live(layout, refresh_per_second=10, screen=True) as live:
		# Add MainTableHandler to logger
		logger.addHandler(MainTableHandler(main_table, layout, logger.level))
		for count in my_counter:
			sleep(1)
			logger.info(f"I made it to count {count}")
			my_progress_bar.update(my_task, completed=count)

			if count % 5 == 0:
				logger.warning(f"Gettin some STATS")
				stats_table = get_stats()
				layout["stats"].update(Panel(stats_table, border_style="magenta"))
				logger.critical("Stranger Danger")
			live.refresh()

	logger.info("Done logging.")


if __name__ == "__main__":
	main()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants