Skip to content

Commit

Permalink
Merge pull request #59 from datarootsio/add-rich-tables
Browse files Browse the repository at this point in the history
Add rich tables
  • Loading branch information
murilo-cunha committed Nov 11, 2022
2 parents 1d8cdb7 + c9b46a4 commit eeb84c7
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 59 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
Expand Down
32 changes: 16 additions & 16 deletions databooks/data_models/cell.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Data models - Cells and components."""
from __future__ import annotations

from typing import Any, Dict, Iterable, List, Optional, Sequence, Union
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Union

from pydantic import PositiveInt, validator
from rich.console import Console, ConsoleOptions, ConsoleRenderable, RenderResult
Expand All @@ -11,6 +11,7 @@
from rich.text import Text

from databooks.data_models.base import DatabooksBase
from databooks.data_models.rich_helpers import HtmlTable
from databooks.logging import get_logger

logger = get_logger(__file__)
Expand Down Expand Up @@ -145,23 +146,22 @@ class CellDisplayDataOutput(DatabooksBase):
@property
def rich_output(self) -> Sequence[ConsoleRenderable]:
"""Dynamically compute the rich output - also in `CellExecuteResultOutput`."""
mime_func = {
"image/png": None,
"text/html": None,
mime_func: Dict[str, Callable[[str], Optional[ConsoleRenderable]]] = {
"image/png": lambda s: None,
"text/html": lambda s: HtmlTable("".join(s)).rich(),
"text/plain": lambda s: Text("".join(s)),
}
supported = [k for k, v in mime_func.items() if v is not None]
not_supported = [
Text(f"<✨Rich✨ `{mime}` not currently supported 😢>")
for mime in self.data.keys()
if mime not in supported
]
return not_supported + [
next(
mime_func[mime](content) # type: ignore
for mime, content in self.data.items()
if mime in supported
)
_rich = {
mime: mime_func.get(mime, lambda s: None)(content) # try to render element
for mime, content in self.data.items()
}
return [
*[
Text(f"<✨Rich✨ `{mime}` not available 😢>")
for mime, renderable in _rich.items()
if renderable is None
],
next(renderable for renderable in _rich.values() if renderable is not None),
]

def __rich_console__(
Expand Down
64 changes: 64 additions & 0 deletions databooks/data_models/rich_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Rich helpers functions for rich renderables in data models."""
from html.parser import HTMLParser
from typing import Any, List, Optional, Tuple

from rich import box
from rich.table import Table

HtmlAttr = Tuple[str, Optional[str]]


class HtmlTable(HTMLParser):
"""Rich table from HTML string."""

def __init__(self, html: str, *args: Any, **kwargs: Any) -> None:
"""Initialize parser."""
super().__init__(*args, **kwargs)
self.table = self.thead = self.tbody = self.body = self.th = self.td = False
self.headers: List[str] = []
self.row: List[str] = []
self.rows: List[List[str]] = []
self.feed(html)

def handle_starttag(self, tag: str, attrs: List[HtmlAttr]) -> None:
"""Active tags are indicated via instance boolean properties."""
if getattr(self, tag, None):
raise ValueError(f"Already in `{tag}`.")
setattr(self, tag, True)

def handle_endtag(self, tag: str) -> None:
"""Write table properties when closing tags."""
if not getattr(self, tag):
raise ValueError(f"Cannot end unopened `{tag}`.")

# If we are ending a row, either set a table header or row
if tag == "tr":
if self.thead:
self.headers = self.row
if self.tbody:
self.rows.append(self.row)
self.row = [] # restart row values
setattr(self, tag, False)

def handle_data(self, data: str) -> None:
"""Append data depending on active tags."""
if self.table and (self.th or self.td):
self.row.append(data)

def rich(self, **tbl_kwargs: Any) -> Optional[Table]:
"""Generate `rich` representation of table."""
if not self.rows and not self.headers: # HTML is not a table
return None

_ncols = len(self.rows[0])
_headers = [""] * (_ncols - len(self.headers)) + self.headers
if any(len(row) != _ncols for row in self.rows):
raise ValueError(f"Expected all rows to have {_ncols} columns.")

