Skip to content

Commit 92ca939

Browse files
authored
Fix style in Colab (#200)
* Generate the target HTML directly * Test both the connected and offline mode in tests
1 parent 862e300 commit 92ca939

File tree

8 files changed

+129
-61
lines changed

8 files changed

+129
-61
lines changed

docs/changelog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
ITables ChangeLog
22
=================
33

4+
1.6.1 (2023-10-01)
5+
------------------
6+
7+
**Fixed**
8+
- We have fixed an issue when rendering Pandas style objects in Google Colab ([#199](https://github.com/mwouts/itables/issues/199))
9+
10+
411
1.6.0 (2023-09-30)
512
------------------
613

itables/javascript.py

Lines changed: 51 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
logger = logging.getLogger(__name__)
3131

3232
_OPTIONS_NOT_AVAILABLE_WITH_TO_HTML = {
33+
"tags",
3334
"footer",
3435
"column_filters",
3536
"maxBytes",
@@ -226,7 +227,15 @@ def replace_value(template, pattern, value):
226227
after making sure that the pattern is found exactly once."""
227228
if sys.version_info >= (3,):
228229
assert isinstance(template, str)
229-
assert template.count(pattern) == 1
230+
count = template.count(pattern)
231+
if not count:
232+
raise ValueError("pattern={} was not found in template".format(pattern))
233+
elif count > 1:
234+
raise ValueError(
235+
"pattern={} was found multiple times ({}) in template".format(
236+
pattern, count
237+
)
238+
)
230239
return template.replace(pattern, value)
231240

232241

@@ -410,12 +419,6 @@ def to_html_datatable_using_to_html(
410419
# These options are used here, not in DataTable
411420
classes = kwargs.pop("classes")
412421
style = kwargs.pop("style")
413-
tags = kwargs.pop("tags")
414-
415-
if caption is not None:
416-
tags = '{}<caption style="white-space: nowrap; overflow: hidden">{}</caption>'.format(
417-
tags, caption
418-
)
419422

420423
showIndex = kwargs.pop("showIndex")
421424

@@ -434,36 +437,51 @@ def to_html_datatable_using_to_html(
434437
# Polars DataFrame
435438
showIndex = False
436439

437-
if not showIndex:
438-
try:
439-
df.hide()
440-
except AttributeError:
441-
# Not a Pandas Styler object
442-
pass
443-
444440
if "dom" not in kwargs and _df_fits_in_one_page(df, kwargs):
445441
kwargs["dom"] = "t"
446442

447-
tableId = tableId or str(uuid.uuid4())
443+
tableId = (
444+
tableId
445+
# default UUID in Pandas styler objects has uuid_len=5
446+
or str(uuid.uuid4())[:5]
447+
)
448448
if isinstance(df, pd_style.Styler):
449-
df.set_uuid(tableId)
450-
tableId = "T_" + tableId
451-
table_html = df.to_html()
452-
else:
453-
table_html = df.to_html(table_id=tableId)
449+
if not showIndex:
450+
try:
451+
df = df.hide()
452+
except AttributeError:
453+
pass
454454

455-
if style:
456-
style = 'style="{}"'.format(style)
457-
else:
458-
style = ""
455+
if style:
456+
style = 'style="{}"'.format(style)
457+
else:
458+
style = ""
459459

460-
html_table = replace_value(
461-
table_html,
462-
'<table id="{}">'.format(tableId),
463-
"""<table id="{tableId}" class="{classes}"{style}>{tags}""".format(
464-
tableId=tableId, classes=classes, style=style, tags=tags
465-
),
466-
)
460+
try:
461+
to_html_args = dict(
462+
table_uuid=tableId,
463+
table_attributes="""class="{classes}"{style}""".format(
464+
classes=classes, style=style
465+
),
466+
caption=caption,
467+
)
468+
html_table = df.to_html(**to_html_args)
469+
except TypeError:
470+
if caption is not None:
471+
warnings.warn(
472+
"caption is not supported by Styler.to_html in your version of Pandas"
473+
)
474+
del to_html_args["caption"]
475+
html_table = df.to_html(**to_html_args)
476+
tableId = "T_" + tableId
477+
else:
478+
if caption is not None:
479+
raise NotImplementedError(
480+
"caption is not supported when using df.to_html. "
481+
"Use either Pandas Style, or set use_to_html=False."
482+
)
483+
# NB: style is not available neither
484+
html_table = df.to_html(table_id=tableId, classes=classes)
467485

468486
return html_table_from_template(
469487
html_table,
@@ -606,5 +624,6 @@ def safe_reset_index(df):
606624

607625
def show(df=None, caption=None, **kwargs):
608626
"""Show a dataframe"""
609-
html = to_html_datatable(df, caption=caption, connected=_CONNECTED, **kwargs)
627+
connected = kwargs.pop("connected", _CONNECTED)
628+
html = to_html_datatable(df, caption=caption, connected=connected, **kwargs)
610629
display(HTML(html))

itables/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""ITables' version number"""
22

3-
__version__ = "1.6.0"
3+
__version__ = "1.6.1"

tests/conftest.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pandas as pd
22
import pytest
33

4-
from itables.sample_dfs import get_dict_of_test_dfs
4+
from itables.sample_dfs import PANDAS_VERSION_MAJOR, get_dict_of_test_dfs
55

66

77
@pytest.fixture(params=list(get_dict_of_test_dfs()))
@@ -19,3 +19,13 @@ def lengthMenu(request):
1919
if request.param == "2D-array":
2020
return [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]]
2121
return None
22+
23+
24+
@pytest.fixture(params=[False, True])
25+
def connected(request):
26+
return request.param
27+
28+
29+
@pytest.fixture(params=[False, True] if PANDAS_VERSION_MAJOR >= 1 else [False])
30+
def use_to_html(request):
31+
return request.param

tests/test_javascript.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,26 @@
1-
from itables.javascript import _df_fits_in_one_page, to_html_datatable
1+
import pytest
2+
3+
from itables.javascript import _df_fits_in_one_page, replace_value, to_html_datatable
4+
5+
6+
def test_replace_value(
7+
template="line1\nline2\nline3\n", pattern="line2", value="new line2"
8+
):
9+
assert replace_value(template, pattern, value) == "line1\nnew line2\nline3\n"
10+
11+
12+
def test_replace_value_not_found(
13+
template="line1\nline2\nline3\n", pattern="line4", value="new line4"
14+
):
15+
with pytest.raises(ValueError, match="not found"):
16+
assert replace_value(template, pattern, value)
17+
18+
19+
def test_replace_value_multiple(
20+
template="line1\nline2\nline2\n", pattern="line2", value="new line2"
21+
):
22+
with pytest.raises(ValueError, match="found multiple times"):
23+
assert replace_value(template, pattern, value)
224

325

426
def test_warn_on_unexpected_types_not_in_html(df):

tests/test_json_dumps.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,19 @@ def coloredColumnDefs():
2727
]
2828

2929

30-
def test_warning_when_eval_functions_is_missing(df, coloredColumnDefs):
30+
def test_warning_when_eval_functions_is_missing(df, coloredColumnDefs, connected):
3131
with pytest.warns(UserWarning, match="starts with 'function'"):
32-
show(df, columnDefs=coloredColumnDefs)
32+
show(df, connected=connected, columnDefs=coloredColumnDefs)
3333

3434

35-
def test_no_warning_when_eval_functions_is_false(df, coloredColumnDefs):
35+
def test_no_warning_when_eval_functions_is_false(df, coloredColumnDefs, connected):
3636
warnings.simplefilter("error")
37-
show(df, columnDefs=coloredColumnDefs, eval_functions=False)
37+
show(df, connected=connected, columnDefs=coloredColumnDefs, eval_functions=False)
3838

3939

40-
def test_no_warning_when_eval_functions_is_true(df, coloredColumnDefs):
40+
def test_no_warning_when_eval_functions_is_true(df, coloredColumnDefs, connected):
4141
warnings.simplefilter("error")
42-
show(df, columnDefs=coloredColumnDefs, eval_functions=True)
42+
show(df, connected=connected, columnDefs=coloredColumnDefs, eval_functions=True)
4343

4444

4545
@pytest.mark.parametrize("obj", ["a", 1, 1.0, [1.0, "a", {"a": [0, 1]}]])

tests/test_polars.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
@pytest.mark.parametrize(
1313
"name,x", [(name, x) for name, x in get_dict_of_test_series(polars=True).items()]
1414
)
15-
def test_show_polars_series(name, x):
16-
to_html_datatable(x)
15+
def test_show_polars_series(name, x, use_to_html):
16+
to_html_datatable(x, use_to_html)
1717

1818

1919
@pytest.mark.parametrize(
2020
"name,df", [(name, df) for name, df in get_dict_of_test_dfs(polars=True).items()]
2121
)
22-
def test_show_polars_df(name, df):
23-
to_html_datatable(df)
22+
def test_show_polars_df(name, df, use_to_html):
23+
to_html_datatable(df, use_to_html)

tests/test_sample_dfs.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,51 +32,61 @@
3232
pytestmark.append(pytest.mark.filterwarnings("ignore::DeprecationWarning"))
3333

3434

35-
def test_get_countries():
35+
def test_get_countries(connected, use_to_html):
3636
df = get_countries()
3737
assert len(df.columns) > 5
3838
assert len(df.index) > 100
39-
show(df)
39+
show(df, connected=connected, use_to_html=use_to_html)
4040

4141

4242
@pytest.mark.skipif(sys.version_info < (3,), reason="fails in Py2")
43-
def test_get_population():
43+
def test_get_population(connected, use_to_html):
4444
x = get_population()
4545
assert len(x) > 30
4646
assert x.max() > 7e9
47-
show(x)
47+
show(x, connected=connected, use_to_html=use_to_html)
4848

4949

50-
def test_get_indicators():
50+
def test_get_indicators(connected, use_to_html):
5151
df = get_indicators()
5252
assert len(df.index) == 500
5353
assert len(df.columns)
54-
show(df)
54+
show(df, connected=connected, use_to_html=use_to_html)
5555

5656

5757
@pytest.mark.skipif(
5858
sys.version_info < (3, 7),
5959
reason="AttributeError: 'Styler' object has no attribute 'to_html'",
6060
)
61-
def test_get_pandas_styler():
61+
def test_get_pandas_styler(connected, use_to_html):
6262
styler = get_pandas_styler()
63-
show(styler)
63+
show(styler, connected=connected, use_to_html=use_to_html)
6464

6565

6666
def kwargs_remove_none(**kwargs):
6767
return {key: value for key, value in kwargs.items() if value is not None}
6868

6969

70-
def test_show_test_dfs(df, lengthMenu, monkeypatch):
70+
def test_show_test_dfs(df, connected, use_to_html, lengthMenu, monkeypatch):
7171
if "bigint" in df.columns:
7272
monkeypatch.setattr("itables.options.warn_on_int_to_str_conversion", False)
73-
show(df, **kwargs_remove_none(lengthMenu=lengthMenu))
73+
show(
74+
df,
75+
connected=connected,
76+
use_to_html=use_to_html,
77+
**kwargs_remove_none(lengthMenu=lengthMenu)
78+
)
7479

7580

76-
def test_to_html_datatable(df, lengthMenu, monkeypatch):
81+
def test_to_html_datatable(df, connected, use_to_html, lengthMenu, monkeypatch):
7782
if "bigint" in df.columns:
7883
monkeypatch.setattr("itables.options.warn_on_int_to_str_conversion", False)
79-
to_html_datatable(df, **kwargs_remove_none(lengthMenu=lengthMenu))
84+
to_html_datatable(
85+
df,
86+
connected=connected,
87+
use_to_html=use_to_html,
88+
**kwargs_remove_none(lengthMenu=lengthMenu)
89+
)
8090

8191

8292
def test_ordered_categories():
@@ -92,16 +102,16 @@ def test_format_column(series_name, series):
92102

93103

94104
@pytest.mark.parametrize("series_name,series", get_dict_of_test_series().items())
95-
def test_show_test_series(series_name, series, monkeypatch):
105+
def test_show_test_series(series_name, series, connected, use_to_html, monkeypatch):
96106
if "bigint" in series_name:
97107
monkeypatch.setattr("itables.options.warn_on_int_to_str_conversion", False)
98-
show(series)
108+
show(series, connected=connected, use_to_html=use_to_html)
99109

100110

101-
def test_show_df_with_duplicate_column_names():
111+
def test_show_df_with_duplicate_column_names(connected, use_to_html):
102112
df = pd.DataFrame({"a": [0], "b": [0.0], "c": ["str"]})
103113
df.columns = ["duplicated_name"] * 3
104-
show(df)
114+
show(df, connected=connected, use_to_html=use_to_html)
105115

106116

107117
@pytest.mark.parametrize("type", COLUMN_TYPES)

0 commit comments

Comments
 (0)