Skip to content

REGR: Subclassing Styler and addressing from_custom_template #42091

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
2 changes: 2 additions & 0 deletions doc/source/reference/style.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Styler properties

Styler.env
Styler.template_html
Styler.template_html_style
Styler.template_html_table
Styler.template_latex
Styler.loader

Expand Down
60 changes: 53 additions & 7 deletions doc/source/user_guide/style.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -1780,7 +1780,7 @@
" Styler.loader, # the default\n",
" ])\n",
" )\n",
" template_html = env.get_template(\"myhtml.tpl\")"
" template_html_table = env.get_template(\"myhtml.tpl\")"
]
},
{
Expand Down Expand Up @@ -1833,14 +1833,35 @@
"outputs": [],
"source": [
"EasyStyler = Styler.from_custom_template(\"templates\", \"myhtml.tpl\")\n",
"EasyStyler(df3)"
"HTML(EasyStyler(df3).render(table_title=\"Another Title\"))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Here's the template structure:"
"#### Template Structure\n",
"\n",
"Here's the template structure for the both the style generation template and the table generation template:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Style template:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"nbsphinx": "hidden"
},
"outputs": [],
"source": [
"with open(\"templates/html_style_structure.html\") as f:\n",
" style_structure = f.read()"
]
},
{
Expand All @@ -1849,10 +1870,35 @@
"metadata": {},
"outputs": [],
"source": [
"with open(\"templates/template_structure.html\") as f:\n",
" structure = f.read()\n",
" \n",
"HTML(structure)"
"HTML(style_structure)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Table template:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"nbsphinx": "hidden"
},
"outputs": [],
"source": [
"with open(\"templates/html_table_structure.html\") as f:\n",
" table_structure = f.read()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"HTML(table_structure)"
]
},
{
Expand Down
35 changes: 35 additions & 0 deletions doc/source/user_guide/templates/html_style_structure.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!--
This is an HTML fragment that gets included into a notebook & rst document
Inspired by nbconvert
https://github.com/jupyter/nbconvert/blob/8ac591a0b8694147d0f34bf6392594c2811c1395/docs/source/template_structure.html
-->
<style type="text/css">
/* Overrides of notebook CSS for static HTML export */
.template_block {
background-color: hsla(120, 60%, 70%, 0.2);
margin: 10px;
padding: 5px;
border: 1px solid hsla(120, 60%, 70%, 0.5);
border-left: 2px solid black;
}
.template_block pre {
background: transparent;
padding: 0;
}
.big_vertical_ellipsis {
font-size: 24pt;
}
</style>

<div class="template_block">before_style</div>
<div class="template_block">style
<pre>&lt;style type=&quot;text/css&quot;&gt;</pre>
<div class="template_block">table_styles</div>
<div class="template_block">before_cellstyle</div>
<div class="template_block">cellstyle</div>
<pre>&lt;/style&gt;</pre>
</div><!-- /style -->
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,6 @@
}
</style>

<div class="template_block">before_style</div>
<div class="template_block">style
<pre>&lt;style type=&quot;text/css&quot;&gt;</pre>
<div class="template_block">table_styles</div>
<div class="template_block">before_cellstyle</div>
<div class="template_block">cellstyle</div>
<pre>&lt;/style&gt;</pre>
</div><!-- /style -->

<div class="template_block" >before_table</div>

<div class="template_block" >table
Expand Down
2 changes: 1 addition & 1 deletion doc/source/user_guide/templates/myhtml.tpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends "html.tpl" %}
{% extends "html_table.tpl" %}
{% block table %}
<h1>{{ table_title|default("My Table") }}</h1>
{{ super() }}
Expand Down
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,7 @@ Other API changes
- Added new ``engine`` and ``**engine_kwargs`` parameters to :meth:`DataFrame.to_sql` to support other future "SQL engines". Currently we still only use ``SQLAlchemy`` under the hood, but more engines are planned to be supported such as `turbodbc <https://turbodbc.readthedocs.io/en/latest/>`_ (:issue:`36893`)
- Removed redundant ``freq`` from :class:`PeriodIndex` string representation (:issue:`41653`)
- :meth:`ExtensionDtype.construct_array_type` is now a required method instead of an optional one for :class:`ExtensionDtype` subclasses (:issue:`24860`)
- :meth:`.Styler.from_custom_template` now has two new arguments for template names, and removed the old ``name``, due to template inheritance having been introducing for better parsing (:issue:`42053`). Subclassing modifications to Styler attributes are also needed.

.. _whatsnew_130.api_breaking.build:

Expand Down
27 changes: 21 additions & 6 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -2550,23 +2550,35 @@ def highlight_quantile(
)

