From 4799d865767d9ec04e1aee93f98a2b9d97f59b19 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 28 Sep 2020 23:07:44 +0200 Subject: [PATCH 01/17] json on styler --- pandas/io/formats/style.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 1df37da3da8d0..9c4a3d880fc42 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -247,6 +247,39 @@ def to_excel( engine=engine, ) + def to_json(self, **kwargs): + """ + Applies the ``:meth:DataFrame.to_json`` method to the ``Styler`` object + including existing formats. + + Parameters + ---------- + **kwargs + Any additional keyword arguments are passed to ``DataFrame.to_json()``. + Useful to control the output structure of the JSON return. + + Returns + ------- + None or str + + Examples + -------- + >>> df = pd.DataFrame(data=[[1.9999, 1.9999, 1.9999, 1.9999]], + columns=['A', 'B', 'C', 'D']) + >>> df.style.format({ + ... 'A': lambda x: float(f'{x:,.02f}'), + ... 'B': lambda x: int(f'{x:,.00f}'), + ... 'C': '{:,.01f}'.format + ... }).to_json() + {"A":{"0":2.00},"B":{"0":2.0},"C":{"0":"2.0"},"D":{"0":"1.999900"}} + """ + df = self.data.copy() + for r in range(len(df.index)): + for c in range(len(df.columns)): + formatter = self._display_funcs[(r, c)] + df.iloc[r, c] = formatter(self.data.iloc[r, c]) + return df.to_json(**kwargs) + def _translate(self): """ Convert the DataFrame in `self.data` and the attrs from `_build_styles` From a0bcbd1c5a1332b9a158dd8a3346805b5bab6519 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 25 Feb 2021 07:57:42 +0100 Subject: [PATCH 02/17] meth: to_html --- pandas/io/formats/style.py | 79 +++++++++++++++++++++-- pandas/io/formats/templates/html.tpl | 74 ++------------------- pandas/io/formats/templates/html_all.tpl | 70 ++++++++++++++++++++ pandas/io/formats/templates/html_excl.tpl | 46 +++++++++++++ 4 files changed, 195 insertions(+), 74 deletions(-) create mode 100644 pandas/io/formats/templates/html_all.tpl create mode 100644 pandas/io/formats/templates/html_excl.tpl diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 411efda756522..3a1c52f8a75bd 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -29,6 +29,7 @@ from pandas._libs import lib from pandas._typing import ( Axis, + FilePathOrBuffer, FrameOrSeries, FrameOrSeriesUnion, IndexLabel, @@ -50,6 +51,8 @@ from pandas.core.generic import NDFrame from pandas.core.indexes.api import Index +from pandas.io.formats.format import save_to_buffer + jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.") CSSPair = Tuple[str, Union[str, int, float]] @@ -379,8 +382,10 @@ def to_excel( def to_json(self, **kwargs): """ - Applies the ``:meth:DataFrame.to_json`` method to the ``Styler`` object - including existing formats. + Convert a ``Styler`` object to JSON + + Uses the ``:meth:DataFrame.to_json`` method but includes the set + formats applied to ``Styler``. Parameters ---------- @@ -404,12 +409,75 @@ def to_json(self, **kwargs): {"A":{"0":2.00},"B":{"0":2.0},"C":{"0":"2.0"},"D":{"0":"1.999900"}} """ df = self.data.copy() - for r in range(len(df.index)): - for c in range(len(df.columns)): + for r, row_tup in enumerate(df.itertuples()): + for c, value in enumerate(row_tup[1:]): formatter = self._display_funcs[(r, c)] - df.iloc[r, c] = formatter(self.data.iloc[r, c]) + df.iloc[r, c] = formatter(value) return df.to_json(**kwargs) + def to_html( + self, + buf: Optional[FilePathOrBuffer[str]] = None, + # columns: Optional[Sequence] = None, + # ignored: col_space, + # ignored: header + # index: bool = True, + # na_rep: Optional[str] = "NaN", + # formatters: Optional[Dict] = None, + # float_format: Optional[Union[Callable, str]] = None, + # ignored: sparsify, + # ignored: index_names, + # ignored: justify + # ignored: max_rows + # ignored: min_rows + # ignored: max_cols + # ignored: show_dimensions + # ignored: decimal: deprecate in favour of float_format + # ignored: bold_rows + # ignored: classes + # ignored: escape + # ignored: notebook + # ignored: border + encoding: Optional[str] = None, + # ignored: table_id + no_styles=False, + ): + """ + render styler to HTML or file IO + """ + # if columns: + # hidden = [col for col in self.data.columns if col not in columns] + # self.hide_columns(hidden) + # + # if not index: + # self.hide_index() + # + # self.set_na_rep(na_rep) + # + # if float_format: + # float_cols = self.data.select_dtypes(include=[float]) + # self.format({col: float_format for col in float_cols}) + # + # if formatters: + # self.format(formatters) + + # Build HTML string.. + styler_html = self.render(no_styles=no_styles).split("\n") + if no_styles: + styler_html = ["", styler_html[0]] + else: + styler_html = ["\n " + styler_html[0] + " ", styler_html[1]] + html = f""" + + + {styler_html[0]} + + +{styler_html[1]} + +""" + return save_to_buffer(html, buf=buf, encoding=encoding) + def _translate(self): """ Convert the DataFrame in `self.data` and the attrs from `_build_styles` @@ -781,6 +849,7 @@ def render(self, **kwargs) -> str: * table_styles * caption * table_attributes + * no_styles """ self._compute() # TODO: namespace all the pandas keys diff --git a/pandas/io/formats/templates/html.tpl b/pandas/io/formats/templates/html.tpl index 65fc1dfbb37c4..37825adff92d8 100644 --- a/pandas/io/formats/templates/html.tpl +++ b/pandas/io/formats/templates/html.tpl @@ -1,70 +1,6 @@ -{# Update the template_structure.html document too #} -{%- block before_style -%}{%- endblock before_style -%} -{% block style %} - -{% endblock style %} -{% block before_table %}{% endblock before_table %} -{% block table %} - -{% block caption %} -{% if caption %} - +{# Update the template_structure.html documentation too #} +{% if no_styles %} +{% extends "html_excl.tpl" %} +{% else %} +{% extends "html_all.tpl" %} {% endif %} -{% endblock caption %} -{% block thead %} - -{% block before_head_rows %}{% endblock %} -{% for r in head %} -{% block head_tr scoped %} - -{% for c in r %} -{% if c.is_visible != False %} - <{{c.type}} class="{{c.class}}" {{c.attributes}}>{{c.value}} -{% endif %} -{% endfor %} - -{% endblock head_tr %} -{% endfor %} -{% block after_head_rows %}{% endblock %} - -{% endblock thead %} -{% block tbody %} - -{% block before_rows %}{% endblock before_rows %} -{% for r in body %} -{% block tr scoped %} - -{% for c in r %} -{% if c.is_visible != False %} - <{{c.type}} {% if c.id is defined -%} id="T_{{uuid}}{{c.id}}" {%- endif %} class="{{c.class}}" {{c.attributes}}>{{c.display_value}} -{% endif %} -{% endfor %} - -{% endblock tr %} -{% endfor %} -{% block after_rows %}{% endblock after_rows %} - -{% endblock tbody %} -
{{caption}}
-{% endblock table %} -{% block after_table %}{% endblock after_table %} diff --git a/pandas/io/formats/templates/html_all.tpl b/pandas/io/formats/templates/html_all.tpl new file mode 100644 index 0000000000000..65fc1dfbb37c4 --- /dev/null +++ b/pandas/io/formats/templates/html_all.tpl @@ -0,0 +1,70 @@ +{# Update the template_structure.html document too #} +{%- block before_style -%}{%- endblock before_style -%} +{% block style %} + +{% endblock style %} +{% block before_table %}{% endblock before_table %} +{% block table %} + +{% block caption %} +{% if caption %} + +{% endif %} +{% endblock caption %} +{% block thead %} + +{% block before_head_rows %}{% endblock %} +{% for r in head %} +{% block head_tr scoped %} + +{% for c in r %} +{% if c.is_visible != False %} + <{{c.type}} class="{{c.class}}" {{c.attributes}}>{{c.value}} +{% endif %} +{% endfor %} + +{% endblock head_tr %} +{% endfor %} +{% block after_head_rows %}{% endblock %} + +{% endblock thead %} +{% block tbody %} + +{% block before_rows %}{% endblock before_rows %} +{% for r in body %} +{% block tr scoped %} + +{% for c in r %} +{% if c.is_visible != False %} + <{{c.type}} {% if c.id is defined -%} id="T_{{uuid}}{{c.id}}" {%- endif %} class="{{c.class}}" {{c.attributes}}>{{c.display_value}} +{% endif %} +{% endfor %} + +{% endblock tr %} +{% endfor %} +{% block after_rows %}{% endblock after_rows %} + +{% endblock tbody %} +
{{caption}}
+{% endblock table %} +{% block after_table %}{% endblock after_table %} diff --git a/pandas/io/formats/templates/html_excl.tpl b/pandas/io/formats/templates/html_excl.tpl new file mode 100644 index 0000000000000..2b2671e408599 --- /dev/null +++ b/pandas/io/formats/templates/html_excl.tpl @@ -0,0 +1,46 @@ +{# this template excludes all style, ids and classes #} +{% block before_table %}{% endblock before_table %} +{% block table %} + +{% block caption %} +{% if caption %} + +{% endif %} +{% endblock caption %} +{% block thead %} + +{% block before_head_rows %}{% endblock %} +{% for r in head %} +{% block head_tr scoped %} + +{% for c in r %} +{% if c.is_visible != False %} + <{{c.type}} {{c.attributes}}>{{c.value}} +{% endif %} +{% endfor %} + +{% endblock head_tr %} +{% endfor %} +{% block after_head_rows %}{% endblock %} + +{% endblock thead %} +{% block tbody %} + +{% block before_rows %}{% endblock before_rows %} +{% for r in body %} +{% block tr scoped %} + +{% for c in r %} +{% if c.is_visible != False %} + <{{c.type}} {{c.attributes}}>{{c.display_value}} +{% endif %} +{% endfor %} + +{% endblock tr %} +{% endfor %} +{% block after_rows %}{% endblock after_rows %} + +{% endblock tbody %} +
{{caption}}
+{% endblock table %} +{% block after_table %}{% endblock after_table %} From 3e9903d3cebfbd1bcb958a2a2a7107d103a8ad03 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 8 Mar 2021 21:43:54 +0100 Subject: [PATCH 03/17] remove_json --- pandas/io/formats/style.py | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 47db5bf341813..1b0d3017d59d2 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -348,41 +348,6 @@ def to_excel( engine=engine, ) - def to_json(self, **kwargs): - """ - Convert a ``Styler`` object to JSON - - Uses the ``:meth:DataFrame.to_json`` method but includes the set - formats applied to ``Styler``. - - Parameters - ---------- - **kwargs - Any additional keyword arguments are passed to ``DataFrame.to_json()``. - Useful to control the output structure of the JSON return. - - Returns - ------- - None or str - - Examples - -------- - >>> df = pd.DataFrame(data=[[1.9999, 1.9999, 1.9999, 1.9999]], - columns=['A', 'B', 'C', 'D']) - >>> df.style.format({ - ... 'A': lambda x: float(f'{x:,.02f}'), - ... 'B': lambda x: int(f'{x:,.00f}'), - ... 'C': '{:,.01f}'.format - ... }).to_json() - {"A":{"0":2.00},"B":{"0":2.0},"C":{"0":"2.0"},"D":{"0":"1.999900"}} - """ - df = self.data.copy() - for r, row_tup in enumerate(df.itertuples()): - for c, value in enumerate(row_tup[1:]): - formatter = self._display_funcs[(r, c)] - df.iloc[r, c] = formatter(value) - return df.to_json(**kwargs) - def to_html( self, buf: Optional[FilePathOrBuffer[str]] = None, From 766b75835c74798cdcf906e67d5b43f3a7478fa2 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 8 Mar 2021 23:18:23 +0100 Subject: [PATCH 04/17] html tests --- pandas/tests/io/formats/style/test_html.py | 56 +++++++++++++++++++++ pandas/tests/io/formats/style/test_style.py | 43 ---------------- 2 files changed, 56 insertions(+), 43 deletions(-) create mode 100644 pandas/tests/io/formats/style/test_html.py diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py new file mode 100644 index 0000000000000..1135a06d21dfd --- /dev/null +++ b/pandas/tests/io/formats/style/test_html.py @@ -0,0 +1,56 @@ +import pytest + +from pandas import DataFrame + +jinja2 = pytest.importorskip("jinja2") +from pandas.io.formats.style import Styler + + +class TestStylerHTML: + def test_w3_html_format(self): + s = ( + Styler( + DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"]), + uuid_len=0, + ) + .set_table_styles([{"selector": "th", "props": "att2:v2;"}]) + .applymap(lambda x: "att1:v1;") + .set_table_attributes('class="my-cls1" style="attr3:v3;"') + .set_td_classes(DataFrame(["my-cls2"], index=["a"], columns=["A"])) + .format("{:.1f}") + .set_caption("A comprehensive test") + ) + expected = """ + + + + + + + + + + + + + + + + + + +
A comprehensive test
 A
