Skip to content

Commit 2c26ee7

Browse files
Backport PR #42323 on branch 1.3.x (BUG: Styler.to_latex now doesn't manipulate the Styler object.) (#42327)
1 parent 061642a commit 2c26ee7

File tree

4 files changed

+147
-132
lines changed

4 files changed

+147
-132
lines changed

doc/source/whatsnew/v1.3.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ which has been revised and improved (:issue:`39720`, :issue:`39317`, :issue:`404
136136
- 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`)
137137
- 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`)
138138
- Added the option ``styler.render.max_elements`` to avoid browser overload when styling large DataFrames (:issue:`40712`)
139-
- Added the method :meth:`.Styler.to_latex` (:issue:`21673`), which also allows some limited CSS conversion (:issue:`40731`)
139+
- Added the method :meth:`.Styler.to_latex` (:issue:`21673`, :issue:`42320`), which also allows some limited CSS conversion (:issue:`40731`)
140140
- Added the method :meth:`.Styler.to_html` (:issue:`13379`)
141141
- Added the method :meth:`.Styler.set_sticky` to make index and column headers permanently visible in scrolling HTML frames (:issue:`29072`)
142142

pandas/io/formats/style.py

+58-35
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,8 @@ def to_latex(
709709
0 & {\bfseries}{\Huge{1}} \\
710710
\end{tabular}
711711
"""
712+
obj = self._copy(deepcopy=True) # manipulate table_styles on obj, not self
713+
712714
table_selectors = (
713715
[style["selector"] for style in self.table_styles]
714716
if self.table_styles is not None
@@ -717,7 +719,7 @@ def to_latex(
717719

718720
if column_format is not None:
719721
# add more recent setting to table_styles
720-
self.set_table_styles(
722+
obj.set_table_styles(
721723
[{"selector": "column_format", "props": f":{column_format}"}],
722724
overwrite=False,
723725
)
@@ -735,13 +737,13 @@ def to_latex(
735737
column_format += (
736738
("r" if not siunitx else "S") if ci in numeric_cols else "l"
737739
)
738-
self.set_table_styles(
740+
obj.set_table_styles(
739741
[{"selector": "column_format", "props": f":{column_format}"}],
740742
overwrite=False,
741743
)
742744

743745
if position:
744-
self.set_table_styles(
746+
obj.set_table_styles(
745747
[{"selector": "position", "props": f":{position}"}],
746748
overwrite=False,
747749
)
@@ -753,13 +755,13 @@ def to_latex(
753755
f"'raggedright', 'raggedleft', 'centering', "
754756
f"got: '{position_float}'"
755757
)
756-
self.set_table_styles(
758+
obj.set_table_styles(
757759
[{"selector": "position_float", "props": f":{position_float}"}],
758760
overwrite=False,
759761
)
760762

761763
if hrules:
762-
self.set_table_styles(
764+
obj.set_table_styles(
763765
[
764766
{"selector": "toprule", "props": ":toprule"},
765767
{"selector": "midrule", "props": ":midrule"},
@@ -769,20 +771,20 @@ def to_latex(
769771
)
770772

771773
if label:
772-
self.set_table_styles(
774+
obj.set_table_styles(
773775
[{"selector": "label", "props": f":{{{label.replace(':', '§')}}}"}],
774776
overwrite=False,
775777
)
776778

777779
if caption:
778-
self.set_caption(caption)
780+
obj.set_caption(caption)
779781

780782
if sparse_index is None:
781783
sparse_index = get_option("styler.sparse.index")
782784
if sparse_columns is None:
783785
sparse_columns = get_option("styler.sparse.columns")
784786

785-
latex = self._render_latex(
787+
latex = obj._render_latex(
786788
sparse_index=sparse_index,
787789
sparse_columns=sparse_columns,
788790
multirow_align=multirow_align,
@@ -964,39 +966,60 @@ def _update_ctx(self, attrs: DataFrame) -> None:
964966
self.ctx[(i, j)].extend(css_list)
965967

966968
def _copy(self, deepcopy: bool = False) -> Styler:
967-
styler = Styler(
968-
self.data,
969-
precision=self.precision,
970-
caption=self.caption,
971-
table_attributes=self.table_attributes,
972-
cell_ids=self.cell_ids,
973-
na_rep=self.na_rep,
974-
)
969+
"""
970+
Copies a Styler, allowing for deepcopy or shallow copy
975971
976-
styler.uuid = self.uuid
977-
styler.hide_index_ = self.hide_index_
972+
Copying a Styler aims to recreate a new Styler object which contains the same
973+
data and styles as the original.
978974
979-
if deepcopy:
980-
styler.ctx = copy.deepcopy(self.ctx)
981-
styler._todo = copy.deepcopy(self._todo)
982-
styler.table_styles = copy.deepcopy(self.table_styles)
983-
styler.hidden_columns = copy.copy(self.hidden_columns)
984-
styler.cell_context = copy.deepcopy(self.cell_context)
985-
styler.tooltips = copy.deepcopy(self.tooltips)
986-
else:
987-
styler.ctx = self.ctx
988-
styler._todo = self._todo
989-
styler.table_styles = self.table_styles
990-
styler.hidden_columns = self.hidden_columns
991-
styler.cell_context = self.cell_context
992-
styler.tooltips = self.tooltips
975+
Data dependent attributes [copied and NOT exported]:
976+
- formatting (._display_funcs)
977+
- hidden index values or column values (.hidden_rows, .hidden_columns)
978+
- tooltips
979+
- cell_context (cell css classes)
980+
- ctx (cell css styles)
981+
- caption
982+
983+
Non-data dependent attributes [copied and exported]:
984+
- hidden index state and hidden columns state (.hide_index_, .hide_columns_)
985+
- table_attributes
986+
- table_styles
987+
- applied styles (_todo)
988+
989+
"""
990+
# GH 40675
991+
styler = Styler(
992+
self.data, # populates attributes 'data', 'columns', 'index' as shallow
993+
uuid_len=self.uuid_len,
994+
)
995+
shallow = [ # simple string or boolean immutables
996+
"hide_index_",
997+
"hide_columns_",
998+
"table_attributes",
999+
"cell_ids",
1000+
"caption",
1001+
]
1002+
deep = [ # nested lists or dicts
1003+
"_display_funcs",
1004+
"hidden_rows",
1005+
"hidden_columns",
1006+
"ctx",
1007+
"cell_context",
1008+
"_todo",
1009+
"table_styles",
1010+
"tooltips",
1011+
]
1012+
1013+
for attr in shallow:
1014+
setattr(styler, attr, getattr(self, attr))
1015+
1016+
for attr in deep:
1017+
val = getattr(self, attr)
1018+
setattr(styler, attr, copy.deepcopy(val) if deepcopy else val)
9931019

9941020
return styler
9951021

9961022
def __copy__(self) -> Styler:
997-
"""
998-
Deep copy by default.
999-
"""
10001023
return self._copy(deepcopy=False)
10011024

10021025
def __deepcopy__(self, memo) -> Styler:

pandas/tests/io/formats/style/test_style.py

+72-96
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,35 @@ def mi_styler(mi_df):
3838
return Styler(mi_df, uuid_len=0)
3939

4040

41+
@pytest.fixture
42+
def mi_styler_comp(mi_styler):
43+
# comprehensively add features to mi_styler
44+
mi_styler.uuid_len = 5
45+
mi_styler.uuid = "abcde_"
46+
mi_styler.set_caption("capt")
47+
mi_styler.set_table_styles([{"selector": "a", "props": "a:v;"}])
48+
mi_styler.hide_columns()
49+
mi_styler.hide_columns([("c0", "c1_a")])
50+
mi_styler.hide_index()
51+
mi_styler.hide_index([("i0", "i1_a")])
52+
mi_styler.set_table_attributes('class="box"')
53+
mi_styler.format(na_rep="MISSING", precision=3)
54+
mi_styler.highlight_max(axis=None)
55+
mi_styler.set_td_classes(
56+
DataFrame(
57+
[["a", "b"], ["a", "c"]], index=mi_styler.index, columns=mi_styler.columns
58+
)
59+
)
60+
mi_styler.set_tooltips(
61+
DataFrame(
62+
[["a2", "b2"], ["a2", "c2"]],
63+
index=mi_styler.index,
64+
columns=mi_styler.columns,
65+
)
66+
)
67+
return mi_styler
68+
69+
4170
@pytest.mark.parametrize(
4271
"sparse_columns, exp_cols",
4372
[
@@ -156,6 +185,49 @@ def test_render_trimming_mi():
156185
assert {"attributes": 'colspan="2"'}.items() <= ctx["head"][0][2].items()
157186

158187

188+
@pytest.mark.parametrize("comprehensive", [True, False])
189+
@pytest.mark.parametrize("render", [True, False])
190+
@pytest.mark.parametrize("deepcopy", [True, False])
191+
def test_copy(comprehensive, render, deepcopy, mi_styler, mi_styler_comp):
192+
styler = mi_styler_comp if comprehensive else mi_styler
193+
styler.uuid_len = 5
194+
195+
s2 = copy.deepcopy(styler) if deepcopy else copy.copy(styler) # make copy and check
196+
assert s2 is not styler
197+
198+
if render:
199+
styler.to_html()
200+
201+
excl = ["na_rep", "precision", "uuid", "cellstyle_map"] # deprecated or special var
202+
if not deepcopy: # check memory locations are equal for all included attributes
203+
for attr in [a for a in styler.__dict__ if (not callable(a) and a not in excl)]:
204+
assert id(getattr(s2, attr)) == id(getattr(styler, attr))
205+
else: # check memory locations are different for nested or mutable vars
206+
shallow = [
207+
"data",
208+
"columns",
209+
"index",
210+
"uuid_len",
211+
"caption",
212+
"cell_ids",
213+
"hide_index_",
214+
"hide_columns_",
215+
"table_attributes",
216+
]
217+
for attr in shallow:
218+
assert id(getattr(s2, attr)) == id(getattr(styler, attr))
219+
220+
for attr in [
221+
a
222+
for a in styler.__dict__
223+
if (not callable(a) and a not in excl and a not in shallow)
224+
]:
225+
if getattr(s2, attr) is None:
226+
assert id(getattr(s2, attr)) == id(getattr(styler, attr))
227+
else:
228+
assert id(getattr(s2, attr)) != id(getattr(styler, attr))
229+
230+
159231
class TestStyler:
160232
def setup_method(self, method):
161233
np.random.seed(24)
@@ -211,102 +283,6 @@ def test_update_ctx_flatten_multi_and_trailing_semi(self):
211283
}
212284
assert self.styler.ctx == expected
213285

214-
@pytest.mark.parametrize("do_changes", [True, False])
215-
@pytest.mark.parametrize("do_render", [True, False])
216-
def test_copy(self, do_changes, do_render):
217-
# Updated in GH39708
218-
# Change some defaults (to check later if the new values are copied)
219-
if do_changes:
220-
self.styler.set_table_styles(
221-
[{"selector": "th", "props": [("foo", "bar")]}]
222-
)
223-
self.styler.set_table_attributes('class="foo" data-bar')
224-
self.styler.hide_index_ = not self.styler.hide_index_
225-
self.styler.hide_columns("A")
226-
classes = DataFrame(
227-
[["favorite-val red", ""], [None, "blue my-val"]],
228-
index=self.df.index,
229-
columns=self.df.columns,
230-
)
231-
self.styler.set_td_classes(classes)
232-
ttips = DataFrame(
233-
data=[["Favorite", ""], [np.nan, "my"]],
234-
columns=self.df.columns,
235-
index=self.df.index,
236-
)
237-
self.styler.set_tooltips(ttips)
238-
self.styler.cell_ids = not self.styler.cell_ids
239-
240-
if do_render:
241-
self.styler.render()
242-
243-
s_copy = copy.copy(self.styler)
244-
s_deepcopy = copy.deepcopy(self.styler)
245-
246-
assert self.styler is not s_copy
247-
assert self.styler is not s_deepcopy
248-
249-
# Check for identity
250-
assert self.styler.ctx is s_copy.ctx
251-
assert self.styler._todo is s_copy._todo
252-
assert self.styler.table_styles is s_copy.table_styles
253-
assert self.styler.hidden_columns is s_copy.hidden_columns
254-
assert self.styler.cell_context is s_copy.cell_context
255-
assert self.styler.tooltips is s_copy.tooltips
256-
if do_changes: # self.styler.tooltips is not None
257-
assert self.styler.tooltips.tt_data is s_copy.tooltips.tt_data
258-
assert (
259-
self.styler.tooltips.class_properties
260-
is s_copy.tooltips.class_properties
261-
)
262-
assert self.styler.tooltips.table_styles is s_copy.tooltips.table_styles
263-
264-
# Check for non-identity
265-
assert self.styler.ctx is not s_deepcopy.ctx
266-
assert self.styler._todo is not s_deepcopy._todo
267-
assert self.styler.hidden_columns is not s_deepcopy.hidden_columns
268-
assert self.styler.cell_context is not s_deepcopy.cell_context
269-
if do_changes: # self.styler.table_style is not None
270-
assert self.styler.table_styles is not s_deepcopy.table_styles
271-
if do_changes: # self.styler.tooltips is not None
272-
assert self.styler.tooltips is not s_deepcopy.tooltips
273-
assert self.styler.tooltips.tt_data is not s_deepcopy.tooltips.tt_data
274-
assert (
275-
self.styler.tooltips.class_properties
276-
is not s_deepcopy.tooltips.class_properties
277-
)
278-
assert (
279-
self.styler.tooltips.table_styles
280-
is not s_deepcopy.tooltips.table_styles
281-
)
282-
283-
self.styler._update_ctx(self.attrs)
284-
self.styler.highlight_max()
285-
assert self.styler.ctx == s_copy.ctx
286-
assert self.styler.ctx != s_deepcopy.ctx
287-
assert self.styler._todo == s_copy._todo
288-
assert self.styler._todo != s_deepcopy._todo
289-
assert s_deepcopy._todo == []
290-
291-
equal_attributes = [
292-
"table_styles",
293-
"table_attributes",
294-
"cell_ids",
295-
"hide_index_",
296-
"hidden_columns",
297-
"cell_context",
298-
]
299-
for s2 in [s_copy, s_deepcopy]:
300-
for att in equal_attributes:
301-
assert self.styler.__dict__[att] == s2.__dict__[att]
302-
if do_changes: # self.styler.tooltips is not None
303-
tm.assert_frame_equal(self.styler.tooltips.tt_data, s2.tooltips.tt_data)
304-
assert (
305-
self.styler.tooltips.class_properties
306-
== s2.tooltips.class_properties
307-
)
308-
assert self.styler.tooltips.table_styles == s2.tooltips.table_styles
309-
310286
def test_clear(self):
311287
# updated in GH 39396
312288
tt = DataFrame({"A": [None, "tt"]})

pandas/tests/io/formats/style/test_to_latex.py

+16
Original file line numberDiff line numberDiff line change
@@ -489,3 +489,19 @@ def test_parse_latex_css_conversion_option():
489489
expected = [("command", "option--wrap")]
490490
result = _parse_latex_css_conversion(css)
491491
assert result == expected
492+
493+
494+
def test_styler_object_after_render(styler):
495+
# GH 42320
496+
pre_render = styler._copy(deepcopy=True)
497+
styler.to_latex(
498+
column_format="rllr",
499+
position="h",
500+
position_float="centering",
501+
hrules=True,
502+
label="my lab",
503+
caption="my cap",
504+
)
505+
506+
assert pre_render.table_styles == styler.table_styles
507+
assert pre_render.caption == styler.caption

0 commit comments

Comments
 (0)