Skip to content

REF: Remove side effects from importing Styler 2 #53429

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

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 11 additions & 14 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@
from pandas.core.shared_docs import _shared_docs

from pandas.io.formats.format import save_to_buffer

jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.")

from pandas.io.formats.style_render import (
CSSProperties,
CSSStyles,
Expand Down Expand Up @@ -78,22 +75,18 @@

from pandas import ExcelWriter

try:
import matplotlib as mpl
import matplotlib.pyplot as plt

has_mpl = True
except ImportError:
has_mpl = False


@contextmanager
def _mpl(func: Callable) -> Generator[tuple[Any, Any], None, None]:
if has_mpl:
yield plt, mpl
else:
try:
import matplotlib as mpl
import matplotlib.pyplot as plt

except ImportError:
raise ImportError(f"{func.__name__} requires matplotlib.")

yield plt, mpl


####
# Shared Doc Strings
Expand Down Expand Up @@ -3490,6 +3483,10 @@ def from_custom_template(
Has the correct ``env``,``template_html``, ``template_html_table`` and
``template_html_style`` class attributes set.
"""
jinja2 = import_optional_dependency(
"jinja2", extra="DataFrame.style requires jinja2."
)

loader = jinja2.ChoiceLoader([jinja2.FileSystemLoader(searchpath), cls.loader])

# mypy doesn't like dynamically-defined classes
Expand Down
79 changes: 58 additions & 21 deletions pandas/io/formats/style_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@
Axis,
Level,
)
jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.")
from markupsafe import escape as escape_html # markupsafe is jinja2 dependency

BaseFormatter = Union[str, Callable]
ExtFormatter = Union[BaseFormatter, Dict[Any, Optional[BaseFormatter]]]
Expand All @@ -72,13 +70,15 @@ class StylerRenderer:
Base class to process rendering a Styler with a specified jinja2 template.
"""

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")
template_string = env.get_template("string.tpl")
# For cached class properties defined below
_loader = None
_env = None
custom_template_directory = None
template_html = "html.tpl"
template_html_table = "html_table.tpl"
template_html_style = "html_style.tpl"
template_latex = "latex.tpl"
template_string = "string.tpl"

def __init__(
self,
Expand Down Expand Up @@ -147,6 +147,37 @@ def __init__(
tuple[int, int], Callable[[Any], str]
] = defaultdict(lambda: partial(_default_formatter, precision=precision))

@classmethod
@property
def loader(cls):
if cls._loader is None:
jinja2 = import_optional_dependency(
"jinja2", extra="DataFrame.style requires jinja2."
)
cls._loader = jinja2.PackageLoader("pandas", "io/formats/templates")
return cls._loader

@classmethod
@property
def env(cls):
if cls._env is None:
jinja2 = import_optional_dependency(
"jinja2", extra="DataFrame.style requires jinja2."
)
if cls.custom_template_directory is None:
cls._env = jinja2.Environment(loader=cls.loader, trim_blocks=True)
else:
cls._env = jinja2.Environment(
loader=jinja2.ChoiceLoader(
[
jinja2.FileSystemLoader(cls.custom_template_directory),
cls.loader,
]
),
trim_blocks=True,
)
return cls._env

def _render(
self,
sparse_index: bool,
Expand Down Expand Up @@ -206,10 +237,11 @@ def _render_html(
"""
d = self._render(sparse_index, sparse_columns, max_rows, max_cols, " ")
d.update(kwargs)
return self.template_html.render(
template_html = self.env.get_template(self.template_html)
return template_html.render(
**d,
html_table_tpl=self.template_html_table,
html_style_tpl=self.template_html_style,
html_table_tpl=self.env.get_template(self.template_html_table),
html_style_tpl=self.env.get_template(self.template_html_style),
)

def _render_latex(
Expand All @@ -219,13 +251,14 @@ def _render_latex(
Render a Styler in latex format
"""
d = self._render(sparse_index, sparse_columns, None, None)
template_latex = self.env.get_template(self.template_latex)
self._translate_latex(d, clines=clines)
self.template_latex.globals["parse_wrap"] = _parse_latex_table_wrapping
self.template_latex.globals["parse_table"] = _parse_latex_table_styles
self.template_latex.globals["parse_cell"] = _parse_latex_cell_styles
self.template_latex.globals["parse_header"] = _parse_latex_header_span
template_latex.globals["parse_wrap"] = _parse_latex_table_wrapping
template_latex.globals["parse_table"] = _parse_latex_table_styles
template_latex.globals["parse_cell"] = _parse_latex_cell_styles
template_latex.globals["parse_header"] = _parse_latex_header_span
d.update(kwargs)
return self.template_latex.render(**d)
return template_latex.render(**d)

def _render_string(
self,
Expand All @@ -240,7 +273,8 @@ def _render_string(
"""
d = self._render(sparse_index, sparse_columns, max_rows, max_cols)
d.update(kwargs)
return self.template_string.render(**d)
template_string = self.env.get_template(self.template_string)
return template_string.render(**d)

def _compute(self):
"""
Expand Down Expand Up @@ -1778,11 +1812,11 @@ def wrapper(x):
return wrapper


def _str_escape(x, escape):
def _str_escape(x, escape, markupsafe):
"""if escaping: only use on str, else return input"""
if isinstance(x, str):
if escape == "html":
return escape_html(x)
return markupsafe.escape(x)
elif escape == "latex":
return _escape_latex(x)
elif escape == "latex-math":
Expand Down Expand Up @@ -1840,7 +1874,10 @@ def _maybe_wrap_formatter(

# Replace chars if escaping
if escape is not None:
func_1 = lambda x: func_0(_str_escape(x, escape=escape))
markupsafe = import_optional_dependency(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this might be quite an expensive import operation if the formatter is applied over each cell. Will have to check this

"markupsafe", extra="DataFrame.style requires markupsafe."
)
func_1 = lambda x: func_0(_str_escape(x, escape=escape, markupsafe=markupsafe))
else:
func_1 = func_0

Expand Down
5 changes: 5 additions & 0 deletions pandas/tests/io/formats/style/custom_templates/myhtml.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% extends "html_table.tpl" %}
{% block table %}
<h1>{{ table_title|default("My Table") }}</h1>
{{ super() }}
{% endblock table %}
8 changes: 5 additions & 3 deletions pandas/tests/io/formats/style/test_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
)

pytest.importorskip("jinja2")
import markupsafe

from pandas.io.formats.style import Styler
from pandas.io.formats.style_render import _str_escape

Expand Down Expand Up @@ -408,12 +410,12 @@ def test_format_decimal(formatter, thousands, precision, func, col):
def test_str_escape_error():
msg = "`escape` only permitted in {'html', 'latex', 'latex-math'}, got "
with pytest.raises(ValueError, match=msg):
_str_escape("text", "bad_escape")
_str_escape("text", "bad_escape", markupsafe)

with pytest.raises(ValueError, match=msg):
_str_escape("text", [])
_str_escape("text", [], markupsafe)

_str_escape(2.00, "bad_escape") # OK since dtype is float
_str_escape(2.00, "bad_escape", markupsafe) # OK since dtype is float


def test_long_int_formatting():
Expand Down
33 changes: 33 additions & 0 deletions pandas/tests/io/formats/style/test_style.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import contextlib
import copy
import re
import subprocess
import sys
from textwrap import dedent

import numpy as np
Expand Down Expand Up @@ -99,6 +101,23 @@ def styler(df):
return Styler(df)


def test_import_styler_no_side_effects():
# GH#52995
code = (
"import matplotlib.units as units; "
"import matplotlib.dates as mdates; "
"n_conv = len(units.registry); "
"import pandas as pd; "
"from pandas.io.formats.style import Styler; "
"assert len(units.registry) == n_conv; "
# Ensure jinja2 was not imported
"import sys; "
"assert 'jinja2' not in sys.modules; "
)
call = [sys.executable, "-c", code]
subprocess.check_output(call)


@pytest.mark.parametrize(
"sparse_columns, exp_cols",
[
Expand Down Expand Up @@ -1588,3 +1607,17 @@ def test_output_buffer(mi_styler, format):
# gh 47053
with tm.ensure_clean(f"delete_me.{format}") as f:
getattr(mi_styler, f"to_{format}")(f)


def test_custom_template_path(df):
import os

path = os.path.abspath(os.path.dirname(__file__))

class MyStyler(Styler):
custom_template_directory = os.path.join(path, "custom_templates")
template_html = "myhtml.tpl"

styler = MyStyler(df)
result = styler.to_html()
assert "<h1>My Table</h1>" in result