a2.6
b2.7
+""" + assert expected == s.render() + + def test_no_styles(self): + s = Styler(DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"])) + result = s.to_html + return result diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index b938495ca9e31..f15163999eb4d 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -1319,49 +1319,6 @@ def test_uuid_len_raises(self, len_): with pytest.raises(TypeError, match=msg): Styler(df, uuid_len=len_, cell_ids=False).render() - def test_w3_html_format(self): - s = ( - Styler( - DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"]), - uuid_len=0, - ) - .set_table_styles([{"selector": "th", "props": "att2:v2;"}]) - .applymap(lambda x: "att1:v1;") - .set_table_attributes('class="my-cls1" style="attr3:v3;"') - .set_td_classes(DataFrame(["my-cls2"], index=["a"], columns=["A"])) - .format("{:.1f}") - .set_caption("A comprehensive test") - ) - expected = """ - - - - - - - - - - - - - - - - - - -
A comprehensive test
A
a2.6
b2.7
-""" - assert expected == s.render() - @pytest.mark.parametrize( "slc", [ From e558a6aaa9307c020764a0bd51be2edb9f460439 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 9 Mar 2021 00:02:06 +0100 Subject: [PATCH 05/17] html tests --- pandas/tests/io/formats/style/test_html.py | 135 +++++++++++++++++++- pandas/tests/io/formats/style/test_style.py | 46 ------- 2 files changed, 132 insertions(+), 49 deletions(-) diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index 1135a06d21dfd..fdcf2868b272f 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -1,3 +1,5 @@ +import textwrap + import pytest from pandas import DataFrame @@ -7,6 +9,22 @@ class TestStylerHTML: + def setup(self): + loader = jinja2.PackageLoader("pandas", "io/formats/templates") + env = jinja2.Environment(loader=loader, trim_blocks=True) + self.styles_template = env.get_template("html_all.tpl") + self.no_styles_template = env.get_template("html_excl.tpl") + + def test_html_template_extends_options(self): + # make sure if templates are edited tests are updated as are setup fixtures + with open("pandas/io/formats/templates/html.tpl") as file: + result = file.read() + expected = ( + '{% if no_styles %}\n{% extends "html_excl.tpl" %}\n' + '{% else %}\n{% extends "html_all.tpl" %}\n' + ) + assert expected in result + def test_w3_html_format(self): s = ( Styler( @@ -32,7 +50,7 @@ def test_w3_html_format(self): A comprehensive test -   + A @@ -52,5 +70,116 @@ def test_w3_html_format(self): def test_no_styles(self): s = Styler(DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"])) - result = s.to_html - return result + result = s.to_html(no_styles=True) + expected = """ + + + + + + + + + + + + + + + + + + + + + + +
A
a2.610000
b2.690000
+ + +""" + assert result == expected + + def test_styles(self): + s = Styler( + DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"]), uuid="abc" + ) + result = s.to_html() + expected = """ + + + + + + + + + + + + + + + + + + + + + + + +
A
a2.610000
b2.690000
+ + +""" + assert result == expected + + def test_block_names(self): + # catch accidental removal of a block + expected1 = { + "before_style", + "style", + "table_styles", + "before_cellstyle", + "cellstyle", + "before_table", + "table", + "caption", + "thead", + "tbody", + "after_table", + "before_head_rows", + "head_tr", + "after_head_rows", + "before_rows", + "tr", + "after_rows", + } + result1 = set(self.styles_template.blocks) + assert result1 == expected1 + + expected2 = {v for v in expected1 if "style" not in v} + result2 = set(self.no_styles_template.blocks) + assert result2 == expected2 + + +def test_from_custom_template(tmpdir): + p = tmpdir.mkdir("templates").join("myhtml.tpl") + p.write( + textwrap.dedent( + """\ + {% extends "html.tpl" %} + {% block table %} +

