table3 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0 — 2026-11-02
4
+
5
+ table3 is view3 renamed: the table is how a frame prints in a notebook, as
6
+ R's `print()`, and `view` is kept for a data explorer, as R's `View()`. Its
7
+ versions continue view3's, whose entries are below; this is table3's first
8
+ release on PyPI.
9
+
10
+ ### Changed from view3 0.2.0
11
+
12
+ - `view3.view()` is `table3.show()`, and there is no `view()`. Nothing used
13
+ `view()` yet.
14
+ - The hook is `__table3__`, not `__view3__`.
15
+ - The footer no longer names `n=`: "1–10 of 100 rows (first 100 of 344)".
16
+ A package showing the table as its frames' notebook display sets *n*
17
+ itself.
18
+
19
+ ## view3 0.2.0 — 2026-11-02
20
+
21
+ ### Added
22
+
23
+ - `digits=` chooses how floats print. `digits=6` keeps six significant
24
+ digits, as R's `signif()` does, so small numbers keep their precision
25
+ (`0.000123457`, `1.5e-7`) where the default shows `0.000123`;
26
+ `digits="full"` shows every stored digit. The default, `None`, prints as
27
+ polars' text preview does, as before.
28
+
29
+ ## view3 0.1.0 — 2026-11-02
30
+
31
+ Phase 1 of [the design](docs/design.md).
32
+
33
+ ### Added
34
+
35
+ - `view3.view(data)` shows up to `n` rows in a paged table and returns
36
+ `data`; `view3.html(data)` returns the HTML. Ten rows a page
37
+ (`page_size=`), column pages for wide tables, and a scroll box with pinned
38
+ names and types where output scripts don't run or with `pages=False`.
39
+ - polars DataFrames, LazyFrames and Series, pandas DataFrames and Series, and
40
+ any object with a `__view3__(n)` method.
table3-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rigoberto Leyva Salmeron
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include CHANGELOG.md
2
+ recursive-include docs *.md
3
+ recursive-include tests *.py
table3-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: table3
3
+ Version: 0.3.0
4
+ Summary: Paged, theme-aware tables in notebook output: shared by tidy3, tidy3a and stats3
5
+ Author: Rigoberto Leyva Salmeron
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/rleyvasal/table3
8
+ Keywords: notebooks,jupyter,polars,tables,viewer
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Framework :: IPython
11
+ Classifier: Framework :: Jupyter
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: polars>=1.39
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=7.0; extra == "dev"
19
+ Requires-Dist: ipython>=8.0; extra == "dev"
20
+ Requires-Dist: pandas>=2.1.4; extra == "dev"
21
+ Requires-Dist: pyarrow>=15.0; extra == "dev"
22
+ Dynamic: license-file
23
+
24
+ # table3
25
+
26
+ Paged, theme-aware tables in notebook output, the way R notebooks print a
27
+ data frame. It is the shared table display of
28
+ [tidy3](https://github.com/rleyvasal/tidy3),
29
+ [tidy3a](https://github.com/rleyvasal/tidy3a) and
30
+ [stats3](https://github.com/rleyvasal/stats3), released on its own so each
31
+ can show tables without depending on the others. It needs only polars.
32
+
33
+ ```python
34
+ import polars as pl
35
+ import table3
36
+
37
+ penguins = pl.read_csv(
38
+ "https://raw.githubusercontent.com/allisonhorst/palmerpenguins/main/inst/extdata/penguins.csv",
39
+ null_values="NA",
40
+ )
41
+ table3.show(penguins); # ten rows a page, every row up to n=1000
42
+ table3.show(penguins, page_size=25); # bigger pages
43
+ table3.show(penguins, pages=False, height=300); # one box that scrolls
44
+ table3.show(penguins, digits=4); # four significant digits
45
+ ```
46
+
47
+ Column names and types sit in the header, missing values show as `null`,
48
+ floats print as polars' text preview prints them (or to `digits=`
49
+ significant digits, or `digits="full"`), and long values are cut with the
50
+ full value on hover. Wide tables show as many columns as fit, with
51
+ arrows to the rest. The table follows the notebook's light or dark theme in
52
+ VS Code, JupyterLab and SolveIt.
53
+
54
+ `show()` returns what it was given, so it can sit in the middle of a pipe.
55
+ As a cell's last line the notebook also prints that value; end the line with
56
+ `;` to hide it.
57
+
58
+ ## Your own frames
59
+
60
+ A package makes its frames print this way by returning `table3.html(self)`
61
+ from `_repr_html_`. An object with a `__table3__` method shows without table3
62
+ importing its package:
63
+
64
+ ```python
65
+ def __table3__(self, n):
66
+ return first_n_rows_as_polars, total_rows_or_None, "caption or None"
67
+
68
+ def _repr_html_(self):
69
+ return table3.html(self, n=100)
70
+ ```
71
+
72
+ ## More
73
+
74
+ [docs/design.md](docs/design.md) records the design and what each package
75
+ adds. table3 was first released as view3 0.1 and 0.2, where `show()` was
76
+ `view()`, and its versions continue from them. The name `view` is kept for a
77
+ data explorer, as R's `View()` opens one.
table3-0.3.0/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # table3
2
+
3
+ Paged, theme-aware tables in notebook output, the way R notebooks print a
4
+ data frame. It is the shared table display of
5
+ [tidy3](https://github.com/rleyvasal/tidy3),
6
+ [tidy3a](https://github.com/rleyvasal/tidy3a) and
7
+ [stats3](https://github.com/rleyvasal/stats3), released on its own so each
8
+ can show tables without depending on the others. It needs only polars.
9
+
10
+ ```python
11
+ import polars as pl
12
+ import table3
13
+
14
+ penguins = pl.read_csv(
15
+ "https://raw.githubusercontent.com/allisonhorst/palmerpenguins/main/inst/extdata/penguins.csv",
16
+ null_values="NA",
17
+ )
18
+ table3.show(penguins); # ten rows a page, every row up to n=1000
19
+ table3.show(penguins, page_size=25); # bigger pages
20
+ table3.show(penguins, pages=False, height=300); # one box that scrolls
21
+ table3.show(penguins, digits=4); # four significant digits
22
+ ```
23
+
24
+ Column names and types sit in the header, missing values show as `null`,
25
+ floats print as polars' text preview prints them (or to `digits=`
26
+ significant digits, or `digits="full"`), and long values are cut with the
27
+ full value on hover. Wide tables show as many columns as fit, with
28
+ arrows to the rest. The table follows the notebook's light or dark theme in
29
+ VS Code, JupyterLab and SolveIt.
30
+
31
+ `show()` returns what it was given, so it can sit in the middle of a pipe.
32
+ As a cell's last line the notebook also prints that value; end the line with
33
+ `;` to hide it.
34
+
35
+ ## Your own frames
36
+
37
+ A package makes its frames print this way by returning `table3.html(self)`
38
+ from `_repr_html_`. An object with a `__table3__` method shows without table3
39
+ importing its package:
40
+
41
+ ```python
42
+ def __table3__(self, n):
43
+ return first_n_rows_as_polars, total_rows_or_None, "caption or None"
44
+
45
+ def _repr_html_(self):
46
+ return table3.html(self, n=100)
47
+ ```
48
+
49
+ ## More
50
+
51
+ [docs/design.md](docs/design.md) records the design and what each package
52
+ adds. table3 was first released as view3 0.1 and 0.2, where `show()` was
53
+ `view()`, and its versions continue from them. The name `view` is kept for a
54
+ data explorer, as R's `View()` opens one.
@@ -0,0 +1,123 @@
1
+ # A shared package for notebook tables
2
+
3
+ Status: accepted 2026-11-02. Released first as view3 (0.1.0 phase 1 of §6,
4
+ 0.2.0 `digits=` of §2.1); renamed table3 for 0.3.0, with `show()` and the
5
+ table as tidy3's notebook display (§5).
6
+
7
+ tidy3 previews showed ten rows. To see more, a notebook user had to `pull()`
8
+ a column and call `.to_list()` on the parenthesized pipe, losing column
9
+ names, types and the table. R notebooks print every data frame as a paged
10
+ table (R Markdown's `df_print: paged`, `rmarkdown::paged_table()`): ten rows
11
+ a page, up to `max.print` rows to page through. table3 is that table, with
12
+ tidy3's formatting, for any package's `_repr_html_`. It is its own package
13
+ because tidy3, tidy3a and stats3 all produce tables to look at, and none of
14
+ them should depend on another to do it.
15
+
16
+ The name `view` is kept for a data explorer: in R, `View(df)` opens
17
+ RStudio's viewer, where you search, sort and filter as in a spreadsheet.
18
+ That is not this table, so the package is table3 (what it makes), its
19
+ functions `html()` and `show()` (R's `print()`), and a later `table3.view()`
20
+ can be the explorer. view3 0.1 and 0.2 called `show()` `view()`; nothing
21
+ used it yet, so it is gone rather than deprecated.
22
+
23
+ ## 1. Precedents
24
+
25
+ - R splits printing from the table packages: tibble prints through
26
+ [pillar](https://pillar.r-lib.org), which other packages share, and
27
+ RStudio's paged table lives in rmarkdown (`paged_table()`), not dplyr.
28
+ - tidy3syntax already shares notebook syntax among tidy3, tidy3a, plot3 and
29
+ stats3 the same way: one small package, a version range in each.
30
+ - tidy3a reaches tidy3 through `TidyArray.__tidy__` and never imports it.
31
+ table3 reaches its callers' objects through `__table3__` the same way.
32
+
33
+ ## 2. What it shows
34
+
35
+ - A table of the first *n* rows (default 1,000), names and types in the
36
+ header, as tidy3's notebook table prints them; `null` for missing values;
37
+ floats as polars' text preview prints them (`3.3`, `1.2346e8`), following
38
+ `pl.Config(float_precision=)`, unless `digits=` asks for significant
39
+ digits (§2.1); values longer than 40 characters cut with
40
+ "…", the full value on hover.
41
+ - Ten rows a page (`page_size=`), Previous / page numbers / Next, and as many
42
+ columns as fit the cell with `◂ columns` / `columns ▸` for the rest. Column
43
+ groups are measured once over all *n* rows, so they don't shift between
44
+ pages, and they are regrouped when the cell's width changes.
45
+ - The footer counts rows and columns ("21–30 of 344 rows | 10–12 of 12
46
+ columns") and says when *n* cut the rows ("1–10 of 100 rows (first 100 of
47
+ 52,341)").
48
+
49
+ ### 2.1 Float digits
50
+
51
+ Polars' preview keeps up to six decimal places, so `0.000123456789` shows as
52
+ `0.000123`, and its `float_precision` counts decimal places, which turns
53
+ small numbers into `0.00`. `digits=k` instead keeps *k* significant digits,
54
+ as R's `signif()` and tibble's `pillar.sigfig` count them: `0.000123457`,
55
+ `1.5e-7`, `1.23457e8` for `digits=6`, written as polars writes floats (`2.0`,
56
+ an exponent without `+` or a leading zero). `digits="full"` is polars'
57
+ `fmt_float="full"`: every stored digit, float noise included. Text output
58
+ outside a notebook takes `"full"` but not *k*, since polars prints no
59
+ significant digits and converting the column would print its type as `str`.
60
+
61
+ ## 3. Where it runs
62
+
63
+ - **Inline styles only.** CRAFT republishes remote output through
64
+ `display_pub.publish`, which drops `<style>` blocks; tidy3's `display.py`
65
+ is inline-only for the same reason.
66
+ - **Theme.** Text inherits the notebook's colour; rules, stripes and
67
+ scrollbars are translucent grey, which reads on dark and light. The pinned
68
+ header needs a solid background: SolveIt's `--card` (an HSL triplet, so
69
+ `hsl(var(--card))`) drawn as a gradient over `var(--vscode-editor-background,
70
+ var(--jp-layout-color0, Canvas))`. An undefined `--card` makes only the
71
+ gradient invalid, so the colour under it shows. Checked in SolveIt's dark
72
+ theme on 2026-11-02 (card `rgb(23,23,23)`).
73
+ - **Paging is a script over a plain table.** SolveIt runs output scripts and
74
+ inline handlers, and runs them again when a dialog is reloaded (probed
75
+ 2026-11-02); VS Code and trusted Jupyter notebooks run them too. Where
76
+ scripts don't run, the table stays a box `height` pixels tall (default
77
+ 300) that scrolls both ways under a pinned header. `pages=False` asks for
78
+ that box everywhere.
79
+ - **Size.** Every cell carries its style, about 100 bytes a cell: 1,000 rows
80
+ of 12 columns add about 1.2 MB to a saved notebook. The font is set once
81
+ on the table to halve that.
82
+
83
+ ## 4. Interface
84
+
85
+ ```python
86
+ table3.show(data, *, n=1000, pages=True, page_size=10, height=300, digits=None, caption=None) # shows, returns data
87
+ table3.html(data, *, n=1000, pages=True, page_size=10, height=300, digits=None, caption=None) # the HTML
88
+ ```
89
+
90
+ *data* is a polars DataFrame, LazyFrame or Series (a LazyFrame's first *n*
91
+ rows and row count are collected together), a pandas DataFrame or Series
92
+ (read without importing pandas), or any object with
93
+
94
+ ```python
95
+ def __table3__(self, n: int) -> tuple[polars.DataFrame, int | None, str | None]:
96
+ """The first n rows, the whole row count (None if not counted), and a caption."""
97
+ ```
98
+
99
+ Outside a notebook `show()` prints the rows as text. As a cell's last line
100
+ the notebook also prints what `show()` returns; `;` hides it. A package
101
+ prints its frames as this table by returning `table3.html(self, n=…)` from
102
+ `_repr_html_`.
103
+
104
+ ## 5. Each package's part
105
+
106
+ | Package | Change | Dependency |
107
+ |---|---|---|
108
+ | tidy3 | `TidyFrame._repr_html_` returns `table3.html(self, n=preview_max, page_size=preview_rows, digits=preview_digits)` from `options()`; `TidyFrame.__table3__` from `_preview_parts` (respects `preview_count`), caption `TidyFrame (preview groups=[…])`. No verb: every frame in a cell prints this way | `table3>=0.3,<0.4` |
109
+ | stats3 | Result tables are polars, so `table3.show(table)` works as is; its own result objects may return `table3.html` from `_repr_html_` | optional at first |
110
+ | tidy3a | `TidyArray.__table3__` through `as_table` | extra `tidy3a[view]` (tidy3a has no polars) |
111
+ | plot3, tidy3syntax | none | none |
112
+
113
+ None of them exports `show` or `view` bare, so `from … import *` in any order
114
+ leaves the names alone.
115
+
116
+ ## 6. Phases
117
+
118
+ 1. view3 0.1.0: the prototype's code without tidy3, tests, CI, release.
119
+ 2. tidy3: dependency, `__table3__`, the table as `_repr_html_`, reference docs,
120
+ release.
121
+ 3. stats3 and tidy3a: hooks and docs.
122
+ 4. Later: tidy3's ordinary notebook table formats through table3 (its floats
123
+ print in full today); loading further rows while paging.
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "table3"
7
+ dynamic = ["version"]
8
+ description = "Paged, theme-aware tables in notebook output: shared by tidy3, tidy3a and stats3"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Rigoberto Leyva Salmeron" }]
14
+ keywords = ["notebooks", "jupyter", "polars", "tables", "viewer"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Framework :: IPython",
18
+ "Framework :: Jupyter",
19
+ "Programming Language :: Python :: 3",
20
+ ]
21
+ # Polars formats the values; pandas frames are read without importing pandas.
22
+ # IPython is optional and imported only to show a table in a notebook.
23
+ dependencies = [
24
+ "polars>=1.39",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ # pandas and pyarrow: the pandas input tests.
29
+ dev = ["pytest>=7.0", "ipython>=8.0", "pandas>=2.1.4", "pyarrow>=15.0"]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/rleyvasal/table3"
33
+
34
+ [tool.setuptools.dynamic]
35
+ version = { attr = "table3.__version__.__version__" }
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["src"]
39
+
40
+ [tool.setuptools.package-data]
41
+ table3 = ["py.typed"]
42
+
43
+ [tool.pytest.ini_options]
44
+ testpaths = ["tests"]
45
+ pythonpath = ["src", "tests"]
table3-0.3.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,153 @@
1
+ """Paged, theme-aware tables in notebook output.
2
+
3
+ ``table3.html(data)`` is a table of up to *n* rows shown ten at a time, with
4
+ Previous / page / Next buttons and, for wide data, as many columns as fit
5
+ the cell plus arrows to the rest: what a package returns from its frames'
6
+ ``_repr_html_`` so they print as R notebooks print data frames.
7
+ ``table3.show(data)`` displays the same table and returns *data* unchanged.
8
+
9
+ *data* is a polars DataFrame, LazyFrame or Series, a pandas DataFrame or
10
+ Series, or any object with a ``__table3__`` method::
11
+
12
+ def __table3__(self, n: int) -> tuple[polars.DataFrame, int | None, str | None]:
13
+ # the first n rows, the whole row count (None if not counted),
14
+ # and a caption shown above the table (None for none)
15
+
16
+ so a package's own frames show without table3 importing that package.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any
22
+
23
+ import polars as pl
24
+
25
+ from table3.__version__ import __version__
26
+ from table3._html import table_html
27
+
28
+ __all__ = ["__version__", "html", "show"]
29
+
30
+
31
+ def _from_pandas(df: Any) -> pl.DataFrame:
32
+ try:
33
+ return pl.from_pandas(df)
34
+ except Exception: # no pyarrow, or a dtype polars can't take: show the text
35
+ return pl.DataFrame({str(c): df[c].astype(str).tolist() for c in df.columns})
36
+
37
+
38
+ def _parts(data: Any, n: int) -> tuple[pl.DataFrame, int | None, str | None]:
39
+ if hasattr(type(data), "__table3__"):
40
+ return data.__table3__(n)
41
+ if isinstance(data, pl.Series):
42
+ data = data.to_frame()
43
+ if isinstance(data, pl.LazyFrame):
44
+ head, count = pl.collect_all([data.head(n), data.select(pl.len())])
45
+ return head, int(count.item()), None
46
+ if isinstance(data, pl.DataFrame):
47
+ return data.head(n), data.height, None
48
+ # pandas, duck-typed so table3 never imports it
49
+ if hasattr(data, "to_frame") and hasattr(data, "iloc") and not hasattr(data, "columns"):
50
+ data = data.to_frame()
51
+ if hasattr(data, "columns") and hasattr(data, "iloc"):
52
+ return _from_pandas(data.head(n)), len(data), None
53
+ raise TypeError(
54
+ f"table3 can't show a {type(data).__name__}: pass a polars or pandas "
55
+ "DataFrame or Series, or an object with a __table3__ method"
56
+ )
57
+
58
+
59
+ def _positive_int(value: Any) -> bool:
60
+ return isinstance(value, int) and not isinstance(value, bool) and value > 0
61
+
62
+
63
+ def _check(n: Any, pages: Any, page_size: Any, height: Any, digits: Any) -> None:
64
+ if not isinstance(n, int) or isinstance(n, bool) or n < 0:
65
+ raise ValueError("table3: n must be a non-negative integer")
66
+ if not isinstance(pages, bool):
67
+ raise TypeError("table3: pages must be True or False")
68
+ if not _positive_int(page_size):
69
+ raise ValueError("table3: page_size must be a positive integer")
70
+ if height is not None and not _positive_int(height):
71
+ raise ValueError("table3: height must be a positive integer or None")
72
+ if digits is not None and digits != "full" and not _positive_int(digits):
73
+ raise ValueError('table3: digits must be a positive integer, "full", or None')
74
+
75
+
76
+ def html(
77
+ data: Any,
78
+ *,
79
+ n: int = 1000,
80
+ pages: bool = True,
81
+ page_size: int = 10,
82
+ height: int | None = 300,
83
+ digits: int | str | None = None,
84
+ caption: str | None = None,
85
+ ) -> str:
86
+ """The table's HTML for the first *n* rows of *data*.
87
+
88
+ *page_size* rows show per page. ``pages=False``, or a notebook that
89
+ doesn't run output scripts, shows every row instead in a box *height*
90
+ pixels tall that scrolls; ``height=None`` grows it to fit.
91
+
92
+ Floats print as polars' text preview prints them (up to six decimals,
93
+ following ``pl.Config(float_precision=)``) unless *digits* says
94
+ otherwise: ``digits=6`` keeps six significant digits, as R's
95
+ ``signif()`` does, so small numbers keep their precision (0.000123457);
96
+ ``digits="full"`` shows every stored digit. *caption* replaces the one
97
+ ``__table3__`` gives.
98
+ """
99
+ _check(n, pages, page_size, height, digits)
100
+ df, total, own_caption = _parts(data, n)
101
+ return table_html(
102
+ df,
103
+ total,
104
+ own_caption if caption is None else caption,
105
+ pages=pages,
106
+ page_size=page_size,
107
+ height=height,
108
+ digits=digits,
109
+ )
110
+
111
+
112
+ def _text(data: Any, n: int, digits: int | str | None = None) -> str:
113
+ df, total, caption = _parts(data, n)
114
+ # Polars prints no significant digits; digits=k applies to the table only.
115
+ full = {"fmt_float": "full"} if digits == "full" else {}
116
+ with pl.Config(tbl_rows=max(df.height, 1), tbl_hide_dataframe_shape=True, **full):
117
+ lines = [f"shape: ({'?' if total is None else total}, {df.width})", repr(df)]
118
+ if caption:
119
+ lines.insert(0, caption)
120
+ if total is None or total > df.height:
121
+ lines.append(f"… first {df.height:,} rows")
122
+ return "\n".join(lines)
123
+
124
+
125
+ def show(
126
+ data: Any,
127
+ *,
128
+ n: int = 1000,
129
+ pages: bool = True,
130
+ page_size: int = 10,
131
+ height: int | None = 300,
132
+ digits: int | str | None = None,
133
+ caption: str | None = None,
134
+ ) -> Any:
135
+ """Show up to *n* rows of *data* in a paged table and return *data*.
136
+
137
+ Arguments as in ``html()``. Outside a notebook it prints the rows as
138
+ text. As a cell's last line the notebook also prints *data* itself; end
139
+ the line with ``;`` to hide it.
140
+ """
141
+ _check(n, pages, page_size, height, digits)
142
+ try:
143
+ from IPython import get_ipython
144
+ from IPython.display import HTML, display
145
+ except ImportError:
146
+ get_ipython = None
147
+ if get_ipython is not None and get_ipython() is not None:
148
+ options = {"n": n, "pages": pages, "page_size": page_size, "height": height}
149
+ display(HTML(html(data, digits=digits, caption=caption, **options)))
150
+ else:
151
+ print(_text(data, n, digits))
152
+ return data
153
+
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -0,0 +1,81 @@
1
+ """Cell text: values, types and floats as polars' text preview prints them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html as _html
6
+
7
+ import polars as pl
8
+
9
+ MAX_CHARS = 40 # longer values are cut, with the full value on hover
10
+
11
+
12
+ def escape(v: object) -> str:
13
+ if v is None:
14
+ return "null"
15
+ return _html.escape(str(v))
16
+
17
+
18
+ def cell(v: object) -> str:
19
+ text = "null" if v is None else str(v)
20
+ if len(text) <= MAX_CHARS:
21
+ return escape(v)
22
+ short = _html.escape(text[: MAX_CHARS - 1]) + "…"
23
+ return f"<span title='{_html.escape(text)}'>{short}</span>"
24
+
25
+
26
+ def dtype_short(t: pl.DataType) -> str:
27
+ """Polars-style short dtype name ("i64"); falls back to str(t)."""
28
+ try:
29
+ from polars._plr import dtype_str_repr
30
+
31
+ return dtype_str_repr(t)
32
+ except Exception:
33
+ return str(t)
34
+
35
+
36
+ def significant(v: float, digits: int) -> str:
37
+ """*v* to *digits* significant digits, as R's ``signif()`` keeps them.
38
+
39
+ Written as polars writes floats: "2.0" not "2", "1.5e-7" not "1.5e-07".
40
+ """
41
+ if v != v:
42
+ return "NaN"
43
+ if v in (float("inf"), float("-inf")):
44
+ return "inf" if v > 0 else "-inf"
45
+ text = format(v, f".{digits}g")
46
+ if "e" in text:
47
+ mantissa, exponent = text.split("e")
48
+ return f"{mantissa}e{int(exponent)}"
49
+ return text if "." in text else text + ".0"
50
+
51
+
52
+ def float_text(s: pl.Series, digits: int | str | None = None) -> list[object]:
53
+ """Floats as polars' text preview prints them ("3.3", "1.2346e8").
54
+
55
+ Read off polars' own printer, so ``pl.Config(float_precision=)`` applies.
56
+ *digits* = k keeps k significant digits instead; "full" every stored digit.
57
+ """
58
+ if isinstance(digits, int):
59
+ return [None if v is None else significant(v, digits) for v in s.to_list()]
60
+ full = {"fmt_float": "full"} if digits == "full" else {}
61
+ with pl.Config(
62
+ tbl_formatting="NOTHING",
63
+ tbl_hide_column_names=True,
64
+ tbl_hide_column_data_types=True,
65
+ tbl_hide_dataframe_shape=True,
66
+ tbl_rows=-1,
67
+ tbl_width_chars=10_000,
68
+ **full,
69
+ ):
70
+ lines = [line.strip() for line in repr(s.to_frame()).splitlines()]
71
+ if len(lines) != len(s):
72
+ return s.to_list()
73
+ return [None if v is None else t for v, t in zip(s.to_list(), lines)]
74
+
75
+
76
+ def display_rows(df: pl.DataFrame, digits: int | str | None = None) -> list[tuple]:
77
+ cols = [
78
+ float_text(s, digits) if s.dtype.is_float() else s.to_list()
79
+ for s in df.get_columns()
80
+ ]
81
+ return list(zip(*cols))
@@ -0,0 +1,125 @@
1
+ """The table's HTML: inline styles only.
2
+
3
+ CRAFT republishes remote output through ``display_pub.publish``, which drops
4
+ ``<style>`` blocks, so every element carries its own style (as in tidy3's
5
+ ``display.py``). The pinned header needs a solid background: SolveIt's card
6
+ colour (``--card``, an HSL triplet) is drawn as a gradient over a plain colour
7
+ taken from VS Code, then JupyterLab, then the browser. Where ``--card`` is
8
+ undefined the gradient drops out alone.
9
+
10
+ Paging is a script over this table. Where output scripts don't run, the
11
+ table stays a box of fixed height that scrolls down and sideways, with the
12
+ column names and types pinned at the top.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import html as _html
18
+ import uuid
19
+
20
+ import polars as pl
21
+
22
+ from table3._format import cell, display_rows, dtype_short, escape
23
+ from table3._pager import PAGER_JS
24
+
25
+ MONO = "12px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace"
26
+ RULE = "border-bottom:1px solid rgba(127,127,127,.45)"
27
+ # The font is set once on the table: a cell's style repeats on up to
28
+ # n x columns cells, and the font list is half its length.
29
+ CELL = "font:inherit;padding:3px 10px;text-align:right;white-space:pre;border:0"
30
+ HEAD_BG = (
31
+ "background-color:var(--vscode-editor-background,"
32
+ "var(--jp-layout-color0,Canvas));"
33
+ "background-image:linear-gradient(hsl(var(--card)),hsl(var(--card)))"
34
+ )
35
+ ZEBRA = "background:rgba(127,127,127,.08)"
36
+ SCROLLBAR = "scrollbar-color:rgba(127,127,127,.5) transparent"
37
+
38
+
39
+ def _plural(k: int) -> str:
40
+ return f"{k:,} row" + ("s" if k != 1 else "")
41
+
42
+
43
+ def rows_label(shown: int, total: int | None) -> str:
44
+ """Footer for the scroll box: "35 rows", or how many of how many show."""
45
+ if total is not None and total <= shown:
46
+ return _plural(total)
47
+ of = "more" if total is None else f"{total:,}"
48
+ return f"showing first {shown:,} of {of} rows"
49
+
50
+
51
+ def pages_label(shown: int, total: int | None) -> str:
52
+ """What the pager counts rows of: "344 rows", or the first 100 of more."""
53
+ if total is not None and total <= shown:
54
+ return _plural(shown)
55
+ of = "more" if total is None else f"{total:,}"
56
+ return f"{_plural(shown)} (first {shown:,} of {of})"
57
+
58
+
59
+ def table_html(
60
+ df: pl.DataFrame,
61
+ total: int | None,
62
+ caption: str | None,
63
+ *,
64
+ pages: bool,
65
+ page_size: int,
66
+ height: int | None,
67
+ digits: int | str | None = None,
68
+ ) -> str:
69
+ """*df* is the first rows; *total* the whole row count, None if not counted."""
70
+
71
+ def _tr(cells: list[str], style: str, row_style: str = "") -> str:
72
+ tds = "".join(f"<td style='{style}'>{c}</td>" for c in cells)
73
+ return f"<tr style='{row_style}'>{tds}</tr>"
74
+
75
+ head = _tr([f"<b>{escape(c)}</b>" for c in df.columns], f"{CELL};{RULE}")
76
+ types = _tr(
77
+ [escape(dtype_short(t)) for t in df.dtypes], f"{CELL};opacity:.55;{RULE}"
78
+ )
79
+ body = [
80
+ _tr([cell(v) for v in row], CELL, ZEBRA if i % 2 else "")
81
+ for i, row in enumerate(display_rows(df, digits))
82
+ ]
83
+
84
+ shape = f"shape: ({'?' if total is None else total}, {df.width})"
85
+ box = f"overflow:auto;{SCROLLBAR}"
86
+ if height is not None:
87
+ box = f"max-height:{height}px;{box}"
88
+ small = f"font:{MONO};opacity:.65"
89
+ cap = ""
90
+ if caption:
91
+ cap = (
92
+ f"<div style='font:{MONO};opacity:.85;margin:0 0 2px 0'>"
93
+ f"{_html.escape(caption)}</div>"
94
+ )
95
+
96
+ root = "<div>"
97
+ pager = script = ""
98
+ if pages:
99
+ uid = f"table3-{uuid.uuid4().hex}"
100
+ label = _html.escape(pages_label(df.height, total))
101
+ root = f"<div id='{uid}' data-page-size='{page_size}' data-label='{label}'>"
102
+ pager = (
103
+ f"<div data-pager style='font:{MONO};margin:4px 0 0 0;display:none'>"
104
+ "</div>"
105
+ )
106
+ script = f"<script>{PAGER_JS.replace('__ID__', uid)}</script>"
107
+ return (
108
+ root
109
+ + cap
110
+ + f"<small style='{small}'>{shape}</small>"
111
+ f"<div data-box style='{box}'>"
112
+ "<table style='border-collapse:separate;border-spacing:0;margin:2px 0;"
113
+ f"font:{MONO}'>"
114
+ f"<thead style='position:sticky;top:0;z-index:1;{HEAD_BG}'>"
115
+ + head
116
+ + types
117
+ + "</thead><tbody>"
118
+ + "".join(body)
119
+ + "</tbody></table></div>"
120
+ f"<small data-foot style='{small}'>"
121
+ f"{rows_label(df.height, total)}</small>"
122
+ + pager
123
+ + "</div>"
124
+ + script
125
+ )
@@ -0,0 +1,106 @@
1
+ """The script that pages a table3 table in place."""
2
+
3
+ # Column widths are measured once over all rows, so a column page keeps its
4
+ # columns whichever row page is shown. __ID__ is the table's root element.
5
+ PAGER_JS = """
6
+ (function () {
7
+ var root = document.getElementById("__ID__");
8
+ if (!root || root.dataset.paged) return;
9
+ root.dataset.paged = "1";
10
+ var box = root.querySelector("[data-box]");
11
+ var table = box.querySelector("table");
12
+ var head = [].slice.call(table.tHead.rows);
13
+ var body = [].slice.call(table.tBodies[0].rows);
14
+ var foot = root.querySelector("[data-foot]");
15
+ var pager = root.querySelector("[data-pager]");
16
+ var size = +root.dataset.pageSize, label = root.dataset.label;
17
+ var widths = [].map.call(head[0].cells, function (c) { return c.offsetWidth; });
18
+ var page = 0, cpage = 0, groups = [], seenWidth = -1;
19
+ var BTN = "font:inherit;color:inherit;background:none;cursor:pointer;" +
20
+ "border:1px solid transparent;border-radius:4px;padding:1px 7px;margin:0 1px";
21
+
22
+ function group() {
23
+ var avail = box.clientWidth || Infinity, cur = [], w = 0;
24
+ seenWidth = box.clientWidth;
25
+ groups = [];
26
+ widths.forEach(function (x, j) {
27
+ if (cur.length && w + x > avail) { groups.push(cur); cur = []; w = 0; }
28
+ cur.push(j); w += x;
29
+ });
30
+ groups.push(cur);
31
+ cpage = Math.min(cpage, groups.length - 1);
32
+ }
33
+
34
+ function button(text, on, go, current) {
35
+ var b = document.createElement("button");
36
+ b.type = "button"; b.textContent = text; b.style.cssText = BTN;
37
+ if (current) b.style.borderColor = "rgba(127,127,127,.6)";
38
+ if (!on) { b.disabled = true; b.style.opacity = ".35"; b.style.cursor = "default"; }
39
+ else b.onclick = go;
40
+ pager.appendChild(b);
41
+ }
42
+
43
+ function gap(text) {
44
+ var s = document.createElement("span");
45
+ s.textContent = text || "…"; s.style.cssText = "opacity:.55;padding:0 4px";
46
+ pager.appendChild(s);
47
+ }
48
+
49
+ function pageList(n, cur) {
50
+ if (n <= 7) return Array.from({ length: n }, function (_, i) { return i; });
51
+ var keep = [0, n - 1, cur - 1, cur, cur + 1].filter(function (i) {
52
+ return i >= 0 && i < n;
53
+ });
54
+ return keep.filter(function (v, i) { return keep.indexOf(v) === i; })
55
+ .sort(function (a, b) { return a - b; });
56
+ }
57
+
58
+ function render() {
59
+ var npages = Math.max(1, Math.ceil(body.length / size));
60
+ page = Math.min(page, npages - 1);
61
+ var lo = page * size, hi = Math.min(lo + size, body.length);
62
+ var cols = groups[cpage], keep = {};
63
+ cols.forEach(function (j) { keep[j] = 1; });
64
+ body.forEach(function (tr, i) {
65
+ var on = i >= lo && i < hi;
66
+ tr.style.display = on ? "" : "none";
67
+ if (on) tr.style.background = (i - lo) % 2 ? "rgba(127,127,127,.08)" : "";
68
+ });
69
+ head.concat(body.slice(lo, hi)).forEach(function (tr) {
70
+ [].forEach.call(tr.cells, function (td, j) { td.style.display = keep[j] ? "" : "none"; });
71
+ });
72
+
73
+ var text = body.length > size ? (lo + 1) + "–" + hi + " of " + label : label;
74
+ if (groups.length > 1)
75
+ text += " | " + (cols[0] + 1) + "–" + (cols[cols.length - 1] + 1) +
76
+ " of " + widths.length + " columns";
77
+ foot.textContent = text;
78
+
79
+ pager.textContent = "";
80
+ if (groups.length > 1) {
81
+ button("◂ columns", cpage > 0, function () { cpage--; render(); });
82
+ button("columns ▸", cpage < groups.length - 1, function () { cpage++; render(); });
83
+ if (npages > 1) gap("|");
84
+ }
85
+ if (npages > 1) {
86
+ button("Previous", page > 0, function () { page--; render(); });
87
+ var last = -1;
88
+ pageList(npages, page).forEach(function (p) {
89
+ if (p > last + 1) gap();
90
+ button(String(p + 1), p !== page, function () { page = p; render(); }, p === page);
91
+ last = p;
92
+ });
93
+ button("Next", page < npages - 1, function () { page++; render(); });
94
+ }
95
+ pager.style.display = pager.childNodes.length ? "" : "none";
96
+ }
97
+
98
+ group();
99
+ box.style.maxHeight = "none";
100
+ render();
101
+ if (window.ResizeObserver)
102
+ new ResizeObserver(function () {
103
+ if (box.clientWidth !== seenWidth) { group(); render(); }
104
+ }).observe(box);
105
+ })();
106
+ """
File without changes
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: table3
3
+ Version: 0.3.0
4
+ Summary: Paged, theme-aware tables in notebook output: shared by tidy3, tidy3a and stats3
5
+ Author: Rigoberto Leyva Salmeron
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/rleyvasal/table3
8
+ Keywords: notebooks,jupyter,polars,tables,viewer
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Framework :: IPython
11
+ Classifier: Framework :: Jupyter
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: polars>=1.39
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=7.0; extra == "dev"
19
+ Requires-Dist: ipython>=8.0; extra == "dev"
20
+ Requires-Dist: pandas>=2.1.4; extra == "dev"
21
+ Requires-Dist: pyarrow>=15.0; extra == "dev"
22
+ Dynamic: license-file
23
+
24
+ # table3
25
+
26
+ Paged, theme-aware tables in notebook output, the way R notebooks print a
27
+ data frame. It is the shared table display of
28
+ [tidy3](https://github.com/rleyvasal/tidy3),
29
+ [tidy3a](https://github.com/rleyvasal/tidy3a) and
30
+ [stats3](https://github.com/rleyvasal/stats3), released on its own so each
31
+ can show tables without depending on the others. It needs only polars.
32
+
33
+ ```python
34
+ import polars as pl
35
+ import table3
36
+
37
+ penguins = pl.read_csv(
38
+ "https://raw.githubusercontent.com/allisonhorst/palmerpenguins/main/inst/extdata/penguins.csv",
39
+ null_values="NA",
40
+ )
41
+ table3.show(penguins); # ten rows a page, every row up to n=1000
42
+ table3.show(penguins, page_size=25); # bigger pages
43
+ table3.show(penguins, pages=False, height=300); # one box that scrolls
44
+ table3.show(penguins, digits=4); # four significant digits
45
+ ```
46
+
47
+ Column names and types sit in the header, missing values show as `null`,
48
+ floats print as polars' text preview prints them (or to `digits=`
49
+ significant digits, or `digits="full"`), and long values are cut with the
50
+ full value on hover. Wide tables show as many columns as fit, with
51
+ arrows to the rest. The table follows the notebook's light or dark theme in
52
+ VS Code, JupyterLab and SolveIt.
53
+
54
+ `show()` returns what it was given, so it can sit in the middle of a pipe.
55
+ As a cell's last line the notebook also prints that value; end the line with
56
+ `;` to hide it.
57
+
58
+ ## Your own frames
59
+
60
+ A package makes its frames print this way by returning `table3.html(self)`
61
+ from `_repr_html_`. An object with a `__table3__` method shows without table3
62
+ importing its package:
63
+
64
+ ```python
65
+ def __table3__(self, n):
66
+ return first_n_rows_as_polars, total_rows_or_None, "caption or None"
67
+
68
+ def _repr_html_(self):
69
+ return table3.html(self, n=100)
70
+ ```
71
+
72
+ ## More
73
+
74
+ [docs/design.md](docs/design.md) records the design and what each package
75
+ adds. table3 was first released as view3 0.1 and 0.2, where `show()` was
76
+ `view()`, and its versions continue from them. The name `view` is kept for a
77
+ data explorer, as R's `View()` opens one.
@@ -0,0 +1,18 @@
1
+ CHANGELOG.md
2
+ LICENSE
3
+ MANIFEST.in
4
+ README.md
5
+ pyproject.toml
6
+ docs/design.md
7
+ src/table3/__init__.py
8
+ src/table3/__version__.py
9
+ src/table3/_format.py
10
+ src/table3/_html.py
11
+ src/table3/_pager.py
12
+ src/table3/py.typed
13
+ src/table3.egg-info/PKG-INFO
14
+ src/table3.egg-info/SOURCES.txt
15
+ src/table3.egg-info/dependency_links.txt
16
+ src/table3.egg-info/requires.txt
17
+ src/table3.egg-info/top_level.txt
18
+ tests/test_table3.py
@@ -0,0 +1,7 @@
1
+ polars>=1.39
2
+
3
+ [dev]
4
+ pytest>=7.0
5
+ ipython>=8.0
6
+ pandas>=2.1.4
7
+ pyarrow>=15.0
@@ -0,0 +1 @@
1
+ table3
@@ -0,0 +1,206 @@
1
+ import re
2
+
3
+ import polars as pl
4
+ import pytest
5
+
6
+ import table3
7
+
8
+
9
+ def _body_rows(html: str) -> int:
10
+ return html.split("<tbody>")[1].count("<tr")
11
+
12
+
13
+ @pytest.fixture
14
+ def islands():
15
+ # 29 distinct values, like a distinct() over one column
16
+ return pl.DataFrame({"island": [f"island {i:02d}" for i in range(29)]})
17
+
18
+
19
+ class Frame:
20
+ """A package's own frame: shown through __table3__ without table3 knowing it."""
21
+
22
+ def __init__(self, df, counted=True):
23
+ self.df, self.counted, self.asked = df, counted, []
24
+
25
+ def __table3__(self, n):
26
+ self.asked.append(n)
27
+ return self.df.head(n), self.df.height if self.counted else None, "Frame (view)"
28
+
29
+
30
+ def test_all_rows_shown(islands):
31
+ html = table3.html(islands)
32
+ assert _body_rows(html) == 29
33
+ assert "island 28" in html
34
+ assert "max-height:300px" in html
35
+ assert ">29 rows<" in html
36
+
37
+
38
+ def test_header_and_types_pinned(islands):
39
+ thead = table3.html(islands).split("<thead")[1].split("</thead>")[0]
40
+ assert "position:sticky;top:0" in thead
41
+ assert "<b>island</b>" in thead and ">str<" in thead
42
+
43
+
44
+ def test_row_limit_labelled():
45
+ html = table3.html(pl.DataFrame({"x": range(2500)}), n=1000)
46
+ assert _body_rows(html) == 1000
47
+ assert "showing first 1,000 of 2,500 rows<" in html
48
+ assert "data-label='1,000 rows (first 1,000 of 2,500)'" in html
49
+ assert "shape: (2500, 1)" in html
50
+
51
+
52
+ def test_lazy_frame_counted():
53
+ html = table3.html(pl.LazyFrame({"x": range(50)}), n=10)
54
+ assert _body_rows(html) == 10 and "shape: (50, 1)" in html
55
+
56
+
57
+ def test_table3_hook_gives_rows_count_and_caption():
58
+ frame = Frame(pl.DataFrame({"x": range(50)}), counted=False)
59
+ html = table3.html(frame, n=10)
60
+ assert frame.asked == [10]
61
+ assert ">Frame (view)<" in html
62
+ assert "shape: (?, 1)" in html
63
+ assert "showing first 10 of more rows" in html
64
+ assert "(first 10 of more)" in html
65
+ assert ">Mine<" in table3.html(frame, caption="Mine")
66
+
67
+
68
+ def test_nulls_mixed_types_and_long_strings():
69
+ df = pl.DataFrame(
70
+ {"n": [1, None, 3], "s": ["short", None, "<b>" + "y" * 100], "d": [1.5, 2.0, None]}
71
+ )
72
+ html = table3.html(df)
73
+ assert html.split("<tbody>")[1].count(">null<") == 3
74
+ assert ">i64<" in html and ">f64<" in html
75
+ assert "<b>yyy" not in html # escaped, never live markup
76
+ assert "title='&lt;b&gt;" + "y" * 100 + "'" in html
77
+ assert re.search(r"&lt;b&gt;y{36}…</span>", html)
78
+
79
+
80
+ def test_floats_as_text_preview():
81
+ df = pl.DataFrame(
82
+ {"x": [3.3000000000000003, 1.9015544041450778, None, 123456789.123, 2.0],
83
+ "y": [1, 2, 3, 4, 5]}
84
+ )
85
+ body = table3.html(df).split("<tbody>")[1]
86
+ cells = re.findall(r"<td style='[^']*'>([^<]*)</td>", body)
87
+ assert cells[0::2] == ["3.3", "1.901554", "null", "1.2346e8", "2.0"]
88
+ assert cells[1::2] == ["1", "2", "3", "4", "5"]
89
+ with pl.Config(float_precision=2):
90
+ assert ">1.90<" in table3.html(df)
91
+
92
+
93
+ def _float_cells(html: str) -> list[str]:
94
+ return re.findall(r"<td style='[^']*'>([^<]*)</td>", html.split("<tbody>")[1])
95
+
96
+
97
+ SMALL = pl.DataFrame(
98
+ {"x": [0.000123456789, 1.5e-7, 2.5e-12, 1.9015544041450778, 123456789.123,
99
+ 2.0, -0.5, float("nan"), float("inf"), None]}
100
+ )
101
+
102
+
103
+ def test_digits_significant_keeps_small_numbers():
104
+ assert _float_cells(table3.html(SMALL, digits=6)) == [
105
+ "0.000123457", "1.5e-7", "2.5e-12", "1.90155", "1.23457e8",
106
+ "2.0", "-0.5", "NaN", "inf", "null",
107
+ ]
108
+ assert _float_cells(table3.html(SMALL, digits=2))[:4] == [
109
+ "0.00012", "1.5e-7", "2.5e-12", "1.9",
110
+ ]
111
+
112
+
113
+ def test_digits_default_and_full():
114
+ default = _float_cells(table3.html(SMALL))
115
+ assert default[:2] == ["0.000123", "1.5000e-7"]
116
+ full = _float_cells(table3.html(SMALL, digits="full"))
117
+ assert full[:5] == [
118
+ "0.000123456789", "0.00000015", "0.0000000000025", "1.9015544041450778",
119
+ "123456789.123",
120
+ ]
121
+
122
+
123
+ def test_digits_leave_other_columns_and_float32():
124
+ df = pl.DataFrame({"i": [123456789], "s": ["0.123456789"],
125
+ "f": pl.Series([0.1], dtype=pl.Float32)})
126
+ assert _float_cells(table3.html(df, digits=3)) == ["123456789", "0.123456789", "0.1"]
127
+
128
+
129
+ def test_digits_in_text_output(capsys):
130
+ table3.show(SMALL.head(1), digits="full")
131
+ assert "0.000123456789" in capsys.readouterr().out
132
+ table3.show(SMALL.head(1), digits=3) # polars prints its own floats
133
+ assert "f64" in capsys.readouterr().out
134
+
135
+
136
+ def test_empty():
137
+ html = table3.html(pl.DataFrame({"x": []}, schema={"x": pl.Int64}))
138
+ assert _body_rows(html) == 0
139
+ assert "<b>x</b>" in html and ">0 rows<" in html
140
+
141
+
142
+ def test_wide_table_keeps_every_column():
143
+ html = table3.html(pl.DataFrame({f"c{i}": [i] for i in range(45)}))
144
+ assert [i for i in range(45) if f"<b>c{i}</b>" not in html] == []
145
+ assert "…</td>" not in html
146
+
147
+
148
+ def test_series_and_pandas():
149
+ assert "<b>x</b>" in table3.html(pl.Series("x", [1, 2]))
150
+ pd = pytest.importorskip("pandas")
151
+ df = pd.DataFrame({"a": [1.5, None], "b": ["p", "q"]})
152
+ html = table3.html(df)
153
+ assert "<b>a</b>" in html and ">1.5<" in html and ">null<" in html
154
+ assert "<b>a</b>" in table3.html(df["a"])
155
+
156
+
157
+ def test_unknown_data_refused():
158
+ with pytest.raises(TypeError, match="can't show a list"):
159
+ table3.html([1, 2])
160
+
161
+
162
+ def test_pages_script_targets_its_table(islands):
163
+ html = table3.html(islands, page_size=5)
164
+ uid = re.search(r"<div id='(table3-[0-9a-f]{32})'", html).group(1)
165
+ assert f'document.getElementById("{uid}")' in html
166
+ assert "data-page-size='5'" in html and "data-label='29 rows'" in html
167
+ assert html.count("<script>") == 1 and html.endswith("</script>")
168
+ assert uid not in table3.html(islands) # each output pages its own table
169
+
170
+
171
+ def test_pages_off_is_the_scroll_box(islands):
172
+ html = table3.html(islands, pages=False)
173
+ assert "<script" not in html and "data-pager" not in html
174
+ assert "max-height:300px" in html
175
+
176
+
177
+ def test_show_returns_data_unchanged(capsys):
178
+ df = pl.DataFrame({"x": [3, 1, 2], "y": ["a", "b", None]})
179
+ before = df.clone()
180
+ assert table3.show(df) is df
181
+ assert df.equals(before)
182
+ out = capsys.readouterr().out # text outside a notebook
183
+ assert "shape: (3, 2)" in out and "│ 3" in out
184
+
185
+
186
+ def test_show_text_labels_the_limit(capsys):
187
+ frame = Frame(pl.DataFrame({"x": range(50)}))
188
+ assert table3.show(frame, n=5) is frame
189
+ out = capsys.readouterr().out
190
+ assert out.startswith("Frame (view)\nshape: (50, 1)")
191
+ assert "… first 5 rows" in out
192
+
193
+
194
+ def test_arguments_checked():
195
+ with pytest.raises(ValueError):
196
+ table3.html(pl.DataFrame({"x": [1]}), n=-1)
197
+ with pytest.raises(ValueError):
198
+ table3.show(pl.DataFrame({"x": [1]}), page_size=0)
199
+ with pytest.raises(ValueError):
200
+ table3.show(pl.DataFrame({"x": [1]}), height=0)
201
+ with pytest.raises(TypeError):
202
+ table3.show(pl.DataFrame({"x": [1]}), pages="yes")
203
+ for bad in (0, True, 2.5, "all"):
204
+ with pytest.raises(ValueError, match="digits"):
205
+ table3.html(pl.DataFrame({"x": [1.0]}), digits=bad)
206
+