_box = tbl_kwargs.pop("box", box.SIMPLE_HEAVY)
_row_styles = tbl_kwargs.pop("row_styles", ["on bright_black", ""])

table = Table(*_headers, box=_box, row_styles=_row_styles, **tbl_kwargs)
for row in self.rows:
table.add_row(*row)
return table
Binary file modified docs/images/databooks-diff.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/databooks-show.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 17 additions & 14 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,9 @@ def test_show() -> None:
with resources.path("tests.files", "tui-demo.ipynb") as nb_path:
result = runner.invoke(app, ["show", str(nb_path)])
assert result.exit_code == 0
assert result.output == dedent(
"""\
assert (
result.output
== """\
──────────────────────────────── tui-demo.ipynb ────────────────────────────────
Python 3 (ipykernel)
╭──────────────────────────────────────────────────────────────────────────────╮
Expand Down Expand Up @@ -437,18 +438,20 @@ def test_show() -> None:
A dataframe! 🐼
Out [5]:
<✨Rich✨ `text/html` not currently supported 😢>
col0 col1 col2
0 0.849474 0.756456 0.268569
1 0.511937 0.357224 0.570879
2 0.836116 0.928280 0.946514
3 0.803129 0.540215 0.335783
4 0.074853 0.661168 0.344527
5 0.299696 0.782420 0.970147
6 0.159906 0.566822 0.243798
7 0.896461 0.174406 0.758376
8 0.708324 0.895195 0.769364
9 0.860726 0.381919 0.329727
\n\
col0 col1 col2 \n\
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ \n\
0 0.849474 0.756456 0.268569 \n\
1 0.511937 0.357224 0.570879 \n\
2 0.836116 0.928280 0.946514 \n\
3 0.803129 0.540215 0.335783 \n\
4 0.074853 0.661168 0.344527 \n\
5 0.299696 0.782420 0.970147 \n\
6 0.159906 0.566822 0.243798 \n\
7 0.896461 0.174406 0.758376 \n\
8 0.708324 0.895195 0.769364 \n\
9 0.860726 0.381919 0.329727 \n\
\n\
In [ ]:
╭──────────────────────────────────────────────────────────────────────────────╮
│ │
Expand Down
114 changes: 114 additions & 0 deletions tests/test_data_models/test_rich_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from databooks.data_models.rich_helpers import HtmlTable
from tests.test_tui import render


def test_html_table() -> None:
"""HTML can be rendered to a `rich` table."""
html = [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
'<table border="1" class="dataframe">\n',
" <thead>\n",
' <tr style="text-align: right;">\n',
" <th></th>\n",
" <th>col0</th>\n",
" <th>col1</th>\n",
" <th>col2</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>0.849474</td>\n",
" <td>0.756456</td>\n",
" <td>0.268569</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>0.511937</td>\n",
" <td>0.357224</td>\n",
" <td>0.570879</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>0.836116</td>\n",
" <td>0.928280</td>\n",
" <td>0.946514</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>0.803129</td>\n",
" <td>0.540215</td>\n",
" <td>0.335783</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>0.074853</td>\n",
" <td>0.661168</td>\n",
" <td>0.344527</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>0.299696</td>\n",
" <td>0.782420</td>\n",
" <td>0.970147</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>0.159906</td>\n",
" <td>0.566822</td>\n",
" <td>0.243798</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>0.896461</td>\n",
" <td>0.174406</td>\n",
" <td>0.758376</td>\n",
" </tr>\n",
" <tr>\n",
" <th>8</th>\n",
" <td>0.708324</td>\n",
" <td>0.895195</td>\n",
" <td>0.769364</td>\n",
" </tr>\n",
" <tr>\n",
" <th>9</th>\n",
" <td>0.860726</td>\n",
" <td>0.381919</td>\n",
" <td>0.329727</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>",
]
assert (
render(HtmlTable("".join(html)).rich())
== """\
\n\
col0 col1 col2 \n\
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ \n\
0 0.849474 0.756456 0.268569 \n\
1 0.511937 0.357224 0.570879 \n\
2 0.836116 0.928280 0.946514 \n\
3 0.803129 0.540215 0.335783 \n\
4 0.074853 0.661168 0.344527 \n\
5 0.299696 0.782420 0.970147 \n\
6 0.159906 0.566822 0.243798 \n\
7 0.896461 0.174406 0.758376 \n\
8 0.708324 0.895195 0.769364 \n\
9 0.860726 0.381919 0.329727 \n\
\n\
"""
)
58 changes: 30 additions & 28 deletions tests/test_tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
with resources.path("tests.files", "tui-demo.ipynb") as nb_path:
nb = JupyterNotebook.parse_file(nb_path)

rich_nb = dedent(
"""\
rich_nb = """\
Python 3 (ipykernel)
╭────────────────────────────────────────────────╮
│ ╔════════════════════════════════════════════╗ │
Expand Down Expand Up @@ -58,24 +57,25 @@
A dataframe! 🐼
Out [5]:
<✨Rich✨ `text/html` not currently supported 😢>
col0 col1 col2
0 0.849474 0.756456 0.268569
1 0.511937 0.357224 0.570879
2 0.836116 0.928280 0.946514
3 0.803129 0.540215 0.335783
4 0.074853 0.661168 0.344527
5 0.299696 0.782420 0.970147
6 0.159906 0.566822 0.243798
7 0.896461 0.174406 0.758376
8 0.708324 0.895195 0.769364
9 0.860726 0.381919 0.329727
\n\
col0 col1 col2 \n\
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ \n\
0 0.849474 0.756456 0.268569 \n\
1 0.511937 0.357224 0.570879 \n\
2 0.836116 0.928280 0.946514 \n\
3 0.803129 0.540215 0.335783 \n\
4 0.074853 0.661168 0.344527 \n\
5 0.299696 0.782420 0.970147 \n\
6 0.159906 0.566822 0.243798 \n\
7 0.896461 0.174406 0.758376 \n\
8 0.708324 0.895195 0.769364 \n\
9 0.860726 0.381919 0.329727 \n\
\n\
In [ ]:
╭────────────────────────────────────────────────╮
│ │
╰────────────────────────────────────────────────╯
"""
)


def render(obj: ConsoleRenderable, width: int = 50) -> str:
Expand Down Expand Up @@ -129,7 +129,7 @@ def test_code_cell_error() -> None:

def test_code_cell_df() -> None:
"""Prints code cell data frame and has print statement."""
assert render(nb.cells[6]) == dedent(
assert render(nb.cells[6]) == (
"""\
In [5]:
╭────────────────────────────────────────────────╮
Expand All @@ -142,18 +142,20 @@ def test_code_cell_df() -> None:
A dataframe! 🐼
Out [5]:
<✨Rich✨ `text/html` not currently supported 😢>
col0 col1 col2
0 0.849474 0.756456 0.268569
1 0.511937 0.357224 0.570879
2 0.836116 0.928280 0.946514
3 0.803129 0.540215 0.335783
4 0.074853 0.661168 0.344527
5 0.299696 0.782420 0.970147
6 0.159906 0.566822 0.243798
7 0.896461 0.174406 0.758376
8 0.708324 0.895195 0.769364
9 0.860726 0.381919 0.329727
\n\
col0 col1 col2 \n\
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ \n\
0 0.849474 0.756456 0.268569 \n\
1 0.511937 0.357224 0.570879 \n\
2 0.836116 0.928280 0.946514 \n\
3 0.803129 0.540215 0.335783 \n\
4 0.074853 0.661168 0.344527 \n\
5 0.299696 0.782420 0.970147 \n\
6 0.159906 0.566822 0.243798 \n\
7 0.896461 0.174406 0.758376 \n\
8 0.708324 0.895195 0.769364 \n\
9 0.860726 0.381919 0.329727 \n\
\n\
"""
)

Expand Down

0 comments on commit eeb84c7

Please sign in to comment.