{{ table_title|default("My Table") }}

+ {{ super() }} + {% endblock table %}""" + ) + ) + result = Styler.from_custom_template(str(tmpdir.join("templates")), "myhtml.tpl") + assert issubclass(result, Styler) + assert result.env is not Styler.env + assert result.template is not Styler.template + styler = result(DataFrame({"A": [1, 2]})) + assert styler.render() diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index f15163999eb4d..687457883657e 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -1,6 +1,5 @@ import copy import re -import textwrap import numpy as np import pytest @@ -1405,48 +1404,3 @@ def test_non_reducing_multi_slice_on_multiindex(self, slice_): expected = df.loc[slice_] result = df.loc[_non_reducing_slice(slice_)] tm.assert_frame_equal(result, expected) - - -def test_block_names(): - # catch accidental removal of a block - expected = { - "before_style", - "style", - "table_styles", - "before_cellstyle", - "cellstyle", - "before_table", - "table", - "caption", - "thead", - "tbody", - "after_table", - "before_head_rows", - "head_tr", - "after_head_rows", - "before_rows", - "tr", - "after_rows", - } - result = set(Styler.template.blocks) - assert result == expected - - -def test_from_custom_template(tmpdir): - p = tmpdir.mkdir("templates").join("myhtml.tpl") - p.write( - textwrap.dedent( - """\ - {% extends "html.tpl" %} - {% block table %} -

{{ table_title|default("My Table") }}

