Skip to content

Commit 2c33f02

Browse files
attack68simonjayhawkins
authored andcommitted
CLN: simplify Styler copy mechanics and tests (pandas-dev#42187)
1 parent 9289161 commit 2c33f02

File tree

2 files changed

+120
-123
lines changed

2 files changed

+120
-123
lines changed

pandas/io/formats/style.py

+48-27
Original file line numberDiff line numberDiff line change
@@ -966,39 +966,60 @@ def _update_ctx(self, attrs: DataFrame) -> None:
966966
self.ctx[(i, j)].extend(css_list)
967967

968968
def _copy(self, deepcopy: bool = False) -> Styler:
969-
styler = Styler(
970-
self.data,
971-
precision=self.precision,
972-
caption=self.caption,
973-
table_attributes=self.table_attributes,
974-
cell_ids=self.cell_ids,
975-
na_rep=self.na_rep,
976-
)
969+
"""
970+
Copies a Styler, allowing for deepcopy or shallow copy
977971
978-
styler.uuid = self.uuid
979-
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.
980974
981-
if deepcopy:
982-
styler.ctx = copy.deepcopy(self.ctx)
983-
styler._todo = copy.deepcopy(self._todo)
984-
styler.table_styles = copy.deepcopy(self.table_styles)
985-
styler.hidden_columns = copy.copy(self.hidden_columns)
986-
styler.cell_context = copy.deepcopy(self.cell_context)
987-
styler.tooltips = copy.deepcopy(self.tooltips)
988-
else:
989-
styler.ctx = self.ctx
990-
styler._todo = self._todo
991-
styler.table_styles = self.table_styles
992-
styler.hidden_columns = self.hidden_columns
993-
styler.cell_context = self.cell_context
994-
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)
9951019

9961020
return styler
9971021

9981022
def __copy__(self) -> Styler:
999-
"""
1000-
Deep copy by default.
1001-
"""
10021023
return self._copy(deepcopy=False)
10031024

10041025
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"]})

0 commit comments

Comments
 (0)