table3 0.4.0__tar.gz → 0.5.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.
- {table3-0.4.0 → table3-0.5.0}/CHANGELOG.md +26 -0
- {table3-0.4.0/src/table3.egg-info → table3-0.5.0}/PKG-INFO +7 -1
- {table3-0.4.0 → table3-0.5.0}/README.md +6 -0
- {table3-0.4.0 → table3-0.5.0}/docs/design.md +7 -3
- {table3-0.4.0 → table3-0.5.0}/docs/explorer.md +23 -4
- {table3-0.4.0 → table3-0.5.0}/pyproject.toml +1 -1
- {table3-0.4.0 → table3-0.5.0}/src/table3/__init__.py +14 -1
- table3-0.5.0/src/table3/__version__.py +1 -0
- {table3-0.4.0 → table3-0.5.0}/src/table3/_explore.py +2 -8
- {table3-0.4.0 → table3-0.5.0}/src/table3/_explorer_js.py +5 -2
- {table3-0.4.0 → table3-0.5.0}/src/table3/_format.py +10 -0
- {table3-0.4.0 → table3-0.5.0}/src/table3/_html.py +16 -7
- {table3-0.4.0 → table3-0.5.0}/src/table3/_pager.py +25 -1
- table3-0.5.0/src/table3/_solveit.py +93 -0
- {table3-0.4.0 → table3-0.5.0/src/table3.egg-info}/PKG-INFO +7 -1
- {table3-0.4.0 → table3-0.5.0}/src/table3.egg-info/SOURCES.txt +3 -0
- {table3-0.4.0 → table3-0.5.0}/tests/test_explorer_browser.py +17 -1
- table3-0.5.0/tests/test_pager_browser.py +88 -0
- table3-0.5.0/tests/test_solveit.py +110 -0
- {table3-0.4.0 → table3-0.5.0}/tests/test_table3.py +37 -6
- table3-0.4.0/src/table3/__version__.py +0 -1
- {table3-0.4.0 → table3-0.5.0}/LICENSE +0 -0
- {table3-0.4.0 → table3-0.5.0}/MANIFEST.in +0 -0
- {table3-0.4.0 → table3-0.5.0}/setup.cfg +0 -0
- {table3-0.4.0 → table3-0.5.0}/src/table3/py.typed +0 -0
- {table3-0.4.0 → table3-0.5.0}/src/table3.egg-info/dependency_links.txt +0 -0
- {table3-0.4.0 → table3-0.5.0}/src/table3.egg-info/requires.txt +0 -0
- {table3-0.4.0 → table3-0.5.0}/src/table3.egg-info/top_level.txt +0 -0
- {table3-0.4.0 → table3-0.5.0}/tests/test_explore.py +0 -0
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 — 2026-11-03
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- In SolveIt, `view()` hides its cell from the AI (SolveIt's red eye), as
|
|
8
|
+
plot3 does for plots, so the explorer's rows don't fill the model's
|
|
9
|
+
context. SolveIt hides the cell's code with its output. `view(hide=False)`
|
|
10
|
+
keeps one cell visible, and `table3.autohide(False)` turns it off for the
|
|
11
|
+
session. `show()` and printed tables are not hidden. Outside SolveIt
|
|
12
|
+
nothing changes.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- A paged table writes only its first page as styled cells, and every row
|
|
17
|
+
as compact JSON its script draws into the other pages: about a quarter
|
|
18
|
+
of the size (a printed 8-column frame of 100 rows, 23 KB instead of
|
|
19
|
+
86 KB), which matters in SolveIt, where a cell's output goes to the AI.
|
|
20
|
+
Where scripts don't run, the first page shows. `pages=False` is
|
|
21
|
+
unchanged.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- The explorer's headers show that they sort before the first click: a
|
|
26
|
+
faint ↕ beside each name, which turns into ▲ or ▼ on the sorted column,
|
|
27
|
+
and "Click to sort by …" on hover.
|
|
28
|
+
|
|
3
29
|
## 0.4.0 — 2026-11-03
|
|
4
30
|
|
|
5
31
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: table3
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Paged, theme-aware tables in notebook output: shared by tidy3, tidy3a and stats3
|
|
5
5
|
Author: Rigoberto Leyva Salmeron
|
|
6
6
|
License-Expression: MIT
|
|
@@ -65,6 +65,12 @@ table3.view(penguins);
|
|
|
65
65
|
table3.view(penguins, n=50_000, height=600); # more rows, a taller grid
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
In SolveIt, which sends every cell's code and output to its AI, `view()`
|
|
69
|
+
hides its cell from the AI (the red eye), so the explorer's rows don't fill
|
|
70
|
+
the model's context; the cell's code is hidden with them. `view(hide=False)`
|
|
71
|
+
keeps a cell visible, and `table3.autohide(False)` stops hiding for the
|
|
72
|
+
session.
|
|
73
|
+
|
|
68
74
|
`show()` and `view()` return what they were given, so they can sit in the
|
|
69
75
|
middle of a pipe.
|
|
70
76
|
As a cell's last line the notebook also prints that value; end the line with
|
|
@@ -40,6 +40,12 @@ table3.view(penguins);
|
|
|
40
40
|
table3.view(penguins, n=50_000, height=600); # more rows, a taller grid
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
In SolveIt, which sends every cell's code and output to its AI, `view()`
|
|
44
|
+
hides its cell from the AI (the red eye), so the explorer's rows don't fill
|
|
45
|
+
the model's context; the cell's code is hidden with them. `view(hide=False)`
|
|
46
|
+
keeps a cell visible, and `table3.autohide(False)` stops hiding for the
|
|
47
|
+
session.
|
|
48
|
+
|
|
43
49
|
`show()` and `view()` return what they were given, so they can sit in the
|
|
44
50
|
middle of a pipe.
|
|
45
51
|
As a cell's last line the notebook also prints that value; end the line with
|
|
@@ -76,9 +76,13 @@ significant digits and converting the column would print its type as `str`.
|
|
|
76
76
|
scripts don't run, the table stays a box `height` pixels tall (default
|
|
77
77
|
300) that scrolls both ways under a pinned header. `pages=False` asks for
|
|
78
78
|
that box everywhere.
|
|
79
|
-
- **Size.**
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
- **Size.** A styled cell is about 100 bytes, and SolveIt sends a cell's
|
|
80
|
+
output to its AI, so a paged table writes only its first page as cells
|
|
81
|
+
and every row as compact JSON, which its script draws into the other
|
|
82
|
+
pages (0.5.0): `penguins`, 8 columns of 100 rows, is 23 KB, not 86 KB.
|
|
83
|
+
Without scripts the first page shows. `pages=False` writes every row as
|
|
84
|
+
cells, about 1.2 MB for 1,000 rows of 12 columns; the font is set once on
|
|
85
|
+
the table to halve that.
|
|
82
86
|
|
|
83
87
|
## 4. Interface
|
|
84
88
|
|
|
@@ -41,13 +41,22 @@ sort and filters on them.
|
|
|
41
41
|
sends each search, sort or filter to polars and gets one screen of rows
|
|
42
42
|
back, so it explores any number of rows lazily. It needs a live kernel and
|
|
43
43
|
shows nothing in a saved notebook, and SolveIt's and CRAFT's support for
|
|
44
|
-
widgets
|
|
45
|
-
probe like the script probe
|
|
44
|
+
widgets was unknown. It would be an optional extra (`table3[explore]`), and a
|
|
45
|
+
probe like the script probe decided whether to build it.
|
|
46
|
+
|
|
47
|
+
Probed 2026-11-03: SolveIt does not draw Jupyter widgets. An anywidget whose
|
|
48
|
+
button sends a click to the kernel printed as its text,
|
|
49
|
+
`<__main__.Probe object at 0x…>`, with no button, so a widget has no kernel
|
|
50
|
+
connection there. The kernel-backed mode is not built: the in-browser
|
|
51
|
+
explorer is the only one, and `n=` is how to explore more rows. A widget
|
|
52
|
+
mode for VS Code and Jupyter alone would split the explorer by notebook, so
|
|
53
|
+
it waits for a need that `n=` can't meet.
|
|
46
54
|
|
|
47
55
|
## 3. Interface
|
|
48
56
|
|
|
49
57
|
```python
|
|
50
|
-
table3.view(data, *, n=10_000, height=500, digits=None, caption=None) # shows, returns data
|
|
58
|
+
table3.view(data, *, n=10_000, height=500, digits=None, caption=None, hide=None) # shows, returns data
|
|
59
|
+
table3.autohide(on=True) # whether view() hides its cell from SolveIt's AI
|
|
51
60
|
```
|
|
52
61
|
|
|
53
62
|
*data* is anything `html()` and `show()` take, `__table3__` objects
|
|
@@ -55,6 +64,15 @@ included. It returns *data* unchanged, like `show()`; `;` hides the
|
|
|
55
64
|
notebook's own print of it. Outside a notebook it prints the first rows as
|
|
56
65
|
`show()` does.
|
|
57
66
|
|
|
67
|
+
In SolveIt, which sends each cell's code and output to its AI, `view()`
|
|
68
|
+
then marks its cell hidden from the AI (`dialoghelper.update_msg(skipped=1)`,
|
|
69
|
+
SolveIt's red eye), as plot3 does for plots: the rows would fill the
|
|
70
|
+
model's context. SolveIt has no flag for an output alone, so the cell's code
|
|
71
|
+
is hidden too; a `view()` cell is usually one line. `show()` and printed
|
|
72
|
+
tables are not hidden, since most analysis cells end in a frame and hiding
|
|
73
|
+
those would hide their code. `hide=False`, or `autohide(False)` for the
|
|
74
|
+
session, keeps cells visible. Added in 0.5.0.
|
|
75
|
+
|
|
58
76
|
tidy3 adds `view()` as a verb and method, as `glimpse()`: `penguins >> view()`,
|
|
59
77
|
`view(penguins)`, `penguins.view()`, passing the frame through. It is the
|
|
60
78
|
only bare `view` in the family; tidy3a and stats3 document `table3.view(x)`.
|
|
@@ -122,5 +140,6 @@ saying the explorer needs output scripts.
|
|
|
122
140
|
1. table3 0.4.0: `table3.view()` in the browser (§2, §4–§6).
|
|
123
141
|
2. tidy3: the `view()` verb and method, reference docs, release.
|
|
124
142
|
3. Probe SolveIt and CRAFT for widgets; if they work and in-browser limits
|
|
125
|
-
bite, a kernel-backed mode as `table3[explore]`.
|
|
143
|
+
bite, a kernel-backed mode as `table3[explore]`. Done 2026-11-03: SolveIt
|
|
144
|
+
does not draw widgets (§2), so there is no kernel-backed mode.
|
|
126
145
|
4. tidy3a and stats3: document `table3.view(x)`.
|
|
@@ -27,7 +27,7 @@ dependencies = [
|
|
|
27
27
|
[project.optional-dependencies]
|
|
28
28
|
# pandas and pyarrow: the pandas input tests.
|
|
29
29
|
dev = ["pytest>=7.0", "ipython>=8.0", "pandas>=2.1.4", "pyarrow>=15.0"]
|
|
30
|
-
# The explorer's
|
|
30
|
+
# The explorer's and the pages' scripts in headless Chromium (tests/test_*_browser.py);
|
|
31
31
|
# also run: python -m playwright install chromium
|
|
32
32
|
browser = ["pytest-playwright>=0.5"]
|
|
33
33
|
|
|
@@ -26,8 +26,10 @@ import polars as pl
|
|
|
26
26
|
|
|
27
27
|
from table3.__version__ import __version__
|
|
28
28
|
from table3._html import table_html
|
|
29
|
+
from table3 import _solveit
|
|
30
|
+
from table3._solveit import autohide
|
|
29
31
|
|
|
30
|
-
__all__ = ["__version__", "html", "show", "view"]
|
|
32
|
+
__all__ = ["__version__", "autohide", "html", "show", "view"]
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
def _from_pandas(df: Any) -> pl.DataFrame:
|
|
@@ -167,6 +169,7 @@ def view(
|
|
|
167
169
|
height: int = 500,
|
|
168
170
|
digits: int | str | None = None,
|
|
169
171
|
caption: str | None = None,
|
|
172
|
+
hide: bool | None = None,
|
|
170
173
|
) -> Any:
|
|
171
174
|
"""Open a data explorer over the first *n* rows of *data*, and return *data*.
|
|
172
175
|
|
|
@@ -178,8 +181,16 @@ def view(
|
|
|
178
181
|
grid's height in pixels; *digits* and *caption* as in ``html()``. Where
|
|
179
182
|
output scripts don't run, it shows the first 100 rows as ``show()``.
|
|
180
183
|
Outside a notebook it prints the first rows.
|
|
184
|
+
|
|
185
|
+
In SolveIt, which sends every cell's code and output to its AI, the
|
|
186
|
+
cell is then hidden from the AI (SolveIt's red eye), so the rows don't
|
|
187
|
+
fill its context. *hide* says whether to, for this call: ``True`` or
|
|
188
|
+
``False``; ``None`` follows ``table3.autohide()``, on by default.
|
|
189
|
+
SolveIt hides the cell's code with its output.
|
|
181
190
|
"""
|
|
182
191
|
_check(n, True, 10, height, digits)
|
|
192
|
+
if hide is not None and not isinstance(hide, bool):
|
|
193
|
+
raise TypeError("table3: hide must be True, False or None")
|
|
183
194
|
if height is None:
|
|
184
195
|
raise ValueError("table3: view() height must be a positive integer")
|
|
185
196
|
from table3._explore import explorer_html
|
|
@@ -191,5 +202,7 @@ def view(
|
|
|
191
202
|
)
|
|
192
203
|
if not _display(page):
|
|
193
204
|
print(_text(data, min(n, 20), digits))
|
|
205
|
+
elif _solveit.AUTOHIDE if hide is None else hide:
|
|
206
|
+
_solveit.hide_calling_cell()
|
|
194
207
|
return data
|
|
195
208
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
|
@@ -4,14 +4,13 @@ plain table for notebooks that don't run output scripts (docs/explorer.md)."""
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import html as _html
|
|
7
|
-
import json
|
|
8
7
|
import math
|
|
9
8
|
import uuid
|
|
10
9
|
|
|
11
10
|
import polars as pl
|
|
12
11
|
|
|
13
12
|
from table3._explorer_js import EXPLORER_JS
|
|
14
|
-
from table3._format import dtype_short, float_text
|
|
13
|
+
from table3._format import dtype_short, float_text, json_for_script
|
|
15
14
|
from table3._html import MONO, table_html
|
|
16
15
|
|
|
17
16
|
CHOICES_MAX = 50 # a text column with at most this many values filters by pick-list
|
|
@@ -84,11 +83,6 @@ def payload(df: pl.DataFrame, total: int | None, n: int, digits: int | str | Non
|
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
|
|
87
|
-
def _json_for_script(obj: dict) -> str:
|
|
88
|
-
# "<" never appears raw, so a value holding "</script>" or "<!--" can't end the block.
|
|
89
|
-
return json.dumps(obj, ensure_ascii=False, allow_nan=False).replace("<", "\\u003c")
|
|
90
|
-
|
|
91
|
-
|
|
92
86
|
def explorer_html(
|
|
93
87
|
df: pl.DataFrame,
|
|
94
88
|
total: int | None,
|
|
@@ -114,7 +108,7 @@ def explorer_html(
|
|
|
114
108
|
f"<div style='font:{MONO};opacity:.85;margin:0 0 4px 0'>"
|
|
115
109
|
f"{_html.escape(caption)}</div>"
|
|
116
110
|
)
|
|
117
|
-
data =
|
|
111
|
+
data = json_for_script(payload(df, total, n, digits))
|
|
118
112
|
return (
|
|
119
113
|
f"<div id='{uid}' data-height='{height}' style='font:{MONO}'>"
|
|
120
114
|
+ cap
|
|
@@ -104,7 +104,7 @@ EXPLORER_JS = r"""
|
|
|
104
104
|
cols.forEach(function (col, c) {
|
|
105
105
|
var td = document.createElement("td");
|
|
106
106
|
td.style.cssText = cellStyle(c) + ";cursor:pointer;user-select:none";
|
|
107
|
-
td.title = "
|
|
107
|
+
td.title = "Click to sort by " + col.name;
|
|
108
108
|
var arrow = document.createElement("span");
|
|
109
109
|
arrow.style.cssText = "opacity:.55";
|
|
110
110
|
var name = document.createElement("b");
|
|
@@ -226,7 +226,10 @@ EXPLORER_JS = r"""
|
|
|
226
226
|
for (var r = 0; r < rows.length; r++) if (passes(r)) order.push(r);
|
|
227
227
|
if (sortCol >= 0) order.sort(compare);
|
|
228
228
|
arrows.forEach(function (a, c) {
|
|
229
|
-
|
|
229
|
+
// Every header shows it sorts: a faint ↕, and ▲ or ▼ on the sorted one.
|
|
230
|
+
var on = c === sortCol;
|
|
231
|
+
a.textContent = !on ? " ↕ " : sortDir > 0 ? " ▲ " : " ▼ ";
|
|
232
|
+
a.style.opacity = on ? ".9" : ".5";
|
|
230
233
|
});
|
|
231
234
|
var label = order.length.toLocaleString("en-US") + " of " +
|
|
232
235
|
rows.length.toLocaleString("en-US") + " rows | " + ncol + " columns";
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import html as _html
|
|
6
|
+
import json
|
|
6
7
|
|
|
7
8
|
import polars as pl
|
|
8
9
|
|
|
@@ -23,6 +24,15 @@ def cell(v: object) -> str:
|
|
|
23
24
|
return f"<span title='{_html.escape(text)}'>{short}</span>"
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
def json_for_script(obj: object) -> str:
|
|
28
|
+
"""*obj* as JSON for a ``<script type="application/json">`` block.
|
|
29
|
+
|
|
30
|
+
"<" never appears raw, so a value holding "</script>" or "<!--" can't
|
|
31
|
+
end the block.
|
|
32
|
+
"""
|
|
33
|
+
return json.dumps(obj, ensure_ascii=False, allow_nan=False).replace("<", "\\u003c")
|
|
34
|
+
|
|
35
|
+
|
|
26
36
|
def dtype_short(t: pl.DataType) -> str:
|
|
27
37
|
"""Polars-style short dtype name ("i64"); falls back to str(t)."""
|
|
28
38
|
try:
|
|
@@ -7,9 +7,12 @@ colour (``--card``, an HSL triplet) is drawn as a gradient over a plain colour
|
|
|
7
7
|
taken from VS Code, then JupyterLab, then the browser. Where ``--card`` is
|
|
8
8
|
undefined the gradient drops out alone.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
A paged table writes only its first page as styled cells, and every row
|
|
11
|
+
as compact JSON beside it, which the paging script turns into the other
|
|
12
|
+
pages: styled cells cost about 100 bytes each, and SolveIt sends a cell's
|
|
13
|
+
output to its AI. Where output scripts don't run, the first page shows in a
|
|
14
|
+
box that scrolls sideways under pinned names and types. ``pages=False``
|
|
15
|
+
writes every row as styled cells, in a box of fixed height that scrolls.
|
|
13
16
|
"""
|
|
14
17
|
|
|
15
18
|
from __future__ import annotations
|
|
@@ -19,7 +22,7 @@ import uuid
|
|
|
19
22
|
|
|
20
23
|
import polars as pl
|
|
21
24
|
|
|
22
|
-
from table3._format import cell, display_rows, dtype_short, escape
|
|
25
|
+
from table3._format import cell, display_rows, dtype_short, escape, json_for_script
|
|
23
26
|
from table3._pager import PAGER_JS
|
|
24
27
|
|
|
25
28
|
MONO = "12px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace"
|
|
@@ -76,9 +79,11 @@ def table_html(
|
|
|
76
79
|
types = _tr(
|
|
77
80
|
[escape(dtype_short(t)) for t in df.dtypes], f"{CELL};opacity:.55;{RULE}"
|
|
78
81
|
)
|
|
82
|
+
rows = display_rows(df, digits)
|
|
83
|
+
written = rows[:page_size] if pages else rows
|
|
79
84
|
body = [
|
|
80
85
|
_tr([cell(v) for v in row], CELL, ZEBRA if i % 2 else "")
|
|
81
|
-
for i, row in enumerate(
|
|
86
|
+
for i, row in enumerate(written)
|
|
82
87
|
]
|
|
83
88
|
|
|
84
89
|
shape = f"shape: ({'?' if total is None else total}, {df.width})"
|
|
@@ -94,7 +99,7 @@ def table_html(
|
|
|
94
99
|
)
|
|
95
100
|
|
|
96
101
|
root = "<div>"
|
|
97
|
-
pager = script = ""
|
|
102
|
+
pager = script = data = ""
|
|
98
103
|
if pages:
|
|
99
104
|
uid = f"table3-{uuid.uuid4().hex}"
|
|
100
105
|
label = _html.escape(pages_label(df.height, total))
|
|
@@ -104,6 +109,9 @@ def table_html(
|
|
|
104
109
|
"</div>"
|
|
105
110
|
)
|
|
106
111
|
script = f"<script>{PAGER_JS.replace('__ID__', uid)}</script>"
|
|
112
|
+
if len(written) < len(rows):
|
|
113
|
+
text = [[None if v is None else str(v) for v in row] for row in rows]
|
|
114
|
+
data = f"<script type='application/json'>{json_for_script(text)}</script>"
|
|
107
115
|
return (
|
|
108
116
|
root
|
|
109
117
|
+ cap
|
|
@@ -118,8 +126,9 @@ def table_html(
|
|
|
118
126
|
+ "".join(body)
|
|
119
127
|
+ "</tbody></table></div>"
|
|
120
128
|
f"<small data-foot style='{small}'>"
|
|
121
|
-
f"{rows_label(
|
|
129
|
+
f"{rows_label(len(written), total)}</small>"
|
|
122
130
|
+ pager
|
|
131
|
+
+ data
|
|
123
132
|
+ "</div>"
|
|
124
133
|
+ script
|
|
125
134
|
)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""The script that pages a table3 table in place."""
|
|
2
2
|
|
|
3
3
|
# Column widths are measured once over all rows, so a column page keeps its
|
|
4
|
-
# columns whichever row page is shown.
|
|
4
|
+
# columns whichever row page is shown. The rows past the first page arrive as
|
|
5
|
+
# JSON and are drawn into the table first. __ID__ is the table's root element.
|
|
5
6
|
PAGER_JS = """
|
|
6
7
|
(function () {
|
|
7
8
|
var root = document.getElementById("__ID__");
|
|
@@ -10,6 +11,29 @@ PAGER_JS = """
|
|
|
10
11
|
var box = root.querySelector("[data-box]");
|
|
11
12
|
var table = box.querySelector("table");
|
|
12
13
|
var head = [].slice.call(table.tHead.rows);
|
|
14
|
+
// Only the first page is written as cells; the rest come as JSON text,
|
|
15
|
+
// drawn here with the first page's cell style ("null", cut at 40 as there).
|
|
16
|
+
var json = root.querySelector("script[type='application/json']");
|
|
17
|
+
if (json) {
|
|
18
|
+
var tb = table.tBodies[0], first = tb.rows[0] && tb.rows[0].cells[0];
|
|
19
|
+
var style = first ? first.getAttribute("style") : "";
|
|
20
|
+
var esc = function (t) {
|
|
21
|
+
return t.replace(/[&<>"']/g, function (ch) {
|
|
22
|
+
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch];
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
var cells = "";
|
|
26
|
+
JSON.parse(json.textContent).forEach(function (row) {
|
|
27
|
+
cells += "<tr>";
|
|
28
|
+
row.forEach(function (v) {
|
|
29
|
+
var t = v === null ? "null" : v, cut = t.length > 40;
|
|
30
|
+
cells += "<td style='" + style + "'" + (cut ? " title='" + esc(t) + "'" : "") + ">" +
|
|
31
|
+
esc(cut ? t.slice(0, 39) + "…" : t) + "</td>";
|
|
32
|
+
});
|
|
33
|
+
cells += "</tr>";
|
|
34
|
+
});
|
|
35
|
+
tb.innerHTML = cells;
|
|
36
|
+
}
|
|
13
37
|
var body = [].slice.call(table.tBodies[0].rows);
|
|
14
38
|
var foot = root.querySelector("[data-foot]");
|
|
15
39
|
var pager = root.querySelector("[data-pager]");
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Hide the calling cell from SolveIt's AI (its red eye), as plot3 does for plots.
|
|
2
|
+
|
|
3
|
+
SolveIt sends every cell's code and output to the model. An explorer's rows
|
|
4
|
+
(about 1 MB for 10,000) would fill that context, so ``view()`` marks its
|
|
5
|
+
cell ``skipped``: SolveIt's "hide from prompt", which hides the cell's code
|
|
6
|
+
with its output, as there is no flag for the output alone. Outside SolveIt
|
|
7
|
+
(no dialoghelper) nothing happens. dialoghelper's calls are async in some
|
|
8
|
+
versions and plain in others, and the cell's id must be resolved while the
|
|
9
|
+
cell still runs, so the call completes before ``view()`` returns.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import inspect
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
AUTOHIDE = True
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def autohide(on: bool = True) -> None:
|
|
21
|
+
"""Whether ``view()`` hides its cell from SolveIt's AI when *hide* isn't given."""
|
|
22
|
+
global AUTOHIDE
|
|
23
|
+
AUTOHIDE = bool(on)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _msg_id() -> str | None:
|
|
27
|
+
"""The running cell's id where SolveIt puts it (``__msg_id``), or None."""
|
|
28
|
+
frame = inspect.currentframe()
|
|
29
|
+
try:
|
|
30
|
+
f = frame.f_back if frame is not None else None
|
|
31
|
+
while f is not None:
|
|
32
|
+
for ns in (f.f_locals, f.f_globals):
|
|
33
|
+
mid = ns.get("__msg_id") if isinstance(ns, dict) else None
|
|
34
|
+
if mid:
|
|
35
|
+
return str(mid)
|
|
36
|
+
f = f.f_back
|
|
37
|
+
finally:
|
|
38
|
+
del frame
|
|
39
|
+
try:
|
|
40
|
+
from IPython import get_ipython
|
|
41
|
+
|
|
42
|
+
ns = getattr(get_ipython(), "user_ns", None) or {}
|
|
43
|
+
mid = ns.get("__msg_id")
|
|
44
|
+
return str(mid) if mid else None
|
|
45
|
+
except Exception:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def _hide(update_msg: Any, mid: str | None) -> None:
|
|
50
|
+
# Without an id, update_msg asks SolveIt for the running cell itself.
|
|
51
|
+
candidates = [mid, mid[1:] if mid.startswith("_") else "_" + mid] if mid else [None]
|
|
52
|
+
error = None
|
|
53
|
+
for cand in candidates:
|
|
54
|
+
try:
|
|
55
|
+
result = update_msg(id=cand, skipped=1) if cand else update_msg(skipped=1)
|
|
56
|
+
if inspect.iscoroutine(result):
|
|
57
|
+
await result
|
|
58
|
+
return
|
|
59
|
+
except Exception as e: # a version that wants the other id form
|
|
60
|
+
error = e
|
|
61
|
+
raise RuntimeError(error)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def hide_calling_cell() -> None:
|
|
65
|
+
"""Mark the running SolveIt cell hidden from the AI; a no-op elsewhere."""
|
|
66
|
+
try:
|
|
67
|
+
from dialoghelper.core import update_msg
|
|
68
|
+
except Exception:
|
|
69
|
+
return
|
|
70
|
+
coro = _hide(update_msg, _msg_id())
|
|
71
|
+
try:
|
|
72
|
+
import asyncio
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
loop = asyncio.get_running_loop()
|
|
76
|
+
except RuntimeError:
|
|
77
|
+
loop = None
|
|
78
|
+
if loop is None:
|
|
79
|
+
asyncio.run(coro)
|
|
80
|
+
return
|
|
81
|
+
try:
|
|
82
|
+
import nest_asyncio
|
|
83
|
+
|
|
84
|
+
nest_asyncio.apply()
|
|
85
|
+
loop.run_until_complete(coro)
|
|
86
|
+
except ImportError:
|
|
87
|
+
import concurrent.futures
|
|
88
|
+
|
|
89
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
90
|
+
pool.submit(asyncio.run, coro).result()
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(f"table3: could not hide this cell from SolveIt's AI ({e}); "
|
|
93
|
+
"view(hide=False) or table3.autohide(False) skips trying")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: table3
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Paged, theme-aware tables in notebook output: shared by tidy3, tidy3a and stats3
|
|
5
5
|
Author: Rigoberto Leyva Salmeron
|
|
6
6
|
License-Expression: MIT
|
|
@@ -65,6 +65,12 @@ table3.view(penguins);
|
|
|
65
65
|
table3.view(penguins, n=50_000, height=600); # more rows, a taller grid
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
In SolveIt, which sends every cell's code and output to its AI, `view()`
|
|
69
|
+
hides its cell from the AI (the red eye), so the explorer's rows don't fill
|
|
70
|
+
the model's context; the cell's code is hidden with them. `view(hide=False)`
|
|
71
|
+
keeps a cell visible, and `table3.autohide(False)` stops hiding for the
|
|
72
|
+
session.
|
|
73
|
+
|
|
68
74
|
`show()` and `view()` return what they were given, so they can sit in the
|
|
69
75
|
middle of a pipe.
|
|
70
76
|
As a cell's last line the notebook also prints that value; end the line with
|
|
@@ -12,6 +12,7 @@ src/table3/_explorer_js.py
|
|
|
12
12
|
src/table3/_format.py
|
|
13
13
|
src/table3/_html.py
|
|
14
14
|
src/table3/_pager.py
|
|
15
|
+
src/table3/_solveit.py
|
|
15
16
|
src/table3/py.typed
|
|
16
17
|
src/table3.egg-info/PKG-INFO
|
|
17
18
|
src/table3.egg-info/SOURCES.txt
|
|
@@ -20,4 +21,6 @@ src/table3.egg-info/requires.txt
|
|
|
20
21
|
src/table3.egg-info/top_level.txt
|
|
21
22
|
tests/test_explore.py
|
|
22
23
|
tests/test_explorer_browser.py
|
|
24
|
+
tests/test_pager_browser.py
|
|
25
|
+
tests/test_solveit.py
|
|
23
26
|
tests/test_table3.py
|
|
@@ -67,7 +67,7 @@ def _wait_status(page, start: str) -> None:
|
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
def _header(page, name: str):
|
|
70
|
-
return page.locator(f"[data-grid] td[title='
|
|
70
|
+
return page.locator(f"[data-grid] td[title='Click to sort by {name}']")
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
random.seed(1)
|
|
@@ -101,6 +101,22 @@ def test_sort_cycles_and_keeps_row_numbers(explore):
|
|
|
101
101
|
assert _rows(page)[:3] == [1, 2, 3]
|
|
102
102
|
|
|
103
103
|
|
|
104
|
+
def _arrows(page) -> list[str]:
|
|
105
|
+
return [t.strip() for t in page.eval_on_selector_all(
|
|
106
|
+
"[data-grid] thead tr:first-child td span", "ss => ss.map(s => s.textContent)")]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_every_header_shows_it_sorts(explore):
|
|
110
|
+
page = explore(FRAME)
|
|
111
|
+
assert _arrows(page) == ["↕"] * 5 # before any click
|
|
112
|
+
_header(page, "species").click()
|
|
113
|
+
assert _arrows(page) == ["↕", "▲", "↕", "↕", "↕"]
|
|
114
|
+
_header(page, "species").click()
|
|
115
|
+
assert _arrows(page)[1] == "▼"
|
|
116
|
+
_header(page, "species").click()
|
|
117
|
+
assert _arrows(page) == ["↕"] * 5
|
|
118
|
+
|
|
119
|
+
|
|
104
120
|
def test_nulls_sort_last_both_ways(explore):
|
|
105
121
|
page = explore(FRAME)
|
|
106
122
|
for _ in range(2):
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""The paged table's script, driven in headless Chromium: rows past the first
|
|
2
|
+
page arrive as JSON and are drawn before paging. Skipped without Playwright."""
|
|
3
|
+
|
|
4
|
+
import polars as pl
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
import table3
|
|
8
|
+
|
|
9
|
+
sync_api = pytest.importorskip("playwright.sync_api")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture(scope="module")
|
|
13
|
+
def browser():
|
|
14
|
+
with sync_api.sync_playwright() as p:
|
|
15
|
+
try:
|
|
16
|
+
b = p.chromium.launch()
|
|
17
|
+
except Exception as e: # no Chromium installed for Playwright
|
|
18
|
+
pytest.skip(f"no headless Chromium: {e}")
|
|
19
|
+
yield b
|
|
20
|
+
b.close()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def show(browser):
|
|
25
|
+
pages = []
|
|
26
|
+
|
|
27
|
+
def _open(df, width=900, **options):
|
|
28
|
+
page = browser.new_page(viewport={"width": width, "height": 700})
|
|
29
|
+
pages.append(page)
|
|
30
|
+
page.set_content(f"<!doctype html><html><body>{table3.html(df, **options)}</body></html>")
|
|
31
|
+
return page
|
|
32
|
+
|
|
33
|
+
yield _open
|
|
34
|
+
for page in pages:
|
|
35
|
+
page.close()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _visible(page) -> list[list[str]]:
|
|
39
|
+
return page.eval_on_selector_all(
|
|
40
|
+
"tbody tr", "trs => trs.filter(t => t.style.display !== 'none')"
|
|
41
|
+
".map(t => [...t.cells].filter(c => c.style.display !== 'none').map(c => c.textContent))")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _foot(page) -> str:
|
|
45
|
+
return page.eval_on_selector("[data-foot]", "f => f.textContent")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _click(page, text: str) -> None:
|
|
49
|
+
page.locator("[data-pager] button", has_text=text).first.click()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
FRAME = pl.DataFrame({"i": range(35), "s": [f"row {k}" for k in range(35)]})
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_pages_drawn_from_json(show):
|
|
56
|
+
page = show(FRAME)
|
|
57
|
+
assert page.eval_on_selector_all("tbody tr", "trs => trs.length") == 35
|
|
58
|
+
assert _foot(page) == "1–10 of 35 rows"
|
|
59
|
+
assert [r[0] for r in _visible(page)] == [str(k) for k in range(10)]
|
|
60
|
+
_click(page, "Next")
|
|
61
|
+
assert _foot(page) == "11–20 of 35 rows"
|
|
62
|
+
assert _visible(page)[0] == ["10", "row 10"]
|
|
63
|
+
_click(page, "4")
|
|
64
|
+
assert [r[0] for r in _visible(page)] == ["30", "31", "32", "33", "34"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_drawn_rows_keep_the_formatting(show):
|
|
68
|
+
df = pl.DataFrame({"f": [1.0] * 10 + [3.3000000000000003, None],
|
|
69
|
+
"s": ["x"] * 10 + ["<b>" + "y" * 60, "ok"]})
|
|
70
|
+
page = show(df)
|
|
71
|
+
_click(page, "Next")
|
|
72
|
+
assert _visible(page) == [["3.3", "<b>" + "y" * 36 + "…"], ["null", "ok"]]
|
|
73
|
+
cut = page.locator("tbody tr").nth(10).locator("td").nth(1)
|
|
74
|
+
assert cut.get_attribute("title") == "<b>" + "y" * 60
|
|
75
|
+
assert cut.get_attribute("style") == page.locator("tbody td").first.get_attribute("style")
|
|
76
|
+
assert page.locator("tbody b").count() == 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_column_pages_over_drawn_rows(show):
|
|
80
|
+
wide = pl.DataFrame({f"column_{c}": [f"value {r}-{c}" for r in range(25)] for c in range(12)})
|
|
81
|
+
page = show(wide, width=600)
|
|
82
|
+
foot = _foot(page)
|
|
83
|
+
assert foot.startswith("1–10 of 25 rows | 1–") and foot.endswith(" of 12 columns")
|
|
84
|
+
shown = len(_visible(page)[0])
|
|
85
|
+
_click(page, "columns ▸")
|
|
86
|
+
assert _visible(page)[0][0] == f"value 0-{shown}"
|
|
87
|
+
_click(page, "Next")
|
|
88
|
+
assert _visible(page)[0][0] == f"value 10-{shown}"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""view() hides its cell from SolveIt's AI, through a stand-in dialoghelper."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import types
|
|
5
|
+
|
|
6
|
+
import polars as pl
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
import table3
|
|
10
|
+
|
|
11
|
+
DF = pl.DataFrame({"x": range(5)})
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Shell:
|
|
15
|
+
def __init__(self, user_ns):
|
|
16
|
+
self.user_ns = user_ns
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def solveit(monkeypatch):
|
|
21
|
+
"""A notebook with SolveIt's dialoghelper: records each update_msg call."""
|
|
22
|
+
import IPython
|
|
23
|
+
import IPython.display
|
|
24
|
+
|
|
25
|
+
calls, user_ns = [], {"__msg_id": "_abc123"}
|
|
26
|
+
|
|
27
|
+
async def update_msg(id=None, **fields):
|
|
28
|
+
calls.append((id, fields))
|
|
29
|
+
|
|
30
|
+
core = types.ModuleType("dialoghelper.core")
|
|
31
|
+
core.update_msg = update_msg
|
|
32
|
+
package = types.ModuleType("dialoghelper")
|
|
33
|
+
package.core = core
|
|
34
|
+
monkeypatch.setitem(sys.modules, "dialoghelper", package)
|
|
35
|
+
monkeypatch.setitem(sys.modules, "dialoghelper.core", core)
|
|
36
|
+
monkeypatch.setattr(IPython, "get_ipython", lambda: Shell(user_ns))
|
|
37
|
+
monkeypatch.setattr(IPython.display, "display", lambda obj: None)
|
|
38
|
+
monkeypatch.setattr(table3._solveit, "AUTOHIDE", True)
|
|
39
|
+
return types.SimpleNamespace(calls=calls, user_ns=user_ns, core=core)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_view_hides_its_cell(solveit):
|
|
43
|
+
assert table3.view(DF) is DF
|
|
44
|
+
assert solveit.calls == [("_abc123", {"skipped": 1})]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_without_an_id_solveit_finds_the_cell(solveit):
|
|
48
|
+
del solveit.user_ns["__msg_id"]
|
|
49
|
+
table3.view(DF)
|
|
50
|
+
assert solveit.calls == [(None, {"skipped": 1})]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_a_plain_update_msg_works_too(solveit):
|
|
54
|
+
solveit.core.update_msg = lambda id=None, **fields: solveit.calls.append((id, fields))
|
|
55
|
+
table3.view(DF)
|
|
56
|
+
assert solveit.calls == [("_abc123", {"skipped": 1})]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_the_other_id_form_is_tried(solveit):
|
|
60
|
+
async def update_msg(id=None, **fields):
|
|
61
|
+
if id.startswith("_"):
|
|
62
|
+
raise KeyError(id)
|
|
63
|
+
solveit.calls.append((id, fields))
|
|
64
|
+
|
|
65
|
+
solveit.core.update_msg = update_msg
|
|
66
|
+
table3.view(DF)
|
|
67
|
+
assert solveit.calls == [("abc123", {"skipped": 1})]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_hide_and_autohide(solveit):
|
|
71
|
+
table3.view(DF, hide=False)
|
|
72
|
+
assert solveit.calls == []
|
|
73
|
+
table3.autohide(False)
|
|
74
|
+
try:
|
|
75
|
+
table3.view(DF)
|
|
76
|
+
assert solveit.calls == []
|
|
77
|
+
table3.view(DF, hide=True)
|
|
78
|
+
assert len(solveit.calls) == 1
|
|
79
|
+
finally:
|
|
80
|
+
table3.autohide(True)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_show_never_hides(solveit):
|
|
84
|
+
table3.show(DF)
|
|
85
|
+
assert solveit.calls == []
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_a_failure_is_reported_not_raised(solveit, capsys):
|
|
89
|
+
async def update_msg(id=None, **fields):
|
|
90
|
+
raise ConnectionError("no SolveIt")
|
|
91
|
+
|
|
92
|
+
solveit.core.update_msg = update_msg
|
|
93
|
+
assert table3.view(DF) is DF
|
|
94
|
+
assert "could not hide this cell" in capsys.readouterr().out
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_nothing_outside_solveit(monkeypatch, capsys):
|
|
98
|
+
import IPython
|
|
99
|
+
import IPython.display
|
|
100
|
+
|
|
101
|
+
monkeypatch.setitem(sys.modules, "dialoghelper", None) # import fails
|
|
102
|
+
monkeypatch.setattr(IPython, "get_ipython", lambda: Shell({}))
|
|
103
|
+
monkeypatch.setattr(IPython.display, "display", lambda obj: None)
|
|
104
|
+
assert table3.view(DF) is DF
|
|
105
|
+
assert capsys.readouterr().out == ""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_hide_checked():
|
|
109
|
+
with pytest.raises(TypeError, match="hide"):
|
|
110
|
+
table3.view(DF, hide="yes")
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import re
|
|
2
3
|
|
|
3
4
|
import polars as pl
|
|
@@ -7,7 +8,14 @@ import table3
|
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
def _body_rows(html: str) -> int:
|
|
10
|
-
|
|
11
|
+
"""Rows written as cells: a paged table's first page, or every row."""
|
|
12
|
+
return html.split("<tbody>")[1].split("</tbody>")[0].count("<tr")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _json_rows(html: str) -> list:
|
|
16
|
+
"""Every row of a paged table, as the JSON its script draws them from."""
|
|
17
|
+
block = re.search(r"<script type='application/json'>(.*?)</script>", html, re.S)
|
|
18
|
+
return json.loads(block.group(1)) if block else []
|
|
11
19
|
|
|
12
20
|
|
|
13
21
|
@pytest.fixture
|
|
@@ -27,14 +35,36 @@ class Frame:
|
|
|
27
35
|
return self.df.head(n), self.df.height if self.counted else None, "Frame (view)"
|
|
28
36
|
|
|
29
37
|
|
|
30
|
-
def
|
|
38
|
+
def test_first_page_as_cells_every_row_as_json(islands):
|
|
31
39
|
html = table3.html(islands)
|
|
32
|
-
assert _body_rows(html) ==
|
|
33
|
-
|
|
40
|
+
assert _body_rows(html) == 10 # the first page, before any script runs
|
|
41
|
+
rows = _json_rows(html)
|
|
42
|
+
assert len(rows) == 29 and rows[28] == ["island 28"]
|
|
34
43
|
assert "max-height:300px" in html
|
|
44
|
+
assert "showing first 10 of 29 rows<" in html # the footer without scripts
|
|
45
|
+
assert "data-label='29 rows'" in html
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_one_page_or_less_needs_no_json(islands):
|
|
49
|
+
html = table3.html(islands, page_size=50)
|
|
50
|
+
assert _body_rows(html) == 29 and _json_rows(html) == []
|
|
35
51
|
assert ">29 rows<" in html
|
|
36
52
|
|
|
37
53
|
|
|
54
|
+
def test_json_is_a_fraction_of_the_cells():
|
|
55
|
+
df = pl.DataFrame({f"c{i}": [i * 1.5] * 100 for i in range(8)})
|
|
56
|
+
paged, cells = table3.html(df), table3.html(df, pages=False)
|
|
57
|
+
assert _body_rows(cells) == 100 and len(_json_rows(paged)) == 100
|
|
58
|
+
assert len(paged) * 3 < len(cells)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_json_rows_are_display_text():
|
|
62
|
+
df = pl.DataFrame({"f": [3.3000000000000003, None], "s": ["</script>", "y" * 50]})
|
|
63
|
+
html = table3.html(df, page_size=1, digits=None)
|
|
64
|
+
# "</script>" in a value would cut the block short if it weren't escaped.
|
|
65
|
+
assert _json_rows(html) == [["3.3", "</script>"], [None, "y" * 50]]
|
|
66
|
+
|
|
67
|
+
|
|
38
68
|
def test_header_and_types_pinned(islands):
|
|
39
69
|
thead = table3.html(islands).split("<thead")[1].split("</thead>")[0]
|
|
40
70
|
assert "position:sticky;top:0" in thead
|
|
@@ -43,8 +73,8 @@ def test_header_and_types_pinned(islands):
|
|
|
43
73
|
|
|
44
74
|
def test_row_limit_labelled():
|
|
45
75
|
html = table3.html(pl.DataFrame({"x": range(2500)}), n=1000)
|
|
46
|
-
assert _body_rows(html) == 1000
|
|
47
|
-
assert "showing first
|
|
76
|
+
assert _body_rows(html) == 10 and len(_json_rows(html)) == 1000
|
|
77
|
+
assert "showing first 10 of 2,500 rows<" in html
|
|
48
78
|
assert "data-label='1,000 rows (first 1,000 of 2,500)'" in html
|
|
49
79
|
assert "shape: (2500, 1)" in html
|
|
50
80
|
|
|
@@ -52,6 +82,7 @@ def test_row_limit_labelled():
|
|
|
52
82
|
def test_lazy_frame_counted():
|
|
53
83
|
html = table3.html(pl.LazyFrame({"x": range(50)}), n=10)
|
|
54
84
|
assert _body_rows(html) == 10 and "shape: (50, 1)" in html
|
|
85
|
+
assert _json_rows(html) == [] # ten rows: one page
|
|
55
86
|
|
|
56
87
|
|
|
57
88
|
def test_table3_hook_gives_rows_count_and_caption():
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|