diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4fc7a2a8..4eb1a3f1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 }} diff --git a/databooks/data_models/cell.py b/databooks/data_models/cell.py index ceb01374..0229ea35 100644 --- a/databooks/data_models/cell.py +++ b/databooks/data_models/cell.py @@ -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 @@ -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__) @@ -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__( diff --git a/databooks/data_models/rich_helpers.py b/databooks/data_models/rich_helpers.py new file mode 100644 index 00000000..604369b5 --- /dev/null +++ b/databooks/data_models/rich_helpers.py @@ -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 diff --git a/docs/images/databooks-diff.gif b/docs/images/databooks-diff.gif index e973625b..dbb3c6e8 100644 Binary files a/docs/images/databooks-diff.gif and b/docs/images/databooks-diff.gif differ diff --git a/docs/images/databooks-show.gif b/docs/images/databooks-show.gif index 256dfc4f..8a26a83e 100644 Binary files a/docs/images/databooks-show.gif and b/docs/images/databooks-show.gif differ diff --git a/tests/test_cli.py b/tests/test_cli.py index 9c4e31aa..befac6b4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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) โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ @@ -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 [ ]: โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ โ”‚ diff --git a/tests/test_data_models/test_rich_helpers.py b/tests/test_data_models/test_rich_helpers.py new file mode 100644 index 00000000..c03ba04c --- /dev/null +++ b/tests/test_data_models/test_rich_helpers.py @@ -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 = [ + "
\n", + "\n", + '\n', + " \n", + ' \n', + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
col0col1col2
00.8494740.7564560.268569
10.5119370.3572240.570879
20.8361160.9282800.946514
30.8031290.5402150.335783
40.0748530.6611680.344527
50.2996960.7824200.970147
60.1599060.5668220.243798
70.8964610.1744060.758376
80.7083240.8951950.769364
90.8607260.3819190.329727
\n", + "
", + ] + 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\ +""" + ) diff --git a/tests/test_tui.py b/tests/test_tui.py index 5ecd69f9..288717bb 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -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) โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— โ”‚ @@ -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: @@ -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]: โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ @@ -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\ """ )