Skip to content

Backport PR #42323 on branch 1.3.x (BUG: Styler.to_latex now doesn't manipulate the Styler object.) #42327

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
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
2 changes: 1 addition & 1 deletion doc/source/whatsnew/v1.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ which has been revised and improved (:issue:`39720`, :issue:`39317`, :issue:`404
- Many features of the :class:`.Styler` class are now either partially or fully usable on a DataFrame with a non-unique indexes or columns (:issue:`41143`)
- One has greater control of the display through separate sparsification of the index or columns using the :ref:`new styler options <options.available>`, which are also usable via :func:`option_context` (:issue:`41142`)
- Added the option ``styler.render.max_elements`` to avoid browser overload when styling large DataFrames (:issue:`40712`)
- Added the method :meth:`.Styler.to_latex` (:issue:`21673`), which also allows some limited CSS conversion (:issue:`40731`)
- Added the method :meth:`.Styler.to_latex` (:issue:`21673`, :issue:`42320`), which also allows some limited CSS conversion (:issue:`40731`)
- Added the method :meth:`.Styler.to_html` (:issue:`13379`)
- Added the method :meth:`.Styler.set_sticky` to make index and column headers permanently visible in scrolling HTML frames (:issue:`29072`)

Expand Down
93 changes: 58 additions & 35 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,8 @@ def to_latex(
0 & {\bfseries}{\Huge{1}} \\
\end{tabular}
"""
obj = self._copy(deepcopy=True) # manipulate table_styles on obj, not self

table_selectors = (
[style["selector"] for style in self.table_styles]
if self.table_styles is not None
Expand All @@ -717,7 +719,7 @@ def to_latex(

if column_format is not None:
# add more recent setting to table_styles
self.set_table_styles(
obj.set_table_styles(
[{"selector": "column_format", "props": f":{column_format}"}],
overwrite=False,
)
Expand All @@ -735,13 +737,13 @@ def to_latex(
column_format += (
("r" if not siunitx else "S") if ci in numeric_cols else "l"
)
self.set_table_styles(
obj.set_table_styles(
[{"selector": "column_format", "props": f":{column_format}"}],
overwrite=False,
)

if position:
self.set_table_styles(
obj.set_table_styles(
[{"selector": "position", "props": f":{position}"}],
overwrite=False,
)
Expand All @@ -753,13 +755,13 @@ def to_latex(
f"'raggedright', 'raggedleft', 'centering', "
f"got: '{position_float}'"
)
self.set_table_styles(
obj.set_table_styles(
[{"selector": "position_float", "props": f":{position_float}"}],
overwrite=False,
)

if hrules:
self.set_table_styles(
obj.set_table_styles(
[
{"selector": "toprule", "props": ":toprule"},
{"selector": "midrule", "props": ":midrule"},
Expand All @@ -769,20 +771,20 @@ def to_latex(
)

if label:
self.set_table_styles(
obj.set_table_styles(
[{"selector": "label", "props": f":{{{label.replace(':', '§')}}}"}],
overwrite=False,
)

if caption:
self.set_caption(caption)
obj.set_caption(caption)

if sparse_index is None:
sparse_index = get_option("styler.sparse.index")
if sparse_columns is None:
sparse_columns = get_option("styler.sparse.columns")

latex = self._render_latex(
latex = obj._render_latex(
sparse_index=sparse_index,
sparse_columns=sparse_columns,
multirow_align=multirow_align,
Expand Down Expand Up @@ -964,39 +966,60 @@ def _update_ctx(self, attrs: DataFrame) -> None:
self.ctx[(i, j)].extend(css_list)

def _copy(self, deepcopy: bool = False) -> Styler:
styler = Styler(
self.data,
precision=self.precision,
caption=self.caption,
table_attributes=self.table_attributes,
cell_ids=self.cell_ids,
na_rep=self.na_rep,
)
"""
Copies a Styler, allowing for deepcopy or shallow copy

styler.uuid = self.uuid
styler.hide_index_ = self.hide_index_
Copying a Styler aims to recreate a new Styler object which contains the same
data and styles as the original.

if deepcopy:
styler.ctx = copy.deepcopy(self.ctx)
styler._todo = copy.deepcopy(self._todo)
styler.table_styles = copy.deepcopy(self.table_styles)
styler.hidden_columns = copy.copy(self.hidden_columns)
styler.cell_context = copy.deepcopy(self.cell_context)
styler.tooltips = copy.deepcopy(self.tooltips)
else:
styler.ctx = self.ctx
styler._todo = self._todo
styler.table_styles = self.table_styles
styler.hidden_columns = self.hidden_columns
styler.cell_context = self.cell_context
styler.tooltips = self.tooltips
Data dependent attributes [copied and NOT exported]:
- formatting (._display_funcs)
- hidden index values or column values (.hidden_rows, .hidden_columns)
- tooltips
- cell_context (cell css classes)
- ctx (cell css styles)
- caption

Non-data dependent attributes [copied and exported]:
- hidden index state and hidden columns state (.hide_index_, .hide_columns_)
- table_attributes
- table_styles
- applied styles (_todo)

"""
# GH 40675
styler = Styler(
self.data, # populates attributes 'data', 'columns', 'index' as shallow
uuid_len=self.uuid_len,
)
shallow = [ # simple string or boolean immutables
"hide_index_",
"hide_columns_",
"table_attributes",
"cell_ids",
"caption",
]
deep = [ # nested lists or dicts
"_display_funcs",
"hidden_rows",
"hidden_columns",
"ctx",
"cell_context",
"_todo",
"table_styles",
"tooltips",
]

for attr in shallow:
setattr(styler, attr, getattr(self, attr))

for attr in deep:
val = getattr(self, attr)
setattr(styler, attr, copy.deepcopy(val) if deepcopy else val)

return styler

def __copy__(self) -> Styler:
"""
Deep copy by default.
"""
return self._copy(deepcopy=False)

def __deepcopy__(self, memo) -> Styler:
Expand Down
168 changes: 72 additions & 96 deletions pandas/tests/io/formats/style/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,35 @@ def mi_styler(mi_df):
return Styler(mi_df, uuid_len=0)


@pytest.fixture
def mi_styler_comp(mi_styler):
# comprehensively add features to mi_styler
mi_styler.uuid_len = 5
mi_styler.uuid = "abcde_"
mi_styler.set_caption("capt")
mi_styler.set_table_styles([{"selector": "a", "props": "a:v;"}])
mi_styler.hide_columns()
mi_styler.hide_columns([("c0", "c1_a")])
mi_styler.hide_index()
mi_styler.hide_index([("i0", "i1_a")])
mi_styler.set_table_attributes('class="box"')
mi_styler.format(na_rep="MISSING", precision=3)
mi_styler.highlight_max(axis=None)
mi_styler.set_td_classes(
DataFrame(
[["a", "b"], ["a", "c"]], index=mi_styler.index, columns=mi_styler.columns
)
)
mi_styler.set_tooltips(
DataFrame(
[["a2", "b2"], ["a2", "c2"]],
index=mi_styler.index,
columns=mi_styler.columns,
)
)
return mi_styler


@pytest.mark.parametrize(
"sparse_columns, exp_cols",
[
Expand Down Expand Up @@ -156,6 +185,49 @@ def test_render_trimming_mi():
assert {"attributes": 'colspan="2"'}.items() <= ctx["head"][0][2].items()


@pytest.mark.parametrize("comprehensive", [True, False])
@pytest.mark.parametrize("render", [True, False])
@pytest.mark.parametrize("deepcopy", [True, False])
def test_copy(comprehensive, render, deepcopy, mi_styler, mi_styler_comp):
styler = mi_styler_comp if comprehensive else mi_styler
styler.uuid_len = 5

s2 = copy.deepcopy(styler) if deepcopy else copy.copy(styler) # make copy and check
assert s2 is not styler

if render:
styler.to_html()

excl = ["na_rep", "precision", "uuid", "cellstyle_map"] # deprecated or special var
if not deepcopy: # check memory locations are equal for all included attributes
for attr in [a for a in styler.__dict__ if (not callable(a) and a not in excl)]:
assert id(getattr(s2, attr)) == id(getattr(styler, attr))
else: # check memory locations are different for nested or mutable vars
shallow = [
"data",
"columns",
"index",
"uuid_len",
"caption",
"cell_ids",
"hide_index_",
"hide_columns_",
"table_attributes",
]
for attr in shallow:
assert id(getattr(s2, attr)) == id(getattr(styler, attr))

for attr in [
a
for a in styler.__dict__
if (not callable(a) and a not in excl and a not in shallow)
]:
if getattr(s2, attr) is None:
assert id(getattr(s2, attr)) == id(getattr(styler, attr))
else:
assert id(getattr(s2, attr)) != id(getattr(styler, attr))


class TestStyler:
def setup_method(self, method):
np.random.seed(24)
Expand Down Expand Up @@ -211,102 +283,6 @@ def test_update_ctx_flatten_multi_and_trailing_semi(self):
}
assert self.styler.ctx == expected

@pytest.mark.parametrize("do_changes", [True, False])
@pytest.mark.parametrize("do_render", [True, False])
def test_copy(self, do_changes, do_render):
# Updated in GH39708
# Change some defaults (to check later if the new values are copied)
if do_changes:
self.styler.set_table_styles(
[{"selector": "th", "props": [("foo", "bar")]}]
)
self.styler.set_table_attributes('class="foo" data-bar')
self.styler.hide_index_ = not self.styler.hide_index_
self.styler.hide_columns("A")
classes = DataFrame(
[["favorite-val red", ""], [None, "blue my-val"]],
index=self.df.index,
columns=self.df.columns,
)
self.styler.set_td_classes(classes)
ttips = DataFrame(
data=[["Favorite", ""], [np.nan, "my"]],
columns=self.df.columns,
index=self.df.index,
)
self.styler.set_tooltips(ttips)
self.styler.cell_ids = not self.styler.cell_ids

if do_render:
self.styler.render()

s_copy = copy.copy(self.styler)
s_deepcopy = copy.deepcopy(self.styler)

assert self.styler is not s_copy
assert self.styler is not s_deepcopy

# Check for identity
assert self.styler.ctx is s_copy.ctx
assert self.styler._todo is s_copy._todo
assert self.styler.table_styles is s_copy.table_styles
assert self.styler.hidden_columns is s_copy.hidden_columns
assert self.styler.cell_context is s_copy.cell_context
assert self.styler.tooltips is s_copy.tooltips
if do_changes: # self.styler.tooltips is not None
assert self.styler.tooltips.tt_data is s_copy.tooltips.tt_data
assert (
self.styler.tooltips.class_properties
is s_copy.tooltips.class_properties
)
assert self.styler.tooltips.table_styles is s_copy.tooltips.table_styles

# Check for non-identity
assert self.styler.ctx is not s_deepcopy.ctx
assert self.styler._todo is not s_deepcopy._todo
assert self.styler.hidden_columns is not s_deepcopy.hidden_columns
assert self.styler.cell_context is not s_deepcopy.cell_context
if do_changes: # self.styler.table_style is not None
assert self.styler.table_styles is not s_deepcopy.table_styles
if do_changes: # self.styler.tooltips is not None
assert self.styler.tooltips is not s_deepcopy.tooltips
assert self.styler.tooltips.tt_data is not s_deepcopy.tooltips.tt_data
assert (
self.styler.tooltips.class_properties
is not s_deepcopy.tooltips.class_properties
)
assert (
self.styler.tooltips.table_styles
is not s_deepcopy.tooltips.table_styles
)

self.styler._update_ctx(self.attrs)
self.styler.highlight_max()
assert self.styler.ctx == s_copy.ctx
assert self.styler.ctx != s_deepcopy.ctx
assert self.styler._todo == s_copy._todo
assert self.styler._todo != s_deepcopy._todo
assert s_deepcopy._todo == []

equal_attributes = [
"table_styles",
"table_attributes",
"cell_ids",
"hide_index_",
"hidden_columns",
"cell_context",
]
for s2 in [s_copy, s_deepcopy]:
for att in equal_attributes:
assert self.styler.__dict__[att] == s2.__dict__[att]
if do_changes: # self.styler.tooltips is not None
tm.assert_frame_equal(self.styler.tooltips.tt_data, s2.tooltips.tt_data)
assert (
self.styler.tooltips.class_properties
== s2.tooltips.class_properties
)
assert self.styler.tooltips.table_styles == s2.tooltips.table_styles

def test_clear(self):
# updated in GH 39396
tt = DataFrame({"A": [None, "tt"]})
Expand Down
16 changes: 16 additions & 0 deletions pandas/tests/io/formats/style/test_to_latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,3 +489,19 @@ def test_parse_latex_css_conversion_option():
expected = [("command", "option--wrap")]
result = _parse_latex_css_conversion(css)
assert result == expected


def test_styler_object_after_render(styler):
# GH 42320
pre_render = styler._copy(deepcopy=True)
styler.to_latex(
column_format="rllr",
position="h",
position_float="centering",
hrules=True,
label="my lab",
caption="my cap",
)

assert pre_render.table_styles == styler.table_styles
assert pre_render.caption == styler.caption