Skip to content

Commit

Permalink
Fix style in Colab (#200)
Browse files Browse the repository at this point in the history
* Generate the target HTML directly
* Test both the connected and offline mode in tests
  • Loading branch information
mwouts committed Oct 1, 2023
1 parent 862e300 commit 92ca939
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 61 deletions.
7 changes: 7 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
ITables ChangeLog
=================

1.6.1 (2023-10-01)
------------------

**Fixed**
- We have fixed an issue when rendering Pandas style objects in Google Colab ([#199](https://github.com/mwouts/itables/issues/199))


1.6.0 (2023-09-30)
------------------

Expand Down
83 changes: 51 additions & 32 deletions itables/javascript.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
logger = logging.getLogger(__name__)

_OPTIONS_NOT_AVAILABLE_WITH_TO_HTML = {
"tags",
"footer",
"column_filters",
"maxBytes",
Expand Down Expand Up @@ -226,7 +227,15 @@ def replace_value(template, pattern, value):
after making sure that the pattern is found exactly once."""
if sys.version_info >= (3,):
assert isinstance(template, str)
assert template.count(pattern) == 1
count = template.count(pattern)
if not count:
raise ValueError("pattern={} was not found in template".format(pattern))
elif count > 1:
raise ValueError(
"pattern={} was found multiple times ({}) in template".format(
pattern, count
)
)
return template.replace(pattern, value)


Expand Down Expand Up @@ -410,12 +419,6 @@ def to_html_datatable_using_to_html(
# These options are used here, not in DataTable
classes = kwargs.pop("classes")
style = kwargs.pop("style")
tags = kwargs.pop("tags")

if caption is not None:
tags = '{}<caption style="white-space: nowrap; overflow: hidden">{}</caption>'.format(
tags, caption
)

showIndex = kwargs.pop("showIndex")

Expand All @@ -434,36 +437,51 @@ def to_html_datatable_using_to_html(
# Polars DataFrame
showIndex = False

if not showIndex:
try:
df.hide()
except AttributeError:
# Not a Pandas Styler object
pass

if "dom" not in kwargs and _df_fits_in_one_page(df, kwargs):
kwargs["dom"] = "t"

tableId = tableId or str(uuid.uuid4())
tableId = (
tableId
# default UUID in Pandas styler objects has uuid_len=5
or str(uuid.uuid4())[:5]
)
if isinstance(df, pd_style.Styler):
df.set_uuid(tableId)
tableId = "T_" + tableId
table_html = df.to_html()
else:
table_html = df.to_html(table_id=tableId)
if not showIndex:
try:
df = df.hide()
except AttributeError:
pass

if style:
style = 'style="{}"'.format(style)
else:
style = ""
if style:
style = 'style="{}"'.format(style)
else:
style = ""

html_table = replace_value(
table_html,
'<table id="{}">'.format(tableId),
"""<table id="{tableId}" class="{classes}"{style}>{tags}""".format(
tableId=tableId, classes=classes, style=style, tags=tags
),
)
try:
to_html_args = dict(
table_uuid=tableId,
table_attributes="""class="{classes}"{style}""".format(
classes=classes, style=style
),
caption=caption,
)
html_table = df.to_html(**to_html_args)
except TypeError:
if caption is not None:
warnings.warn(
"caption is not supported by Styler.to_html in your version of Pandas"
)
del to_html_args["caption"]
html_table = df.to_html(**to_html_args)
tableId = "T_" + tableId
else:
if caption is not None:
raise NotImplementedError(
"caption is not supported when using df.to_html. "
"Use either Pandas Style, or set use_to_html=False."
)
# NB: style is not available neither
html_table = df.to_html(table_id=tableId, classes=classes)

return html_table_from_template(
html_table,
Expand Down Expand Up @@ -606,5 +624,6 @@ def safe_reset_index(df):

def show(df=None, caption=None, **kwargs):
"""Show a dataframe"""
html = to_html_datatable(df, caption=caption, connected=_CONNECTED, **kwargs)
connected = kwargs.pop("connected", _CONNECTED)
html = to_html_datatable(df, caption=caption, connected=connected, **kwargs)
display(HTML(html))
2 changes: 1 addition & 1 deletion itables/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""ITables' version number"""

__version__ = "1.6.0"
__version__ = "1.6.1"
12 changes: 11 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pandas as pd
import pytest

from itables.sample_dfs import get_dict_of_test_dfs
from itables.sample_dfs import PANDAS_VERSION_MAJOR, get_dict_of_test_dfs


@pytest.fixture(params=list(get_dict_of_test_dfs()))
Expand All @@ -19,3 +19,13 @@ def lengthMenu(request):
if request.param == "2D-array":
return [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]]
return None


@pytest.fixture(params=[False, True])
def connected(request):
return request.param


@pytest.fixture(params=[False, True] if PANDAS_VERSION_MAJOR >= 1 else [False])
def use_to_html(request):
return request.param
24 changes: 23 additions & 1 deletion tests/test_javascript.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,26 @@
from itables.javascript import _df_fits_in_one_page, to_html_datatable
import pytest

from itables.javascript import _df_fits_in_one_page, replace_value, to_html_datatable


def test_replace_value(
template="line1\nline2\nline3\n", pattern="line2", value="new line2"
):
assert replace_value(template, pattern, value) == "line1\nnew line2\nline3\n"


def test_replace_value_not_found(
template="line1\nline2\nline3\n", pattern="line4", value="new line4"
):
with pytest.raises(ValueError, match="not found"):
assert replace_value(template, pattern, value)


def test_replace_value_multiple(
template="line1\nline2\nline2\n", pattern="line2", value="new line2"
):
with pytest.raises(ValueError, match="found multiple times"):
assert replace_value(template, pattern, value)


def test_warn_on_unexpected_types_not_in_html(df):
Expand Down
12 changes: 6 additions & 6 deletions tests/test_json_dumps.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@ def coloredColumnDefs():
]


def test_warning_when_eval_functions_is_missing(df, coloredColumnDefs):
def test_warning_when_eval_functions_is_missing(df, coloredColumnDefs, connected):
with pytest.warns(UserWarning, match="starts with 'function'"):
show(df, columnDefs=coloredColumnDefs)
show(df, connected=connected, columnDefs=coloredColumnDefs)


def test_no_warning_when_eval_functions_is_false(df, coloredColumnDefs):
def test_no_warning_when_eval_functions_is_false(df, coloredColumnDefs, connected):
warnings.simplefilter("error")
show(df, columnDefs=coloredColumnDefs, eval_functions=False)
show(df, connected=connected, columnDefs=coloredColumnDefs, eval_functions=False)


def test_no_warning_when_eval_functions_is_true(df, coloredColumnDefs):
def test_no_warning_when_eval_functions_is_true(df, coloredColumnDefs, connected):
warnings.simplefilter("error")
show(df, columnDefs=coloredColumnDefs, eval_functions=True)
show(df, connected=connected, columnDefs=coloredColumnDefs, eval_functions=True)


@pytest.mark.parametrize("obj", ["a", 1, 1.0, [1.0, "a", {"a": [0, 1]}]])
Expand Down
8 changes: 4 additions & 4 deletions tests/test_polars.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
@pytest.mark.parametrize(
"name,x", [(name, x) for name, x in get_dict_of_test_series(polars=True).items()]
)
def test_show_polars_series(name, x):
to_html_datatable(x)
def test_show_polars_series(name, x, use_to_html):
to_html_datatable(x, use_to_html)


@pytest.mark.parametrize(
"name,df", [(name, df) for name, df in get_dict_of_test_dfs(polars=True).items()]
)
def test_show_polars_df(name, df):
to_html_datatable(df)
def test_show_polars_df(name, df, use_to_html):
to_html_datatable(df, use_to_html)
42 changes: 26 additions & 16 deletions tests/test_sample_dfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,51 +32,61 @@
pytestmark.append(pytest.mark.filterwarnings("ignore::DeprecationWarning"))


def test_get_countries():
def test_get_countries(connected, use_to_html):
df = get_countries()
assert len(df.columns) > 5
assert len(df.index) > 100
show(df)
show(df, connected=connected, use_to_html=use_to_html)


@pytest.mark.skipif(sys.version_info < (3,), reason="fails in Py2")
def test_get_population():
def test_get_population(connected, use_to_html):
x = get_population()
assert len(x) > 30
assert x.max() > 7e9
show(x)
show(x, connected=connected, use_to_html=use_to_html)


def test_get_indicators():
def test_get_indicators(connected, use_to_html):
df = get_indicators()
assert len(df.index) == 500
assert len(df.columns)
show(df)
show(df, connected=connected, use_to_html=use_to_html)


@pytest.mark.skipif(
sys.version_info < (3, 7),
reason="AttributeError: 'Styler' object has no attribute 'to_html'",
)
def test_get_pandas_styler():
def test_get_pandas_styler(connected, use_to_html):
styler = get_pandas_styler()
show(styler)
show(styler, connected=connected, use_to_html=use_to_html)


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


def test_show_test_dfs(df, lengthMenu, monkeypatch):
def test_show_test_dfs(df, connected, use_to_html, lengthMenu, monkeypatch):
if "bigint" in df.columns:
monkeypatch.setattr("itables.options.warn_on_int_to_str_conversion", False)
show(df, **kwargs_remove_none(lengthMenu=lengthMenu))
show(
df,
connected=connected,
use_to_html=use_to_html,
**kwargs_remove_none(lengthMenu=lengthMenu)
)


def test_to_html_datatable(df, lengthMenu, monkeypatch):
def test_to_html_datatable(df, connected, use_to_html, lengthMenu, monkeypatch):
if "bigint" in df.columns:
monkeypatch.setattr("itables.options.warn_on_int_to_str_conversion", False)
to_html_datatable(df, **kwargs_remove_none(lengthMenu=lengthMenu))
to_html_datatable(
df,
connected=connected,
use_to_html=use_to_html,
**kwargs_remove_none(lengthMenu=lengthMenu)
)


def test_ordered_categories():
Expand All @@ -92,16 +102,16 @@ def test_format_column(series_name, series):


@pytest.mark.parametrize("series_name,series", get_dict_of_test_series().items())
def test_show_test_series(series_name, series, monkeypatch):
def test_show_test_series(series_name, series, connected, use_to_html, monkeypatch):
if "bigint" in series_name:
monkeypatch.setattr("itables.options.warn_on_int_to_str_conversion", False)
show(series)
show(series, connected=connected, use_to_html=use_to_html)


def test_show_df_with_duplicate_column_names():
def test_show_df_with_duplicate_column_names(connected, use_to_html):
df = pd.DataFrame({"a": [0], "b": [0.0], "c": ["str"]})
df.columns = ["duplicated_name"] * 3
show(df)
show(df, connected=connected, use_to_html=use_to_html)


@pytest.mark.parametrize("type", COLUMN_TYPES)
Expand Down

0 comments on commit 92ca939

Please sign in to comment.