Coverage for databooks/data_models/rich_helpers.py: 84%
63 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-20 15:04 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-20 15:04 +0200
1"""Rich helpers functions for rich renderables in data models."""
3from html.parser import HTMLParser
4from typing import Any, List, Optional, Tuple
6from rich import box
7from rich.console import Console, ConsoleOptions, RenderResult
8from rich.markdown import Heading, Markdown
9from rich.panel import Panel
10from rich.table import Table
11from rich.text import Text
13HtmlAttr = Tuple[str, Optional[str]]
16class RichHtmlTableError(Exception):
17 """Could not parse HTML table."""
19 def __init__(self, msg: str = "", *args: Any):
20 """Use class docstring as error 'prefix'."""
21 if self.__doc__ is None:
22 raise ValueError("Exception docstring required - used in error message.")
23 super().__init__(" ".join((self.__doc__, msg)), *args)
26class HtmlTable(HTMLParser):
27 """Rich table from HTML string."""
29 def __init__(self, html: str, *args: Any, **kwargs: Any) -> None:
30 """Initialize parser."""
31 super().__init__(*args, **kwargs)
32 self.table = self.thead = self.tbody = self.body = self.th = self.td = False
33 self.headers: List[str] = []
34 self.row: List[str] = []
35 self.rows: List[List[str]] = []
36 self.feed(html)
38 def handle_starttag(self, tag: str, attrs: List[HtmlAttr]) -> None:
39 """Active tags are indicated via instance boolean properties."""
40 if getattr(self, tag, None):
41 raise RichHtmlTableError(f"Already in `{tag}`.")
42 setattr(self, tag, True)
44 def handle_endtag(self, tag: str) -> None:
45 """Write table properties when closing tags."""
46 if not getattr(self, tag):
47 raise RichHtmlTableError(f"Cannot end unopened `{tag}`.")
49 # If we are ending a row, either set a table header or row
50 if tag == "tr":
51 if self.thead:
52 self.headers = self.row
53 if self.tbody:
54 self.rows.append(self.row)
55 self.row = [] # restart row values
56 setattr(self, tag, False)
58 def handle_data(self, data: str) -> None:
59 """Append data depending on active tags."""
60 if self.table and (self.th or self.td):
61 self.row.append(data)
63 def rich(self, **tbl_kwargs: Any) -> Optional[Table]:
64 """Generate `rich` representation of table."""
65 if not self.rows and not self.headers: # HTML is not a table
66 return None
68 _ncols = len(self.rows[0])
69 _headers = [""] * (_ncols - len(self.headers)) + self.headers
70 if any(len(row) != _ncols for row in self.rows):
71 raise RichHtmlTableError(f"Expected all rows to have {_ncols} columns.")
73 _box = tbl_kwargs.pop("box", box.SIMPLE_HEAVY)
74 _row_styles = tbl_kwargs.pop("row_styles", ["on bright_black", ""])
76 table = Table(*_headers, box=_box, row_styles=_row_styles, **tbl_kwargs)
77 for row in self.rows:
78 table.add_row(*row)
79 return table
82class NotebookHeading(Heading):
83 """Preserve the notebook heading layout across Rich versions."""
85 def __rich_console__(
86 self, console: Console, options: ConsoleOptions
87 ) -> RenderResult:
88 """Center headings and draw the original double border around H1."""
89 text = self.text.copy()
90 text.justify = "center"
91 if self.tag == "h1":
92 yield Panel(text, box=box.DOUBLE, style="markdown.h1.border")
93 else:
94 if self.tag == "h2":
95 yield Text("")
96 yield text
99class NotebookMarkdown(Markdown):
100 """Use notebook heading styles without modifying Rich's global renderer."""
102 elements = {**Markdown.elements, "heading_open": NotebookHeading}