@classmethod
def from_custom_template(cls, searchpath, name):
def from_custom_template(
cls, searchpath, html_table: str | None = None, html_style: str | None = None
):
"""
Factory function for creating a subclass of ``Styler``.

Uses a custom template and Jinja environment.
Uses custom templates and Jinja environment.

.. versionchanged:: 1.3.0

Parameters
----------
searchpath : str or list
Path or paths of directories containing the templates.
name : str
Name of your custom template to use for rendering.
html_table : str
Name of your custom template to replace the html_table template.

.. versionadded:: 1.3.0

html_style : str
Name of your custom template to replace the html_style template.

.. versionadded:: 1.3.0

Returns
-------
MyStyler : subclass of Styler
Has the correct ``env`` and ``template`` class attributes set.
Has the correct ``env``,``template_html``, ``template_html_table`` and
``template_html_style`` class attributes set.
"""
loader = jinja2.ChoiceLoader([jinja2.FileSystemLoader(searchpath), cls.loader])

Expand All @@ -2575,7 +2587,10 @@ def from_custom_template(cls, searchpath, name):
# error: Invalid base class "cls"
class MyStyler(cls): # type:ignore[valid-type,misc]
env = jinja2.Environment(loader=loader)
template_html = env.get_template(name)
if html_table:
template_html_table = env.get_template(html_table)
if html_style:
template_html_style = env.get_template(html_style)

return MyStyler

Expand Down
8 changes: 7 additions & 1 deletion pandas/io/formats/style_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ class StylerRenderer:
loader = jinja2.PackageLoader("pandas", "io/formats/templates")
env = jinja2.Environment(loader=loader, trim_blocks=True)
template_html = env.get_template("html.tpl")
template_html_table = env.get_template("html_table.tpl")
template_html_style = env.get_template("html_style.tpl")
template_latex = env.get_template("latex.tpl")

def __init__(
Expand Down Expand Up @@ -120,7 +122,11 @@ def _render_html(self, sparse_index: bool, sparse_columns: bool, **kwargs) -> st
# TODO: namespace all the pandas keys
d = self._translate(sparse_index, sparse_columns)
d.update(kwargs)
return self.template_html.render(**d)
return self.template_html.render(
**d,
html_table_tpl=self.template_html_table,
html_style_tpl=self.template_html_style,
)

def _render_latex(self, sparse_index: bool, sparse_columns: bool, **kwargs) -> str:
"""
Expand Down
10 changes: 5 additions & 5 deletions pandas/io/formats/templates/html.tpl
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{# Update the template_structure.html documentation too #}
{# Update the html_style/table_structure.html documentation too #}
{% if doctype_html %}
<!DOCTYPE html>
<html>
<head>
<meta charset="{{encoding}}">
{% if not exclude_styles %}{% include "html_style.tpl" %}{% endif %}
{% if not exclude_styles %}{% include html_style_tpl %}{% endif %}
</head>
<body>
{% include "html_table.tpl" %}
{% include html_table_tpl %}
</body>
</html>
{% elif not doctype_html %}
{% if not exclude_styles %}{% include "html_style.tpl" %}{% endif %}
{% include "html_table.tpl" %}
{% if not exclude_styles %}{% include html_style_tpl %}{% endif %}
{% include html_table_tpl %}
{% endif %}
46 changes: 34 additions & 12 deletions pandas/tests/io/formats/style/test_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,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_style.tpl" %}' in result
assert '{% include "html_table.tpl" %}' in result
assert "{% include html_style_tpl %}" in result
assert "{% include html_table_tpl %}" in result


def test_exclude_styles(styler):
Expand Down Expand Up @@ -223,24 +223,46 @@ def test_block_names(tpl_style, tpl_table):
assert result2 == expected_table


def test_from_custom_template(tmpdir):
p = tmpdir.mkdir("templates").join("myhtml.tpl")
def test_from_custom_template_table(tmpdir):
p = tmpdir.mkdir("tpl").join("myhtml_table.tpl")
p.write(
dedent(
"""\
{% extends "html.tpl" %}
{% block table %}
<h1>{{ table_title|default("My Table") }}</h1>
{{ super() }}
{% endblock table %}"""
{% extends "html_table.tpl" %}
{% block table %}
<h1>{{custom_title}}</h1>
{{ super() }}
{% endblock table %}"""
)
)
result = Styler.from_custom_template(str(tmpdir.join("templates")), "myhtml.tpl")
result = Styler.from_custom_template(str(tmpdir.join("tpl")), "myhtml_table.tpl")
assert issubclass(result, Styler)
assert result.env is not Styler.env
assert result.template_html is not Styler.template_html
assert result.template_html_table is not Styler.template_html_table
styler = result(DataFrame({"A": [1, 2]}))
assert styler.render()
assert "<h1>My Title</h1>\n\n\n<table" in styler.render(custom_title="My Title")


def test_from_custom_template_style(tmpdir):
p = tmpdir.mkdir("tpl").join("myhtml_style.tpl")
p.write(
dedent(
"""\
{% extends "html_style.tpl" %}
{% block style %}
<link rel="stylesheet" href="mystyle.css">
{{ super() }}
{% endblock style %}"""
)
)
result = Styler.from_custom_template(
str(tmpdir.join("tpl")), html_style="myhtml_style.tpl"
)
assert issubclass(result, Styler)
assert result.env is not Styler.env
assert result.template_html_style is not Styler.template_html_style
styler = result(DataFrame({"A": [1, 2]}))
assert '<link rel="stylesheet" href="mystyle.css">\n\n<style' in styler.render()


def test_caption_as_sequence(styler):
Expand Down