326 lines
12 KiB
Python
326 lines
12 KiB
Python
|
|
"""Shared renderers for typer-test output tables.
|
||
|
|
|
||
|
|
This module centralizes Rich table rendering so typer-test.py stays lean. It includes
|
||
|
|
specialized handling for planets plus generic list/dict/object renderers.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from typing import Any, Dict, List, Optional, Sequence
|
||
|
|
|
||
|
|
from rich.console import Console
|
||
|
|
from rich.panel import Panel
|
||
|
|
from rich.table import Table
|
||
|
|
|
||
|
|
from tarot import Card
|
||
|
|
from utils.attributes import Color, Planet
|
||
|
|
|
||
|
|
|
||
|
|
class Renderer:
|
||
|
|
"""Render arbitrary Tarot objects using Rich tables."""
|
||
|
|
|
||
|
|
def __init__(self, console: Console) -> None:
|
||
|
|
self.console = console
|
||
|
|
self.planet_renderer = PlanetRenderer(self)
|
||
|
|
|
||
|
|
# Public helpers -----------------------------------------------------
|
||
|
|
def render_value(self, value: Any) -> None:
|
||
|
|
if isinstance(value, Planet):
|
||
|
|
self.planet_renderer.render(value)
|
||
|
|
return
|
||
|
|
if isinstance(value, Card):
|
||
|
|
self._render_card(value)
|
||
|
|
return
|
||
|
|
if isinstance(value, list) and value and isinstance(value[0], Card):
|
||
|
|
self.print_cards_table(value)
|
||
|
|
return
|
||
|
|
if hasattr(value, "upright") and hasattr(value, "reversed"):
|
||
|
|
self.print_kv_table(
|
||
|
|
"Meaning", {"upright": value.upright, "reversed": value.reversed}
|
||
|
|
)
|
||
|
|
return
|
||
|
|
if isinstance(value, dict):
|
||
|
|
self._render_dict(value)
|
||
|
|
return
|
||
|
|
if isinstance(value, list):
|
||
|
|
self._render_list(value)
|
||
|
|
return
|
||
|
|
if hasattr(value, "__dict__"):
|
||
|
|
self._render_object(value)
|
||
|
|
return
|
||
|
|
self.console.print(value)
|
||
|
|
|
||
|
|
def print_kv_table(self, title: str, data: Dict[str, Any]) -> None:
|
||
|
|
table = Table(title=title, show_header=False)
|
||
|
|
table.add_column("Field", style="bold")
|
||
|
|
table.add_column("Value")
|
||
|
|
for key, value in data.items():
|
||
|
|
table.add_row(self._humanize_key(str(key)), "" if value is None else str(value))
|
||
|
|
self.console.print(table)
|
||
|
|
|
||
|
|
def print_rows_table(self, title: str, columns: List[str], rows: List[List[Any]]) -> None:
|
||
|
|
table = Table(title=title, show_header=True, show_lines=True)
|
||
|
|
for col in columns:
|
||
|
|
table.add_column(col)
|
||
|
|
for row in rows:
|
||
|
|
str_row = ["" if v is None else str(v) for v in row]
|
||
|
|
table.add_row(*str_row)
|
||
|
|
self.console.print(table)
|
||
|
|
|
||
|
|
def print_cards_table(self, cards: List[Card]) -> None:
|
||
|
|
table = Table(title="Cards", show_lines=True)
|
||
|
|
table.add_column("#", justify="right")
|
||
|
|
table.add_column("Name")
|
||
|
|
table.add_column("Arcana")
|
||
|
|
table.add_column("Suit")
|
||
|
|
table.add_column("Pip/Court")
|
||
|
|
|
||
|
|
for card in cards:
|
||
|
|
suit = getattr(getattr(card, "suit", None), "name", "")
|
||
|
|
pip = str(getattr(card, "pip", "")) if getattr(card, "pip", None) else ""
|
||
|
|
court = getattr(card, "court_rank", "")
|
||
|
|
pip_display = court or pip
|
||
|
|
table.add_row(
|
||
|
|
str(getattr(card, "number", "")),
|
||
|
|
getattr(card, "name", ""),
|
||
|
|
getattr(card, "arcana", ""),
|
||
|
|
suit,
|
||
|
|
pip_display,
|
||
|
|
)
|
||
|
|
|
||
|
|
self.console.print(table)
|
||
|
|
|
||
|
|
# Internal renderers -------------------------------------------------
|
||
|
|
def _render_card(self, card: Card) -> None:
|
||
|
|
suit = getattr(card, "suit", None)
|
||
|
|
lines: List[str] = [f"Arcana: {card.arcana}"]
|
||
|
|
if suit is not None and getattr(suit, "name", None):
|
||
|
|
lines.append(f"Suit: {suit.name}")
|
||
|
|
if getattr(card, "pip", 0):
|
||
|
|
lines.append(f"Pip: {card.pip}")
|
||
|
|
if getattr(card, "court_rank", ""):
|
||
|
|
lines.append(f"Court Rank: {card.court_rank}")
|
||
|
|
meaning = getattr(card, "meaning", None)
|
||
|
|
if meaning is not None:
|
||
|
|
if getattr(meaning, "upright", None):
|
||
|
|
lines.append(f"Upright: {meaning.upright}")
|
||
|
|
if getattr(meaning, "reversed", None):
|
||
|
|
lines.append(f"Reversed: {meaning.reversed}")
|
||
|
|
keywords = getattr(card, "keywords", None)
|
||
|
|
if keywords:
|
||
|
|
lines.append(f"Keywords: {', '.join(keywords)}")
|
||
|
|
reversed_keywords = getattr(card, "reversed_keywords", None)
|
||
|
|
if reversed_keywords:
|
||
|
|
lines.append(f"Reversed Keywords: {', '.join(reversed_keywords)}")
|
||
|
|
guidance = getattr(card, "guidance", None)
|
||
|
|
if guidance:
|
||
|
|
lines.append(f"Guidance: {guidance}")
|
||
|
|
self.console.print(
|
||
|
|
Panel("\n".join(lines), title=f"{card.number}: {card.name}", expand=False)
|
||
|
|
)
|
||
|
|
|
||
|
|
def _render_object(self, obj: Any) -> None:
|
||
|
|
attrs = {k: v for k, v in vars(obj).items() if not k.startswith("_")}
|
||
|
|
display = {k: self._format_value(v) for k, v in attrs.items()}
|
||
|
|
title = getattr(obj, "name", None) or obj.__class__.__name__
|
||
|
|
self.print_kv_table(str(title), display)
|
||
|
|
|
||
|
|
def _render_dict(self, data: Dict[Any, Any], title: str = "Result") -> None:
|
||
|
|
if not data:
|
||
|
|
self.console.print(f"{title}: {{}}")
|
||
|
|
return
|
||
|
|
|
||
|
|
values = list(data.values())
|
||
|
|
|
||
|
|
if all(self._is_scalar(v) for v in values):
|
||
|
|
self.print_kv_table(title, data)
|
||
|
|
return
|
||
|
|
|
||
|
|
if all(isinstance(v, list) for v in values) and values and values[0]:
|
||
|
|
first_item = values[0][0]
|
||
|
|
if hasattr(first_item, "__dict__"):
|
||
|
|
self._render_dict_of_list_objects(data, title)
|
||
|
|
return
|
||
|
|
|
||
|
|
if all(hasattr(v, "__dict__") for v in values):
|
||
|
|
rows = []
|
||
|
|
sample_attrs = self._simple_attrs(values[0])
|
||
|
|
columns = ["Key"] + [self._humanize_key(k) for k in sample_attrs.keys()]
|
||
|
|
for key, obj in data.items():
|
||
|
|
attrs = self._simple_attrs(obj)
|
||
|
|
rows.append([key] + [attrs.get(k, "") for k in sample_attrs.keys()])
|
||
|
|
self.print_rows_table(title, columns, rows)
|
||
|
|
return
|
||
|
|
|
||
|
|
self.print_kv_table(title, {k: self._short_value(v) for k, v in data.items()})
|
||
|
|
|
||
|
|
def _render_list(self, items: List[Any], title: str = "Result") -> None:
|
||
|
|
if not items:
|
||
|
|
self.console.print(f"{title}: []")
|
||
|
|
return
|
||
|
|
|
||
|
|
first = items[0]
|
||
|
|
|
||
|
|
if isinstance(first, Card):
|
||
|
|
self.print_cards_table(items)
|
||
|
|
return
|
||
|
|
|
||
|
|
if hasattr(first, "__dict__"):
|
||
|
|
sample_attrs = self._simple_attrs(first)
|
||
|
|
columns = [self._humanize_key(k) for k in sample_attrs.keys()]
|
||
|
|
rows = []
|
||
|
|
for obj in items:
|
||
|
|
attrs = self._simple_attrs(obj)
|
||
|
|
rows.append([attrs.get(k, "") for k in sample_attrs.keys()])
|
||
|
|
self.print_rows_table(title, columns, rows)
|
||
|
|
return
|
||
|
|
|
||
|
|
if isinstance(first, dict):
|
||
|
|
keys = list(first.keys())
|
||
|
|
columns = [self._humanize_key(k) for k in keys]
|
||
|
|
rows = []
|
||
|
|
for obj in items:
|
||
|
|
rows.append([obj.get(k, "") for k in keys])
|
||
|
|
self.print_rows_table(title, columns, rows)
|
||
|
|
return
|
||
|
|
|
||
|
|
self.print_kv_table(title, {i: self._short_value(v) for i, v in enumerate(items)})
|
||
|
|
|
||
|
|
def _render_dict_of_list_objects(self, data: Dict[Any, List[Any]], title: str) -> None:
|
||
|
|
first_key = next(iter(data))
|
||
|
|
first_list = data[first_key]
|
||
|
|
if not first_list:
|
||
|
|
self.print_kv_table(title, {k: "[]" for k in data.keys()})
|
||
|
|
return
|
||
|
|
sample_attrs = self._simple_attrs(first_list[0])
|
||
|
|
columns = ["Key"] + [self._humanize_key(k) for k in sample_attrs.keys()]
|
||
|
|
rows: List[List[Any]] = []
|
||
|
|
for key, lst in data.items():
|
||
|
|
for obj in lst:
|
||
|
|
attrs = self._simple_attrs(obj)
|
||
|
|
rows.append([key] + [attrs.get(k, "") for k in sample_attrs.keys()])
|
||
|
|
self.print_rows_table(title, columns, rows)
|
||
|
|
|
||
|
|
# Value helpers ------------------------------------------------------
|
||
|
|
@staticmethod
|
||
|
|
def _simple_attrs(obj: Any) -> Dict[str, Any]:
|
||
|
|
return {k: v for k, v in vars(obj).items() if not k.startswith("_")}
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _is_scalar(val: Any) -> bool:
|
||
|
|
return isinstance(val, (str, int, float, bool)) or val is None
|
||
|
|
|
||
|
|
def _format_value(self, val: Any, depth: int = 0) -> str:
|
||
|
|
if isinstance(val, list):
|
||
|
|
if not val:
|
||
|
|
return "[]"
|
||
|
|
lines = []
|
||
|
|
for item in val:
|
||
|
|
lines.append(" " * depth + "- " + self._format_value(item, depth + 1).lstrip())
|
||
|
|
return "\n".join(lines)
|
||
|
|
if isinstance(val, dict):
|
||
|
|
lines = []
|
||
|
|
for k, v in val.items():
|
||
|
|
lines.append(
|
||
|
|
" " * depth
|
||
|
|
+ f"{self._humanize_key(str(k))}: "
|
||
|
|
+ self._format_value(v, depth + 1)
|
||
|
|
)
|
||
|
|
return "\n".join(lines)
|
||
|
|
if hasattr(val, "__dict__"):
|
||
|
|
inner = {k: v for k, v in vars(val).items() if not k.startswith("_")}
|
||
|
|
if not inner:
|
||
|
|
return str(val)
|
||
|
|
parts = [
|
||
|
|
f"{self._humanize_key(str(k))}={self._short_value(v)}" for k, v in inner.items()
|
||
|
|
]
|
||
|
|
name = getattr(val, "name", val.__class__.__name__)
|
||
|
|
return f"{name}: " + "; ".join(parts)
|
||
|
|
return str(val)
|
||
|
|
|
||
|
|
def _short_value(self, val: Any) -> str:
|
||
|
|
if isinstance(val, Card):
|
||
|
|
return f"Card {getattr(val, 'number', '?')}: {getattr(val, 'name', '')}"
|
||
|
|
if isinstance(val, list):
|
||
|
|
if not val:
|
||
|
|
return "[]"
|
||
|
|
preview = ", ".join(self._short_value(v) for v in val[:4])
|
||
|
|
suffix = "..." if len(val) > 4 else ""
|
||
|
|
return f"[{preview}{suffix}]"
|
||
|
|
if isinstance(val, dict):
|
||
|
|
return f"dict({len(val)})"
|
||
|
|
return str(val)
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _humanize_key(key: str) -> str:
|
||
|
|
return key.replace("_", " ")
|
||
|
|
|
||
|
|
|
||
|
|
class PlanetRenderer:
|
||
|
|
"""Type-specific table rendering for planets."""
|
||
|
|
|
||
|
|
def __init__(self, renderer: Renderer) -> None:
|
||
|
|
self.renderer = renderer
|
||
|
|
|
||
|
|
@property
|
||
|
|
def console(self) -> Console:
|
||
|
|
return self.renderer.console
|
||
|
|
|
||
|
|
def render(self, planet: Planet) -> None:
|
||
|
|
base_fields = {
|
||
|
|
"Symbol": planet.symbol,
|
||
|
|
"Element": planet.element,
|
||
|
|
"Ruling Zodiac": self._join_list(planet.ruling_zodiac),
|
||
|
|
"Associated Letters": self._join_list(planet.associated_letters),
|
||
|
|
"Keywords": self._join_list(planet.keywords),
|
||
|
|
"Description": planet.description,
|
||
|
|
}
|
||
|
|
self.renderer.print_kv_table(f"Planet: {planet.name}", self._compact(base_fields))
|
||
|
|
|
||
|
|
if planet.associated_numbers:
|
||
|
|
rows = []
|
||
|
|
for number in planet.associated_numbers:
|
||
|
|
color_label = self._color_label(number.color)
|
||
|
|
rows.append(
|
||
|
|
[
|
||
|
|
getattr(number, "value", ""),
|
||
|
|
getattr(number, "sephera", ""),
|
||
|
|
getattr(number, "element", ""),
|
||
|
|
getattr(number, "compliment", ""),
|
||
|
|
color_label,
|
||
|
|
]
|
||
|
|
)
|
||
|
|
self.renderer.print_rows_table(
|
||
|
|
"Associated Numbers",
|
||
|
|
["Value", "Sephera", "Element", "Compliment", "Color"],
|
||
|
|
rows,
|
||
|
|
)
|
||
|
|
|
||
|
|
if planet.color:
|
||
|
|
color = planet.color
|
||
|
|
color_fields = {
|
||
|
|
"Name": color.name,
|
||
|
|
"Hex": color.hex_value,
|
||
|
|
"Element": color.element,
|
||
|
|
"Scale": color.scale,
|
||
|
|
"Meaning": color.meaning,
|
||
|
|
"Tarot Associations": self._join_list(getattr(color, "tarot_associations", [])),
|
||
|
|
"Description": color.description,
|
||
|
|
}
|
||
|
|
self.renderer.print_kv_table("Planetary Color", self._compact(color_fields))
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _join_list(items: Sequence[Any]) -> str:
|
||
|
|
return ", ".join(str(item) for item in items) if items else ""
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _compact(data: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
return {k: v for k, v in data.items() if v not in (None, "", [], {})}
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _color_label(color: Optional[Color]) -> str:
|
||
|
|
if isinstance(color, Color):
|
||
|
|
hex_part = f" ({color.hex_value})" if getattr(color, "hex_value", "") else ""
|
||
|
|
return f"{color.name}{hex_part}"
|
||
|
|
return ""
|