class Table:
"""Display information in a table
```py
from arc.present.table import Table
t = Table(["Name", "Age", "Stand"])
t.add_row(["Jonathen Joestar", 20, "-"])
t.add_row(["Joseph Joestar", 18, "Hermit Purple (in Part 3)"])
t.add_row(["Jotaro Kujo", 18, "Star Platinum"])
t.add_row(["Josuke Higashikata", 16, "Crazy Diamond"])
t.add_row(["Giorno Giovanna", 15, "Gold Experience"])
t.add_row(["Joylene Kujo", 19, "Stone Free"])
print(t)
```
Will yield:
```console
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Name ┃ Age ┃ Stand ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ Jonathen Joestar │ 20 │ - │
│ Joseph Joestar │ 18 │ Hermit Purple (in Part 3) │
│ Jotaro Kujo │ 18 │ Star Platinum │
│ Josuke Higashikata │ 16 │ Crazy Diamond │
│ Giorno Giovanna │ 15 │ Gold Experience │
│ Joylene Kujo │ 19 │ Stone Free │
└────────────────────┴─────┴───────────────────────────┘
```
"""
def __init__(
self,
columns: ColumnInput,
rows: t.Sequence[t.Sequence[t.Any]] | None = None,
default_formatting: bool = True,
) -> None:
self.__columns: t.Sequence[Column] = self.__resolve_columns(columns)
self.__rows: list[Row] = []
self._border = drawing.borders["light"]
self._head_border = BORDER_HEAVY_TRANS
self._header_cell_formatter: TableFormatter = (
_format_header_cell if default_formatting else _format_cell
)
self._cell_formatter: TableFormatter = _format_cell
self._type_formatters: dict[type, TableFormatter] = (
_DEFAULT_TYPE_FORMATTERS if default_formatting else {}
)
if rows:
self.add_rows(rows)
def __str__(self) -> str:
for col in self.__columns:
cells = [row[col["name"]] for row in self.__rows]
cells.append(col["name"])
col["width"] = max(
Ansi.len(self._fmt_cell_contents(cell)) + 2 for cell in cells
)
table = ""
table += self._fmt_header()
table += "\n"
for row, has_next_row in has_next(self.__rows):
table += self._fmt_row(row, has_next_row)
if has_next_row:
table += "\n"
return table
def add_row(self, row: t.Sequence[t.Any]) -> None:
if len(row) > len(self.__columns):
raise errors.ArcError("Too many values")
resolved = {}
for col, value in itertools.zip_longest(self.__columns, row, fillvalue=""):
resolved[col["name"]] = value # type: ignore
self.__rows.append(resolved)
def add_rows(self, rows: t.Sequence[t.Sequence[t.Any]]) -> None:
for row in rows:
self.add_row(row)
def fmt_header_cell(
self, func: TableFormatter | None = None
) -> TableFormatter | t.Callable[[TableFormatter], TableFormatter]:
def inner(func: TableFormatter) -> TableFormatter:
self._cell_formatter = func
return func
if func:
return inner(func)
return inner
def fmt_cell(
self, func: TableFormatter | None = None
) -> TableFormatter | t.Callable[[TableFormatter], TableFormatter]:
"""Formats any cell that does not already have a formatter applied
```py
from arc.color import fg, fx
from arc.present import Table
table = Table()
@table.fmt_cell
def fmt(cell):
return f"{fg.RED}{cell}{fx.CLEAR}"
```
"""
def inner(func: TableFormatter) -> TableFormatter:
self._cell_formatter = func
return func
if func:
return inner(func)
return inner
def fmt_type(self, cls: type) -> t.Callable[[TableFormatter], TableFormatter]:
def inner(func: t.Callable[[t.Any], str]) -> TableFormatter:
self._type_formatters[cls] = func
return func
return inner
def _fmt_header(self) -> str:
border = self._head_border
header = ""
header += border["corner"]["top_left"]
for idx, col in enumerate(self.__columns):
header += border["horizontal"] * col["width"]
if idx < len(self.__columns) - 1:
header += border["intersect"]["hori_top"]
else:
header += border["corner"]["top_right"]
header += "\n"
header += border["vertical"]
for col in self.__columns:
header += self._fmt_cell(
col["name"],
col["width"],
col["justify"],
header=True,
)
header += border["vertical"]
header += "\n"
header += border["intersect"]["vert_left"]
for col, has_next_column in has_next(self.__columns):
header += border["horizontal"] * col["width"]
if has_next_column:
header += border["intersect"]["cross"]
else:
header += border["intersect"]["vert_right"]
return header
def _fmt_row(self, row: Row, next_row: bool) -> str:
border = self._border
fmt = ""
fmt += border["vertical"]
for col in self.__columns:
cell = row.get(col["name"], "")
fmt += self._fmt_cell(cell, col["width"], col["justify"])
fmt += border["vertical"]
if not next_row:
fmt += "\n"
fmt += border["corner"]["bot_left"]
for col, has_next_column in has_next(self.__columns):
fmt += border["horizontal"] * col["width"]
if has_next_column:
fmt += border["intersect"]["hori_bot"]
else:
fmt += border["corner"]["bot_right"]
return fmt
def _fmt_cell(
self,
cell: t.Any,
width: int,
justify: drawing.Justification,
header: bool = False,
) -> str:
formatted_cell = self._fmt_cell_contents(cell, header)
width = width - 2
padding = " " * (width - Ansi.len(formatted_cell))
if justify == "left":
return " " + formatted_cell + padding + " "
elif justify == "right":
return " " + padding + formatted_cell + " "
elif justify == "center":
padding_width, remainder = divmod(width - Ansi.len(formatted_cell), 2)
padding = " " * padding_width
return (
" " + padding + formatted_cell + padding + (" " if remainder else " ")
)
@functools.cache
def _fmt_cell_contents(self, cell: t.Any, header: bool = False) -> str:
if header:
return self._header_cell_formatter(cell)
if type(cell) in self._type_formatters:
return self._type_formatters[type(cell)](cell)
else:
return self._cell_formatter(cell)
@staticmethod
def __resolve_columns(columns: ColumnInput) -> list[Column]:
resolved: list[Column] = []
for column in columns:
copy = DEFAULT_COLUMN.copy()
if isinstance(column, str):
copy.update({"name": column})
elif isinstance(column, dict):
column = t.cast(Column, column)
copy.update(column) # type: ignore
else:
raise errors.ArcError("Columns must either be a string or a dictionary")
resolved.append(copy)
return resolved