diff --git a/docs/changelog.md b/docs/changelog.md index 1e48a374..fba71996 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -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) ------------------ diff --git a/itables/javascript.py b/itables/javascript.py index 999b15b4..e9b2fdc5 100644 --- a/itables/javascript.py +++ b/itables/javascript.py @@ -30,6 +30,7 @@ logger = logging.getLogger(__name__) _OPTIONS_NOT_AVAILABLE_WITH_TO_HTML = { + "tags", "footer", "column_filters", "maxBytes", @@ -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) @@ -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 = '{}{}'.format( - tags, caption - ) showIndex = kwargs.pop("showIndex") @@ -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, - ''.format(tableId), - """
{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, @@ -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)) diff --git a/itables/version.py b/itables/version.py index c99b4ffe..acd2cad8 100644 --- a/itables/version.py +++ b/itables/version.py @@ -1,3 +1,3 @@ """ITables' version number""" -__version__ = "1.6.0" +__version__ = "1.6.1" diff --git a/tests/conftest.py b/tests/conftest.py index 77811b2d..d5abdc24 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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())) @@ -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 diff --git a/tests/test_javascript.py b/tests/test_javascript.py index d73205c0..9e45108f 100644 --- a/tests/test_javascript.py +++ b/tests/test_javascript.py @@ -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): diff --git a/tests/test_json_dumps.py b/tests/test_json_dumps.py index 937f6a13..a1cc286b 100644 --- a/tests/test_json_dumps.py +++ b/tests/test_json_dumps.py @@ -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]}]]) diff --git a/tests/test_polars.py b/tests/test_polars.py index c8fa5fa6..19d40e68 100644 --- a/tests/test_polars.py +++ b/tests/test_polars.py @@ -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) diff --git a/tests/test_sample_dfs.py b/tests/test_sample_dfs.py index b31a3f12..7001f752 100644 --- a/tests/test_sample_dfs.py +++ b/tests/test_sample_dfs.py @@ -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(): @@ -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)