- {{ super() }} - {% endblock table %}""" - ) - ) - result = Styler.from_custom_template(str(tmpdir.join("templates")), "myhtml.tpl") - assert issubclass(result, Styler) - assert result.env is not Styler.env - assert result.template is not Styler.template - styler = result(DataFrame({"A": [1, 2]})) - assert styler.render() From dd6785e73a3e8413b9065030941e1c3fd5d6370f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 22 Mar 2021 07:12:13 +0100 Subject: [PATCH 06/17] to_html parameters --- pandas/io/formats/style.py | 86 +++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index d080970528a6d..41df48100aea6 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -354,48 +354,75 @@ def to_excel( def to_html( self, buf: Optional[FilePathOrBuffer[str]] = None, - # columns: Optional[Sequence] = None, + columns: Optional[Sequence] = None, + hide_columns: bool = False, # ignored: col_space, - # ignored: header - # index: bool = True, - # na_rep: Optional[str] = "NaN", - # formatters: Optional[Dict] = None, - # float_format: Optional[Union[Callable, str]] = None, - # ignored: sparsify, - # ignored: index_names, + header: bool = True, + index: bool = True, + formatter: Optional[ExtFormatter] = None, + na_rep: Optional[str] = None, + precision: Optional[int] = None, + escape: bool = False, + bold_headers: bool = True, + # ignored: sparsify - not yet implementable: TODO + # ignored: index_names - not yet implementable: MAYBE TODO # ignored: justify # ignored: max_rows # ignored: min_rows # ignored: max_cols - # ignored: show_dimensions - # ignored: decimal: deprecate in favour of float_format - # ignored: bold_rows - # ignored: classes - # ignored: escape - # ignored: notebook - # ignored: border + table_uuid: Optional[str] = None, + table_attributes: Optional[str] = None, + # ignored: border - this attribute is deprecated in HTML in favour of CSS. + # is technically still possible anyway by adding 'table_attributes="border=1"' encoding: Optional[str] = None, - # ignored: table_id no_styles=False, + # ignored: notebook - not sure what this property actually did. + # ignored: table_id - replaced by UUID. + # ignored: classes - removed in favour of table_attributes which is more flexble + # ignored: bold_rows - changed to bold_headers + # ignored: float_format - removed in favour of using `.format()` method + # ignored: formatters - removed and switched to formatter for `.format()` + # ignored: decimal - removed since too much cross over with float_format + # ignored: show_dimensions - removed since not necessary feature of to_html ): """ render styler to HTML or file IO """ - # if columns: - # hidden = [col for col in self.data.columns if col not in columns] - # self.hide_columns(hidden) - # - # if not index: - # self.hide_index() - # - # self.set_na_rep(na_rep) - # + if columns and not hide_columns: + hidden = [col for col in self.data.columns if col not in columns] + self.hide_columns(hidden) + elif columns and hide_columns: + self.hide_columns(columns) + + if not index: + self.hide_index() + + if not header: + self.set_table_styles( + [{"selector": "thead tr", "props": "display:none;"}], overwrite=False + ) + + if bold_headers: + self.set_tablestyles( + [{"selector": "th", "props": "fornt-weight: bold;"}], overwrite=False + ) + + if any( + formatter is not None, precision is not None, na_rep is not None, escape + ): + self.format( + formatter=formatter, precision=precision, na_rep=na_rep, escape=escape + ) + + if table_uuid: + self.set_uuid(table_uuid) + + if table_attributes: + self.set_table_attributes(table_attributes) + # if float_format: # float_cols = self.data.select_dtypes(include=[float]) # self.format({col: float_format for col in float_cols}) - # - # if formatters: - # self.format(formatters) # Build HTML string.. styler_html = self.render(no_styles=no_styles).split("\n") @@ -403,7 +430,8 @@ def to_html( styler_html = ["", styler_html[0]] else: styler_html = ["\n " + styler_html[0] + " ", styler_html[1]] - html = f""" + html = f"""\ + {styler_html[0]} From 42cefe6a1c30086d82c5fb6c05351230b728df07 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 1 Apr 2021 14:08:15 +0200 Subject: [PATCH 07/17] remove formatting code fro to_html --- pandas/io/formats/style.py | 67 +++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 3d0b31b22eb2c..bab1d179d1881 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -362,15 +362,15 @@ def to_excel( def to_html( self, buf: Optional[FilePathOrBuffer[str]] = None, - columns: Optional[Sequence] = None, - hide_columns: bool = False, + # columns: Optional[Sequence] = None, + # hide_columns: bool = False, # ignored: col_space, - header: bool = True, - index: bool = True, - formatter: Optional[ExtFormatter] = None, - na_rep: Optional[str] = None, - precision: Optional[int] = None, - escape: bool = False, + # header: bool = True, + # index: bool = True, + # formatter: Optional[ExtFormatter] = None, + # na_rep: Optional[str] = None, + # precision: Optional[int] = None, + # escape: bool = False, bold_headers: bool = True, # ignored: sparsify - not yet implementable: TODO # ignored: index_names - not yet implementable: MAYBE TODO @@ -382,7 +382,7 @@ def to_html( table_attributes: Optional[str] = None, # ignored: border - this attribute is deprecated in HTML in favour of CSS. # is technically still possible anyway by adding 'table_attributes="border=1"' - encoding: Optional[str] = None, + encoding: Union[str, bool] = False, no_styles=False, # ignored: notebook - not sure what this property actually did. # ignored: table_id - replaced by UUID. @@ -396,31 +396,31 @@ def to_html( """ render styler to HTML or file IO """ - if columns and not hide_columns: - hidden = [col for col in self.data.columns if col not in columns] - self.hide_columns(hidden) - elif columns and hide_columns: - self.hide_columns(columns) + # if columns and not hide_columns: + # hidden = [col for col in self.data.columns if col not in columns] + # self.hide_columns(hidden) + # elif columns and hide_columns: + # self.hide_columns(columns) - if not index: - self.hide_index() + # if not index: + # self.hide_index() - if not header: - self.set_table_styles( - [{"selector": "thead tr", "props": "display:none;"}], overwrite=False - ) + # if not header: + # self.set_table_styles( + # [{"selector": "thead tr", "props": "display:none;"}], overwrite=False + # ) if bold_headers: self.set_tablestyles( [{"selector": "th", "props": "fornt-weight: bold;"}], overwrite=False ) - if any( - formatter is not None, precision is not None, na_rep is not None, escape - ): - self.format( - formatter=formatter, precision=precision, na_rep=na_rep, escape=escape - ) + # if any( + # formatter is not None, precision is not None, na_rep is not None, escape + # ): + # self.format( + # formatter=formatter, precision=precision, na_rep=na_rep, escape=escape + # ) if table_uuid: self.set_uuid(table_uuid) @@ -435,19 +435,26 @@ def to_html( # Build HTML string.. styler_html = self.render(no_styles=no_styles).split("\n") if no_styles: - styler_html = ["", styler_html[0]] + styler_html = ("", styler_html[0]) else: - styler_html = ["\n " + styler_html[0] + " ", styler_html[1]] - html = f"""\ + styler_html = ("\n " + styler_html[0] + " ", styler_html[1]) + + if encoding: + encoding = "utf-8" if encoding is True else encoding + html = f"""\ - {styler_html[0]} + {styler_html[0]} {styler_html[1]} """ + else: + encoding = None + html = styler_html[0] + styler_html[1] + return save_to_buffer(html, buf=buf, encoding=encoding) def _translate(self): From 10f007ccebb234c681ba0fadf29c537f532aa179 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 14 Apr 2021 19:35:47 +0200 Subject: [PATCH 08/17] reduce pr scope to simpler initial additions --- pandas/io/formats/style.py | 128 +++++------ pandas/io/formats/templates/html.tpl | 6 +- .../{html_excl.tpl => html_basic.tpl} | 0 .../{html_all.tpl => html_styles.tpl} | 0 pandas/tests/io/formats/style/test_html.py | 209 +++++++++--------- pandas/tests/io/formats/style/test_style.py | 25 --- 6 files changed, 162 insertions(+), 206 deletions(-) rename pandas/io/formats/templates/{html_excl.tpl => html_basic.tpl} (100%) rename pandas/io/formats/templates/{html_all.tpl => html_styles.tpl} (100%) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 9ed6e750dab01..99a4db74f5646 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -327,100 +327,72 @@ def to_excel( def to_html( self, buf: FilePathOrBuffer[str] | None = None, - # columns: Optional[Sequence] = None, - # hide_columns: bool = False, - # ignored: col_space, - # header: bool = True, - # index: bool = True, - # formatter: Optional[ExtFormatter] = None, - # na_rep: Optional[str] = None, - # precision: Optional[int] = None, - # escape: bool = False, - bold_headers: bool = True, - # ignored: sparsify - not yet implementable: TODO - # ignored: index_names - not yet implementable: MAYBE TODO - # ignored: justify - # ignored: max_rows - # ignored: min_rows - # ignored: max_cols table_uuid: str | None = None, table_attributes: str | None = None, - # ignored: border - this attribute is deprecated in HTML in favour of CSS. - # is technically still possible anyway by adding 'table_attributes="border=1"' - encoding: str | bool = False, - no_styles: bool = False, - # ignored: notebook - not sure what this property actually did. - # ignored: table_id - replaced by UUID. - # ignored: classes - removed in favour of table_attributes which is more flexble - # ignored: bold_rows - changed to bold_headers - # ignored: float_format - removed in favour of using `.format()` method - # ignored: formatters - removed and switched to formatter for `.format()` - # ignored: decimal - removed since too much cross over with float_format - # ignored: show_dimensions - removed since not necessary feature of to_html + encoding: str | None = None, + doctype_html: bool = True, + exclude_styles: bool = False, ): """ - render styler to HTML or file IO - """ - # if columns and not hide_columns: - # hidden = [col for col in self.data.columns if col not in columns] - # self.hide_columns(hidden) - # elif columns and hide_columns: - # self.hide_columns(columns) - - # if not index: - # self.hide_index() - - # if not header: - # self.set_table_styles( - # [{"selector": "thead tr", "props": "display:none;"}], overwrite=False - # ) - - if bold_headers: - self.set_tablestyles( - [{"selector": "th", "props": "fornt-weight: bold;"}], overwrite=False - ) + Write Styler to a file, buffer or string in HTML-CSS format. - # if any( - # formatter is not None, precision is not None, na_rep is not None, escape - # ): - # self.format( - # formatter=formatter, precision=precision, na_rep=na_rep, escape=escape - # ) + Parameters + ---------- + buf : str, Path, or StringIO-like, optional, default None + Buffer to write to. If ``None``, the output is returned as a string. + table_uuid: str, optional + Id attribute assigned to the HTML element in the format: + + ``
`` + + If not given uses Styler's initially assigned value. + table_attributes: str, optional + Attributes to assign within the `
` HTML element in the format: + + ``
>`` + + If not given defaults to Styler's preexisting value. + encoding : str, optional + Character encoding setting for file output, and HTML meta tags, + defaults to "utf-8" if None. + doctype_html : bool, default True + Whether to output a fully structured HTML file including all + HTML elements, or just the core ``\n") - if no_styles: - styler_html = ("", styler_html[0]) + styler_html = self.render(exclude_styles=exclude_styles).split("\n") + if exclude_styles: + style, body = "", styler_html[0] else: - styler_html = ("\n " + styler_html[0] + " ", styler_html[1]) - - if encoding: - encoding = "utf-8" if encoding is True else encoding - html = f"""\ - - - - {styler_html[0]} - - -{styler_html[1]} - -""" + style, body = "\n " + styler_html[0] + " ", styler_html[1] + + if doctype_html: + html = ( + f'\n\n\n ' + f"{style}\n\n\n{body}\n\n" + ) else: - encoding = None - html = styler_html[0] + styler_html[1] + html = style + body - return save_to_buffer(html, buf=buf, encoding=encoding) + return save_to_buffer( + html, buf=buf, encoding=(encoding if buf is not None else None) + ) def set_td_classes(self, classes: DataFrame) -> Styler: """ diff --git a/pandas/io/formats/templates/html.tpl b/pandas/io/formats/templates/html.tpl index 37825adff92d8..04ba084444624 100644 --- a/pandas/io/formats/templates/html.tpl +++ b/pandas/io/formats/templates/html.tpl @@ -1,6 +1,6 @@ {# Update the template_structure.html documentation too #} -{% if no_styles %} -{% extends "html_excl.tpl" %} +{% if exclude_styles %} +{% extends "html_basic.tpl" %} {% else %} -{% extends "html_all.tpl" %} +{% extends "html_styles.tpl" %} {% endif %} diff --git a/pandas/io/formats/templates/html_excl.tpl b/pandas/io/formats/templates/html_basic.tpl similarity index 100% rename from pandas/io/formats/templates/html_excl.tpl rename to pandas/io/formats/templates/html_basic.tpl diff --git a/pandas/io/formats/templates/html_all.tpl b/pandas/io/formats/templates/html_styles.tpl similarity index 100% rename from pandas/io/formats/templates/html_all.tpl rename to pandas/io/formats/templates/html_styles.tpl diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index fdcf2868b272f..4ab6cb9c77c04 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -7,38 +7,79 @@ jinja2 = pytest.importorskip("jinja2") from pandas.io.formats.style import Styler +loader = jinja2.PackageLoader("pandas", "io/formats/templates") +env = jinja2.Environment(loader=loader, trim_blocks=True) -class TestStylerHTML: - def setup(self): - loader = jinja2.PackageLoader("pandas", "io/formats/templates") - env = jinja2.Environment(loader=loader, trim_blocks=True) - self.styles_template = env.get_template("html_all.tpl") - self.no_styles_template = env.get_template("html_excl.tpl") - - def test_html_template_extends_options(self): - # make sure if templates are edited tests are updated as are setup fixtures - with open("pandas/io/formats/templates/html.tpl") as file: - result = file.read() - expected = ( - '{% if no_styles %}\n{% extends "html_excl.tpl" %}\n' - '{% else %}\n{% extends "html_all.tpl" %}\n' - ) - assert expected in result - - def test_w3_html_format(self): - s = ( - Styler( - DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"]), - uuid_len=0, - ) - .set_table_styles([{"selector": "th", "props": "att2:v2;"}]) - .applymap(lambda x: "att1:v1;") - .set_table_attributes('class="my-cls1" style="attr3:v3;"') - .set_td_classes(DataFrame(["my-cls2"], index=["a"], columns=["A"])) - .format("{:.1f}") - .set_caption("A comprehensive test") + +@pytest.fixture +def tpl_styles(): + return env.get_template("html_styles.tpl") + + +@pytest.fixture +def tpl_basic(): + return env.get_template("html_basic.tpl") + + +def test_html_template_extends_options(): + # make sure if templates are edited tests are updated as are setup fixtures + # to understand the dependency + with open("pandas/io/formats/templates/html.tpl") as file: + result = file.read() + expected = ( + '{% if exclude_styles %}\n{% extends "html_basic.tpl" %}\n' + '{% else %}\n{% extends "html_styles.tpl" %}\n' + ) + assert expected in result + + +def test_exclude_styles(): + s = Styler(DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"])) + result = s.to_html(exclude_styles=True) + expected = """ + + + + + +
+ + + + + + + + + + + + + + + + +
 A
a2.610000
b2.690000
+ + +""" + assert result == expected + + +def test_w3_html_format(): + s = ( + Styler( + DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"]), + uuid_len=0, ) - expected = """ - - - - - - - - - - - - - - - - - - -
A comprehensive test
 A
a2.6
b2.7
-""" - assert expected == s.render() - @pytest.mark.parametrize( "slc", [ @@ -1418,23 +1359,3 @@ def test_non_reducing_multi_slice_on_multiindex(self, slice_): expected = df.loc[slice_] result = df.loc[non_reducing_slice(slice_)] tm.assert_frame_equal(result, expected) - - -def test_from_custom_template(tmpdir): - p = tmpdir.mkdir("templates").join("myhtml.tpl") - p.write( - textwrap.dedent( - """\ - {% extends "html.tpl" %} - {% block table %} -

{{ table_title|default("My Table") }}

- {{ super() }} - {% endblock table %}""" - ) - ) - result = Styler.from_custom_template(str(tmpdir.join("templates")), "myhtml.tpl") - assert issubclass(result, Styler) - assert result.env is not Styler.env - assert result.template_html is not Styler.template_html - styler = result(DataFrame({"A": [1, 2]})) - assert styler.render() From 130fba49e9d97457ba6a725bb899e7f33e60225e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 15 Apr 2021 13:54:55 +0200 Subject: [PATCH 10/17] use dedent --- pandas/tests/io/formats/style/test_html.py | 181 +++++++++++---------- 1 file changed, 95 insertions(+), 86 deletions(-) diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index 6818d073a6f72..b061bd6f85700 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -1,4 +1,4 @@ -import textwrap +from textwrap import dedent import pytest @@ -36,33 +36,36 @@ def test_html_template_extends_options(): def test_exclude_styles(): s = Styler(DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"])) result = s.to_html(exclude_styles=True) - expected = """ - - - - - - - - - - - - - - - - - - - - - - -
 A
a2.610000
b2.690000
- - -""" + expected = dedent( + """\ + + + + + + + + + + + + + + + + + + + + + + + +
 A
a2.610000
b2.690000
+ + + """ + ) assert result == expected @@ -79,34 +82,37 @@ def test_w3_html_format(): .format("{:.1f}") .set_caption("A comprehensive test") ) - expected = """ - - - - - - - - - - - - - - - - - - -
A comprehensive test
 A
a2.6
b2.7
-""" + expected = dedent( + """\ + + + + + + + + + + + + + + + + + + + +
A comprehensive test
 A
a2.6
b2.7
+ """ + ) assert expected == s.render() @@ -130,35 +136,38 @@ def test_rowspan_w3(): def test_styles(): s = Styler(DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"]), uuid="abc") result = s.to_html() - expected = """ - - - - - - - - - - - - - - - - - - - - - - - -
 A
a2.610000
b2.690000
- - -""" + expected = dedent( + """\ + + + + + + + + + + + + + + + + + + + + + + + + +
 A
a2.610000
b2.690000
+ + + """ + ) assert result == expected @@ -194,7 +203,7 @@ def test_block_names(tpl_styles, tpl_basic): def test_from_custom_template(tmpdir): p = tmpdir.mkdir("templates").join("myhtml.tpl") p.write( - textwrap.dedent( + dedent( """\ {% extends "html.tpl" %} {% block table %} From e1818f277da672640369eef0ba4680055e6ac558 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 23 Apr 2021 12:57:56 +0200 Subject: [PATCH 11/17] refactor template inheritance --- pandas/io/formats/style.py | 21 +++----- pandas/io/formats/templates/html.tpl | 18 ++++++- .../{html_basic.tpl => html_body_exc.tpl} | 1 - .../{html_styles.tpl => html_body_inc.tpl} | 25 --------- pandas/io/formats/templates/html_style.tpl | 24 +++++++++ pandas/tests/io/formats/style/test_html.py | 51 +++++++++++-------- 6 files changed, 77 insertions(+), 63 deletions(-) rename pandas/io/formats/templates/{html_basic.tpl => html_body_exc.tpl} (94%) rename pandas/io/formats/templates/{html_styles.tpl => html_body_inc.tpl} (64%) create mode 100644 pandas/io/formats/templates/html_style.tpl diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 3762ff6461a11..077845ef88cc5 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -394,23 +394,14 @@ def to_html( self.set_table_attributes(table_attributes) # Build HTML string.. - styler_html = self.render(exclude_styles=exclude_styles).split("\n") - if exclude_styles: - style, body = "", styler_html[0] - else: - style, body = "\n " + styler_html[0] + " ", styler_html[1] - - if doctype_html: - html = ( - f'\n\n\n ' - f"{style}\n\n\n{body}\n\n" - ) - else: - html = style + body + styler_html = self.render( + exclude_styles=exclude_styles, + encoding=encoding if encoding else "utf-8", + doctype_html=doctype_html, + ) return save_to_buffer( - html, buf=buf, encoding=(encoding if buf is not None else None) + styler_html, buf=buf, encoding=(encoding if buf is not None else None) ) def set_td_classes(self, classes: DataFrame) -> Styler: diff --git a/pandas/io/formats/templates/html.tpl b/pandas/io/formats/templates/html.tpl index 04ba084444624..af0a353f37234 100644 --- a/pandas/io/formats/templates/html.tpl +++ b/pandas/io/formats/templates/html.tpl @@ -1,6 +1,20 @@ {# Update the template_structure.html documentation too #} +{% if doctype_html %} + + + + +{% if not exclude_styles %}{% include "html_style.tpl" %}{% endif %} + + +{% if not exclude_styles %}{% include "html_body_inc.tpl" %}{% else %}{% include "html_body_exc.tpl" %}{% endif %} + + +{% elif not doctype_html %} {% if exclude_styles %} -{% extends "html_basic.tpl" %} +{% include "html_body_exc.tpl" %} {% else %} -{% extends "html_styles.tpl" %} +{% include "html_style.tpl" %} +{% include "html_body_inc.tpl" %} +{% endif %} {% endif %} diff --git a/pandas/io/formats/templates/html_basic.tpl b/pandas/io/formats/templates/html_body_exc.tpl similarity index 94% rename from pandas/io/formats/templates/html_basic.tpl rename to pandas/io/formats/templates/html_body_exc.tpl index 2b2671e408599..1c3b59c55f8d0 100644 --- a/pandas/io/formats/templates/html_basic.tpl +++ b/pandas/io/formats/templates/html_body_exc.tpl @@ -1,4 +1,3 @@ -{# this template excludes all style, ids and classes #} {% block before_table %}{% endblock before_table %} {% block table %} diff --git a/pandas/io/formats/templates/html_styles.tpl b/pandas/io/formats/templates/html_body_inc.tpl similarity index 64% rename from pandas/io/formats/templates/html_styles.tpl rename to pandas/io/formats/templates/html_body_inc.tpl index 65fc1dfbb37c4..99966d5bc7481 100644 --- a/pandas/io/formats/templates/html_styles.tpl +++ b/pandas/io/formats/templates/html_body_inc.tpl @@ -1,28 +1,3 @@ -{# Update the template_structure.html document too #} -{%- block before_style -%}{%- endblock before_style -%} -{% block style %} - -{% endblock style %} {% block before_table %}{% endblock before_table %} {% block table %}
diff --git a/pandas/io/formats/templates/html_style.tpl b/pandas/io/formats/templates/html_style.tpl new file mode 100644 index 0000000000000..b34893076bedd --- /dev/null +++ b/pandas/io/formats/templates/html_style.tpl @@ -0,0 +1,24 @@ +{%- block before_style -%}{%- endblock before_style -%} +{% block style %} + +{% endblock style %} diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index b061bd6f85700..f06b7e5e8ca97 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -12,13 +12,18 @@ @pytest.fixture -def tpl_styles(): - return env.get_template("html_styles.tpl") +def tpl_style(): + return env.get_template("html_style.tpl") @pytest.fixture -def tpl_basic(): - return env.get_template("html_basic.tpl") +def tpl_body_exc(): + return env.get_template("html_body_exc.tpl") + + +@pytest.fixture +def tpl_body_inc(): + return env.get_template("html_body_inc.tpl") def test_html_template_extends_options(): @@ -26,11 +31,9 @@ def test_html_template_extends_options(): # to understand the dependency with open("pandas/io/formats/templates/html.tpl") as file: result = file.read() - expected = ( - '{% if exclude_styles %}\n{% extends "html_basic.tpl" %}\n' - '{% else %}\n{% extends "html_styles.tpl" %}\n' - ) - assert expected in result + assert '{% include "html_body_exc.tpl" %}' in result + assert '{% include "html_style.tpl" %}' in result + assert '{% include "html_body_inc.tpl" %}' in result def test_exclude_styles(): @@ -41,7 +44,7 @@ def test_exclude_styles(): - +
@@ -135,15 +138,19 @@ def test_rowspan_w3(): def test_styles(): s = Styler(DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"]), uuid="abc") + s.set_table_styles([{"selector": "td", "props": "color: red;"}]) result = s.to_html() expected = dedent( """\ - - + +
@@ -171,14 +178,16 @@ def test_styles(): assert result == expected -def test_block_names(tpl_styles, tpl_basic): +def test_block_names(tpl_style, tpl_body_inc, tpl_body_exc): # catch accidental removal of a block - expected1 = { + expected_style = { "before_style", "style", "table_styles", "before_cellstyle", "cellstyle", + } + expected_body = { "before_table", "table", "caption", @@ -192,12 +201,14 @@ def test_block_names(tpl_styles, tpl_basic): "tr", "after_rows", } - result1 = set(tpl_styles.blocks) - assert result1 == expected1 + result1 = set(tpl_style.blocks) + assert result1 == expected_style + + result2 = set(tpl_body_exc.blocks) + assert result2 == expected_body - expected2 = {v for v in expected1 if "style" not in v} - result2 = set(tpl_basic.blocks) - assert result2 == expected2 + result3 = set(tpl_body_inc.blocks) + assert result3 == expected_body def test_from_custom_template(tmpdir): From eeeaf548dc8c418af7870c5ee5161a8bfef9091b Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 23 Apr 2021 16:19:52 +0200 Subject: [PATCH 12/17] template inheritance --- pandas/io/formats/templates/html.tpl | 10 ++--- pandas/io/formats/templates/html_body_exc.tpl | 45 ------------------- .../{html_body_inc.tpl => html_table.tpl} | 22 +++++++-- pandas/tests/io/formats/style/test_html.py | 23 +++------- 4 files changed, 29 insertions(+), 71 deletions(-) delete mode 100644 pandas/io/formats/templates/html_body_exc.tpl rename pandas/io/formats/templates/{html_body_inc.tpl => html_table.tpl} (74%) diff --git a/pandas/io/formats/templates/html.tpl b/pandas/io/formats/templates/html.tpl index af0a353f37234..880c78c8d6b05 100644 --- a/pandas/io/formats/templates/html.tpl +++ b/pandas/io/formats/templates/html.tpl @@ -7,14 +7,10 @@ {% if not exclude_styles %}{% include "html_style.tpl" %}{% endif %} -{% if not exclude_styles %}{% include "html_body_inc.tpl" %}{% else %}{% include "html_body_exc.tpl" %}{% endif %} +{% include "html_table.tpl" %} {% elif not doctype_html %} -{% if exclude_styles %} -{% include "html_body_exc.tpl" %} -{% else %} -{% include "html_style.tpl" %} -{% include "html_body_inc.tpl" %} -{% endif %} +{% if not exclude_styles %}{% include "html_style.tpl" %}{% endif %} +{% include "html_table.tpl" %} {% endif %} diff --git a/pandas/io/formats/templates/html_body_exc.tpl b/pandas/io/formats/templates/html_body_exc.tpl deleted file mode 100644 index 1c3b59c55f8d0..0000000000000 --- a/pandas/io/formats/templates/html_body_exc.tpl +++ /dev/null @@ -1,45 +0,0 @@ -{% block before_table %}{% endblock before_table %} -{% block table %} -
-{% block caption %} -{% if caption %} - -{% endif %} -{% endblock caption %} -{% block thead %} - -{% block before_head_rows %}{% endblock %} -{% for r in head %} -{% block head_tr scoped %} - -{% for c in r %} -{% if c.is_visible != False %} - <{{c.type}} {{c.attributes}}>{{c.value}} -{% endif %} -{% endfor %} - -{% endblock head_tr %} -{% endfor %} -{% block after_head_rows %}{% endblock %} - -{% endblock thead %} -{% block tbody %} - -{% block before_rows %}{% endblock before_rows %} -{% for r in body %} -{% block tr scoped %} - -{% for c in r %} -{% if c.is_visible != False %} - <{{c.type}} {{c.attributes}}>{{c.display_value}} -{% endif %} -{% endfor %} - -{% endblock tr %} -{% endfor %} -{% block after_rows %}{% endblock after_rows %} - -{% endblock tbody %} -
{{caption}}
-{% endblock table %} -{% block after_table %}{% endblock after_table %} diff --git a/pandas/io/formats/templates/html_body_inc.tpl b/pandas/io/formats/templates/html_table.tpl similarity index 74% rename from pandas/io/formats/templates/html_body_inc.tpl rename to pandas/io/formats/templates/html_table.tpl index 99966d5bc7481..dadefa4bd8365 100644 --- a/pandas/io/formats/templates/html_body_inc.tpl +++ b/pandas/io/formats/templates/html_table.tpl @@ -1,6 +1,10 @@ {% block before_table %}{% endblock before_table %} {% block table %} +{% if exclude_styles %} + +{% else %}
+{% endif %} {% block caption %} {% if caption %} @@ -12,11 +16,19 @@ {% for r in head %} {% block head_tr scoped %} +{% if exclude_styles %} +{% for c in r %} +{% if c.is_visible != False %} + <{{c.type}} {{c.attributes}}>{{c.value}} +{% endif %} +{% endfor %} +{% else %} {% for c in r %} {% if c.is_visible != False %} <{{c.type}} class="{{c.class}}" {{c.attributes}}>{{c.value}} {% endif %} {% endfor %} +{% endif %} {% endblock head_tr %} {% endfor %} @@ -29,11 +41,15 @@ {% for r in body %} {% block tr scoped %} -{% for c in r %} -{% if c.is_visible != False %} +{% if exclude_styles %} +{% for c in r %}{% if c.is_visible != False %} + <{{c.type}} {{c.attributes}}>{{c.display_value}} +{% endif %}{% endfor %} +{% else %} +{% for c in r %}{% if c.is_visible != False %} <{{c.type}} {% if c.id is defined -%} id="T_{{uuid}}{{c.id}}" {%- endif %} class="{{c.class}}" {{c.attributes}}>{{c.display_value}} +{% endif %}{% endfor %} {% endif %} -{% endfor %} {% endblock tr %} {% endfor %} diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index f06b7e5e8ca97..03d3b11421534 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -17,13 +17,8 @@ def tpl_style(): @pytest.fixture -def tpl_body_exc(): - return env.get_template("html_body_exc.tpl") - - -@pytest.fixture -def tpl_body_inc(): - return env.get_template("html_body_inc.tpl") +def tpl_table(): + return env.get_template("html_table.tpl") def test_html_template_extends_options(): @@ -31,9 +26,8 @@ def test_html_template_extends_options(): # to understand the dependency with open("pandas/io/formats/templates/html.tpl") as file: result = file.read() - assert '{% include "html_body_exc.tpl" %}' in result assert '{% include "html_style.tpl" %}' in result - assert '{% include "html_body_inc.tpl" %}' in result + assert '{% include "html_table.tpl" %}' in result def test_exclude_styles(): @@ -178,7 +172,7 @@ def test_styles(): assert result == expected -def test_block_names(tpl_style, tpl_body_inc, tpl_body_exc): +def test_block_names(tpl_style, tpl_table): # catch accidental removal of a block expected_style = { "before_style", @@ -187,7 +181,7 @@ def test_block_names(tpl_style, tpl_body_inc, tpl_body_exc): "before_cellstyle", "cellstyle", } - expected_body = { + expected_table = { "before_table", "table", "caption", @@ -204,11 +198,8 @@ def test_block_names(tpl_style, tpl_body_inc, tpl_body_exc): result1 = set(tpl_style.blocks) assert result1 == expected_style - result2 = set(tpl_body_exc.blocks) - assert result2 == expected_body - - result3 = set(tpl_body_inc.blocks) - assert result3 == expected_body + result2 = set(tpl_table.blocks) + assert result2 == expected_table def test_from_custom_template(tmpdir): From b14e7afa8d3f5abe6b95ca62d5df753eca9922b0 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 24 Apr 2021 14:32:35 +0200 Subject: [PATCH 13/17] improve tests --- pandas/io/formats/style.py | 4 +- pandas/tests/io/formats/style/test_html.py | 73 +++++++++++++--------- 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 077845ef88cc5..4fb953eff9d09 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -394,14 +394,14 @@ def to_html( self.set_table_attributes(table_attributes) # Build HTML string.. - styler_html = self.render( + html = self.render( exclude_styles=exclude_styles, encoding=encoding if encoding else "utf-8", doctype_html=doctype_html, ) return save_to_buffer( - styler_html, buf=buf, encoding=(encoding if buf is not None else None) + html, buf=buf, encoding=(encoding if buf is not None else None) ) def set_td_classes(self, classes: DataFrame) -> Styler: diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index 03d3b11421534..9957fd89a367a 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -11,6 +11,11 @@ env = jinja2.Environment(loader=loader, trim_blocks=True) +@pytest.fixture +def styler(): + return Styler(DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"])) + + @pytest.fixture def tpl_style(): return env.get_template("html_style.tpl") @@ -30,9 +35,8 @@ def test_html_template_extends_options(): assert '{% include "html_table.tpl" %}' in result -def test_exclude_styles(): - s = Styler(DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"])) - result = s.to_html(exclude_styles=True) +def test_exclude_styles(styler): + result = styler.to_html(exclude_styles=True) expected = dedent( """\ @@ -66,30 +70,29 @@ def test_exclude_styles(): assert result == expected -def test_w3_html_format(): - s = ( - Styler( - DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"]), - uuid_len=0, - ) - .set_table_styles([{"selector": "th", "props": "att2:v2;"}]) - .applymap(lambda x: "att1:v1;") - .set_table_attributes('class="my-cls1" style="attr3:v3;"') - .set_td_classes(DataFrame(["my-cls2"], index=["a"], columns=["A"])) - .format("{:.1f}") - .set_caption("A comprehensive test") +def test_w3_html_format(styler): + styler.set_uuid("").set_table_styles( + [{"selector": "th", "props": "att2:v2;"}] + ).applymap(lambda x: "att1:v1;").set_table_attributes( + 'class="my-cls1" style="attr3:v3;"' + ).set_td_classes( + DataFrame(["my-cls2"], index=["a"], columns=["A"]) + ).format( + "{:.1f}" + ).set_caption( + "A comprehensive test" ) expected = dedent( """\ -
{{caption}}
+
@@ -99,41 +102,41 @@ def test_w3_html_format(): - - + + - - + +
A comprehensive test
a2.6a2.6
b2.7b2.7
""" ) - assert expected == s.render() + assert expected == styler.render() def test_colspan_w3(): # GH 36223 df = DataFrame(data=[[1, 2]], columns=[["l0", "l0"], ["l1a", "l1b"]]) - s = Styler(df, uuid="_", cell_ids=False) - assert 'l0' in s.render() + styler = Styler(df, uuid="_", cell_ids=False) + assert 'l0' in styler.render() def test_rowspan_w3(): # GH 38533 df = DataFrame(data=[[1, 2]], index=[["l0", "l0"], ["l1a", "l1b"]]) - s = Styler(df, uuid="_", cell_ids=False) + styler = Styler(df, uuid="_", cell_ids=False) assert ( 'l0' in s.render() + 'level0 row0" rowspan="2">l0' in styler.render() ) -def test_styles(): - s = Styler(DataFrame([[2.61], [2.69]], index=["a", "b"], columns=["A"]), uuid="abc") - s.set_table_styles([{"selector": "td", "props": "color: red;"}]) - result = s.to_html() +def test_styles(styler): + styler.set_uuid("abc_") + styler.set_table_styles([{"selector": "td", "props": "color: red;"}]) + result = styler.to_html() expected = dedent( """\ @@ -172,6 +175,14 @@ def test_styles(): assert result == expected +def test_doctype(styler): + result = styler.to_html(doctype_html=False) + assert "" not in result + assert "" not in result + assert "" not in result + assert "" not in result + + def test_block_names(tpl_style, tpl_table): # catch accidental removal of a block expected_style = { From a3ce1efaab186814ef2ddbde7825f47c4bc608f9 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 7 May 2021 11:38:03 +0200 Subject: [PATCH 14/17] make keyword only: simialar to Styler.to_latex() --- pandas/io/formats/style.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 0194703d0d7af..169b826d62699 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -389,6 +389,7 @@ def to_excel( def to_html( self, buf: FilePathOrBuffer[str] | None = None, + *, table_uuid: str | None = None, table_attributes: str | None = None, encoding: str | None = None, From 257505862cbe1a77d9fc83996376a6a987839270 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 22 May 2021 09:21:17 +0200 Subject: [PATCH 15/17] doc comments --- pandas/io/formats/style.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index e071c1a336e15..2ded006879f95 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -419,6 +419,8 @@ def to_html( """ Write Styler to a file, buffer or string in HTML-CSS format. + .. versionadded:: 1.3.0 + Parameters ---------- buf : str, Path, or StringIO-like, optional, default None @@ -450,6 +452,10 @@ def to_html( ------- str or None If `buf` is None, returns the result as a string. Otherwise returns `None`. + + See Also + -------- + DataFrame.to_html: Write a DataFrame to a file, buffer or string in HTML format. """ if table_uuid: self.set_uuid(table_uuid) From c0bc29cfcb749a1ba74a4c42a7fe7192b0cc496b Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 22 May 2021 19:07:55 +0200 Subject: [PATCH 16/17] change default doctyoe_html from True to False --- pandas/io/formats/style.py | 4 ++-- pandas/tests/io/formats/style/test_html.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 2ded006879f95..d4e8003633d4b 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -413,7 +413,7 @@ def to_html( table_uuid: str | None = None, table_attributes: str | None = None, encoding: str | None = None, - doctype_html: bool = True, + doctype_html: bool = False, exclude_styles: bool = False, ): """ @@ -440,7 +440,7 @@ def to_html( encoding : str, optional Character encoding setting for file output, and HTML meta tags, defaults to "utf-8" if None. - doctype_html : bool, default True + doctype_html : bool, default False Whether to output a fully structured HTML file including all HTML elements, or just the core ``