diff --git a/.gitignore b/.gitignore
index a509fcf736ea8..c953020f59342 100644
--- a/.gitignore
+++ b/.gitignore
@@ -103,3 +103,4 @@ doc/source/index.rst
doc/build/html/index.html
# Windows specific leftover:
doc/tmp.sv
+doc/source/templates/
diff --git a/MANIFEST.in b/MANIFEST.in
index b7a7e6039ac9a..31de3466cb357 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -25,3 +25,4 @@ global-exclude *.png
# recursive-include LICENSES *
include versioneer.py
include pandas/_version.py
+include pandas/formats/templates/*.tpl
diff --git a/ci/requirements-3.5_DOC.run b/ci/requirements-3.5_DOC.run
index 7ed60758612bb..9647ab53ab835 100644
--- a/ci/requirements-3.5_DOC.run
+++ b/ci/requirements-3.5_DOC.run
@@ -1,5 +1,6 @@
ipython
ipykernel
+ipywidgets
sphinx
nbconvert
nbformat
diff --git a/doc/source/style.ipynb b/doc/source/style.ipynb
index 2b8bf35a913c1..06763b2a5e741 100644
--- a/doc/source/style.ipynb
+++ b/doc/source/style.ipynb
@@ -54,7 +54,7 @@
},
"outputs": [],
"source": [
- "import matplotlib\n",
+ "import matplotlib.pyplot\n",
"# We have this here to trigger matplotlib's font cache stuff.\n",
"# This cell is hidden from the output"
]
@@ -87,9 +87,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style"
@@ -107,9 +105,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.highlight_null().render().split('\\n')[:10]"
@@ -160,9 +156,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"s = df.style.applymap(color_negative_red)\n",
@@ -208,9 +202,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.apply(highlight_max)"
@@ -234,9 +226,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.\\\n",
@@ -290,9 +280,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.apply(highlight_max, color='darkorange', axis=None)"
@@ -340,9 +328,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.apply(highlight_max, subset=['B', 'C', 'D'])"
@@ -358,9 +344,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.applymap(color_negative_red,\n",
@@ -393,9 +377,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.format(\"{:.2%}\")"
@@ -411,9 +393,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.format({'B': \"{:0<4.0f}\", 'D': '{:+.2f}'})"
@@ -429,9 +409,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.format({\"B\": lambda x: \"±{:.2f}\".format(abs(x))})"
@@ -454,9 +432,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.highlight_null(null_color='red')"
@@ -472,9 +448,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"import seaborn as sns\n",
@@ -495,9 +469,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"# Uses the full color range\n",
@@ -507,9 +479,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"# Compress the color range\n",
@@ -529,9 +499,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.bar(subset=['A', 'B'], color='#d65f5f')"
@@ -547,9 +515,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.highlight_max(axis=0)"
@@ -558,9 +524,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.highlight_min(axis=0)"
@@ -576,9 +540,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.set_properties(**{'background-color': 'black',\n",
@@ -603,9 +565,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df2 = -df\n",
@@ -616,9 +576,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"style2 = df2.style\n",
@@ -671,9 +629,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"with pd.option_context('display.precision', 2):\n",
@@ -693,9 +649,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style\\\n",
@@ -728,9 +682,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.set_caption('Colormaps, with a caption.')\\\n",
@@ -756,9 +708,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"from IPython.display import HTML\n",
@@ -854,9 +804,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"from IPython.html import widgets\n",
@@ -892,16 +840,14 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"np.random.seed(25)\n",
"cmap = cmap=sns.diverging_palette(5, 250, as_cmap=True)\n",
- "df = pd.DataFrame(np.random.randn(20, 25)).cumsum()\n",
+ "bigdf = pd.DataFrame(np.random.randn(20, 25)).cumsum()\n",
"\n",
- "df.style.background_gradient(cmap, axis=1)\\\n",
+ "bigdf.style.background_gradient(cmap, axis=1)\\\n",
" .set_properties(**{'max-width': '80px', 'font-size': '1pt'})\\\n",
" .set_caption(\"Hover to magify\")\\\n",
" .set_precision(2)\\\n",
@@ -924,29 +870,157 @@
"\n",
"### Subclassing\n",
"\n",
- "This section contains a bit of information about the implementation of `Styler`.\n",
- "Since the feature is so new all of this is subject to change, even more so than the end-use API.\n",
- "\n",
- "As users apply styles (via `.apply`, `.applymap` or one of the builtins), we don't actually calculate anything.\n",
- "Instead, we append functions and arguments to a list `self._todo`.\n",
- "When asked (typically in `.render` we'll walk through the list and execute each function (this is in `self._compute()`.\n",
- "These functions update an internal `defaultdict(list)`, `self.ctx` which maps DataFrame row / column positions to CSS attribute, value pairs.\n",
- "\n",
- "We take the extra step through `self._todo` so that we can export styles and set them on other `Styler`s.\n",
- "\n",
- "Rendering uses [Jinja](http://jinja.pocoo.org/) templates.\n",
- "The `.translate` method takes `self.ctx` and builds another dictionary ready to be passed into `Styler.template.render`, the Jinja template.\n",
- "\n",
- "\n",
- "### Alternate templates\n",
- "\n",
- "We've used [Jinja](http://jinja.pocoo.org/) templates to build up the HTML.\n",
- "The template is stored as a class variable ``Styler.template.``. Subclasses can override that.\n",
+ "If the default template doesn't quite suit your needs, you can subclass Styler and extend or override the template.\n",
+ "We'll show an example of extending the default template to insert a custom header before each table."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "from jinja2 import Environment, ChoiceLoader, FileSystemLoader\n",
+ "from IPython.display import HTML\n",
+ "from pandas.io.api import Styler"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%mkdir templates"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This next cell writes the custom template.\n",
+ "We extend the template `html.tpl`, which comes with pandas."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%file templates/myhtml.tpl\n",
+ "{% extends \"html.tpl\" %}\n",
+ "{% block table %}\n",
+ "
{{ table_title|default(\"My Table\") }}
\n",
+ "{{ super() }}\n",
+ "{% endblock table %}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now that we've created a template, we need to set up a subclass of ``pd.Styler`` that\n",
+ "knows about it."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "class MyStyler(pd.Styler):\n",
+ " env = Environment(\n",
+ " loader=ChoiceLoader([\n",
+ " FileSystemLoader(\"templates\"), # contains ours\n",
+ " pd.Styler.loader, # the default\n",
+ " ])\n",
+ " )\n",
+ " template = env.get_template(\"myhtml.tpl\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Notice that we include the original loader in our environment's loader.\n",
+ "That's because we extend the original template, so the Jinja environment needs\n",
+ "to be able to find it.\n",
"\n",
- "```python\n",
- "class CustomStyle(Styler):\n",
- " template = Template(\"\"\"...\"\"\")\n",
- "```"
+ "Now we can use that custom styler. It's `__init__` takes a DataFrame."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "MyStyler(df)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Our custom template accepts a `table_title` keyword. We can provide the value in the `.render` method."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "HTML(MyStyler(df).render(table_title=\"Extending Example\"))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "For convenience, we provide the `Styler.from_custom_template` method that does the same as the custom subclass."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "EasyStyler = pd.Styler.from_custom_template(\"templates\", \"myhtml.tpl\")\n",
+ "EasyStyler(df)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Here's the template structure:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "with open(\"template_structure.html\") as f:\n",
+ " structure = f.read()\n",
+ " \n",
+ "HTML(structure)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "See the template in the [GitHub repo](https://github.com/pandas-dev/pandas) for more details."
]
}
],
diff --git a/doc/source/template_structure.html b/doc/source/template_structure.html
new file mode 100644
index 0000000000000..81dbe2b7d0217
--- /dev/null
+++ b/doc/source/template_structure.html
@@ -0,0 +1,60 @@
+
+
+
+before_style
+style
+
<style type="text/css">
+
table_styles
+
before_cellstyle
+
cellstyle
+
</style>
+
+
+before_table
+
+table
+
<table ...>
+
caption
+
+
thead
+
before_head_rows
+
head_tr (loop over headers)
+
after_head_rows
+
+
+
tbody
+
before_rows
+
tr (loop over data rows)
+
after_rows
+
+
</table>
+
+
+after_table
diff --git a/doc/source/whatsnew/v0.20.0.txt b/doc/source/whatsnew/v0.20.0.txt
index da32de750e7de..ffbc4d0e9796c 100644
--- a/doc/source/whatsnew/v0.20.0.txt
+++ b/doc/source/whatsnew/v0.20.0.txt
@@ -482,6 +482,9 @@ Other Enhancements
- ``DataFrame.to_excel()`` has a new ``freeze_panes`` parameter to turn on Freeze Panes when exporting to Excel (:issue:`15160`)
- ``pd.read_html()`` will parse multiple header rows, creating a multiindex header. (:issue:`13434`).
- HTML table output skips ``colspan`` or ``rowspan`` attribute if equal to 1. (:issue:`15403`)
+- ``pd.io.api.Styler`` template now has blocks for easier extension, :ref:`see the example notebook ` (:issue:`15649`)
+- ``pd.io.api.Styler.render`` now accepts ``**kwargs`` to allow user-defined variables in the template (:issue:`15649`)
+
- ``pd.TimedeltaIndex`` now has a custom datetick formatter specifically designed for nanosecond level precision (:issue:`8711`)
- ``pd.types.concat.union_categoricals`` gained the ``ignore_ordered`` argument to allow ignoring the ordered attribute of unioned categoricals (:issue:`13410`). See the :ref:`categorical union docs ` for more information.
diff --git a/pandas/formats/style.py b/pandas/formats/style.py
index e712010a8b4f2..af02077bd5b41 100644
--- a/pandas/formats/style.py
+++ b/pandas/formats/style.py
@@ -10,7 +10,9 @@
from collections import defaultdict, MutableMapping
try:
- from jinja2 import Template
+ from jinja2 import (
+ PackageLoader, Environment, ChoiceLoader, FileSystemLoader
+ )
except ImportError:
msg = "pandas.Styler requires jinja2. "\
"Please install with `conda install Jinja2`\n"\
@@ -68,7 +70,9 @@ class Styler(object):
Attributes
----------
- template: Jinja Template
+ env : Jinja2 Environment
+ template: Jinja2 Template
+ loader : Jinja2 Loader
Notes
-----
@@ -103,56 +107,12 @@ class Styler(object):
--------
pandas.DataFrame.style
"""
- template = Template("""
-
-
-
- {% if caption %}
- {{caption}}
- {% endif %}
-
-
- {% for r in head %}
-
- {% for c in r %}
- {% if c.is_visible != False %}
- <{{c.type}} class="{{c.class}}" {{ c.attributes|join(" ") }}>
- {{c.value}}
- {% endif %}
- {% endfor %}
-
- {% endfor %}
-
-
- {% for r in body %}
-
- {% for c in r %}
- {% if c.is_visible != False %}
- <{{c.type}} id="T_{{uuid}}{{c.id}}"
- class="{{c.class}}" {{ c.attributes|join(" ") }}>
- {{ c.display_value }}
- {% endif %}
- {% endfor %}
-
- {% endfor %}
-
-
- """)
+ loader = PackageLoader("pandas", "formats/templates")
+ env = Environment(
+ loader=loader,
+ trim_blocks=True,
+ )
+ template = env.get_template("html.tpl")
def __init__(self, data, precision=None, table_styles=None, uuid=None,
caption=None, table_attributes=None):
@@ -400,12 +360,22 @@ def format(self, formatter, subset=None):
self._display_funcs[(i, j)] = formatter
return self
- def render(self):
- """
+ def render(self, **kwargs):
+ r"""
Render the built up styles to HTML
.. versionadded:: 0.17.1
+ Parameters
+ ----------
+ **kwargs:
+ Any additional keyword arguments are passed through
+ to ``self.template.render``. This is useful when you
+ need to provide additional variables for a custom
+ template.
+
+ .. versionadded:: 0.20
+
Returns
-------
rendered: str
@@ -418,8 +388,22 @@ def render(self):
last item in a Notebook cell. When calling ``Styler.render()``
directly, wrap the result in ``IPython.display.HTML`` to view
the rendered HTML in the notebook.
+
+ Pandas uses the following keys in render. Arguments passed
+ in ``**kwargs`` take precedence, so think carefuly if you want
+ to override them:
+
+ * head
+ * cellstyle
+ * body
+ * uuid
+ * precision
+ * table_styles
+ * caption
+ * table_attributes
"""
self._compute()
+ # TODO: namespace all the pandas keys
d = self._translate()
# filter out empty styles, every cell will have a class
# but the list of props may just be [['', '']].
@@ -427,6 +411,7 @@ def render(self):
trimmed = [x for x in d['cellstyle']
if any(any(y) for y in x['props'])]
d['cellstyle'] = trimmed
+ d.update(kwargs)
return self.template.render(**d)
def _update_ctx(self, attrs):
@@ -961,6 +946,35 @@ def _highlight_extrema(data, color='yellow', max_=True):
return pd.DataFrame(np.where(extrema, attr, ''),
index=data.index, columns=data.columns)
+ @classmethod
+ def from_custom_template(cls, searchpath, name):
+ """
+ Factory function for creating a subclass of ``Styler``
+ with a custom template and Jinja environment.
+
+ Parameters
+ ----------
+ searchpath : str or list
+ Path or paths of directories containing the templates
+ name : str
+ Name of your custom template to use for rendering
+
+ Returns
+ -------
+ MyStyler : subclass of Styler
+ has the correct ``env`` and ``template`` class attributes set.
+ """
+ loader = ChoiceLoader([
+ FileSystemLoader(searchpath),
+ cls.loader,
+ ])
+
+ class MyStyler(cls):
+ env = Environment(loader=loader)
+ template = env.get_template(name)
+
+ return MyStyler
+
def _is_visible(idx_row, idx_col, lengths):
"""
diff --git a/pandas/formats/templates/html.tpl b/pandas/formats/templates/html.tpl
new file mode 100644
index 0000000000000..706db1ecdd961
--- /dev/null
+++ b/pandas/formats/templates/html.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 -%}
+ {{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|join(" ") }}>{{c.value}}{{ c.type }}>
+ {%- 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 }} id="T_{{ uuid }}{{ c.id }}" class="{{ c.class }}" {{ c.attributes|join(" ") }}>{{ c.display_value }}{{ c.type }}>
+ {%- endif %}
+ {%- endfor %}
+
+ {%- endblock tr %}
+ {%- endfor %}
+ {%- block after_rows %}{%- endblock after_rows %}
+
+{%- endblock tbody %}
+
+{%- endblock table %}
+{%- block after_table %}{% endblock after_table %}
diff --git a/pandas/io/api.py b/pandas/io/api.py
index e312e7bc2f300..4744d41472ff1 100644
--- a/pandas/io/api.py
+++ b/pandas/io/api.py
@@ -17,6 +17,23 @@
from pandas.io.pickle import read_pickle, to_pickle
from pandas.io.packers import read_msgpack, to_msgpack
from pandas.io.gbq import read_gbq
+try:
+ from pandas.formats.style import Styler
+except ImportError:
+ from pandas.compat import add_metaclass as _add_metaclass
+ from pandas.util.importing import _UnSubclassable
+
+ # We want to *not* raise an ImportError upon importing this module
+ # We *do* want to raise an ImportError with a custom message
+ # when the class is instantiated or subclassed.
+ @_add_metaclass(_UnSubclassable)
+ class Styler(object):
+ msg = ("pandas.io.api.Styler requires jinja2. "
+ "Please install with `conda install jinja2` "
+ "or `pip install jinja2`")
+ def __init__(self, *args, **kargs):
+ raise ImportError(self.msg)
+
# deprecation, xref #13790
def Term(*args, **kwargs):
diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py
index a15d7cf26cbea..6d92898042b23 100644
--- a/pandas/tests/api/test_api.py
+++ b/pandas/tests/api/test_api.py
@@ -49,7 +49,8 @@ class TestPDApi(Base, tm.TestCase):
'Period', 'PeriodIndex', 'RangeIndex', 'UInt64Index',
'Series', 'SparseArray', 'SparseDataFrame',
'SparseSeries', 'TimeGrouper', 'Timedelta',
- 'TimedeltaIndex', 'Timestamp', 'Interval', 'IntervalIndex']
+ 'TimedeltaIndex', 'Timestamp', 'Interval', 'IntervalIndex',
+ 'Styler']
# these are already deprecated; awaiting removal
deprecated_classes = ['WidePanel', 'Panel4D',
diff --git a/pandas/tests/formats/test_style.py b/pandas/tests/formats/test_style.py
index 44af0b8ebb085..08f8f2f32763d 100644
--- a/pandas/tests/formats/test_style.py
+++ b/pandas/tests/formats/test_style.py
@@ -1,6 +1,7 @@
-import pytest
-
import copy
+import textwrap
+
+import pytest
import numpy as np
import pandas as pd
from pandas import DataFrame
@@ -717,3 +718,32 @@ def test_background_gradient(self):
result = (df.style.background_gradient(subset=pd.IndexSlice[1, 'A'])
._compute().ctx)
self.assertEqual(result[(1, 0)], ['background-color: #fff7fb'])
+
+
+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(pd.DataFrame({"A": [1, 2]}))
+ assert styler.render()
diff --git a/pandas/util/importing.py b/pandas/util/importing.py
new file mode 100644
index 0000000000000..9323fb97baac0
--- /dev/null
+++ b/pandas/util/importing.py
@@ -0,0 +1,10 @@
+class _UnSubclassable(type):
+ """
+ Metaclass to raise an ImportError when subclassed
+ """
+ msg = ""
+
+ def __init__(cls, name, bases, clsdict):
+ if len(cls.mro()) > 2:
+ raise ImportError(cls.msg)
+ super(_UnSubclassable, cls).__init__(name, bases, clsdict)
diff --git a/setup.py b/setup.py
index 6707af7eb0908..d8ee52f9b4f43 100755
--- a/setup.py
+++ b/setup.py
@@ -704,7 +704,8 @@ def pxd(name):
'data/html_encoding/*.html',
'json/data/*.json'],
'pandas.tests.tools': ['data/*.csv'],
- 'pandas.tests.tseries': ['data/*.pickle']
+ 'pandas.tests.tseries': ['data/*.pickle'],
+ 'pandas.formats': ['templates/*.tpl']
},
ext_modules=extensions,
maintainer_email=EMAIL,