Skip to content
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 @@ -131,7 +131,7 @@ Other enhancements
- Disallow :class:`DataFrame` indexer for ``iloc`` for :meth:`Series.__getitem__` and :meth:`DataFrame.__getitem__`, (:issue:`39004`)
- :meth:`Series.apply` can now accept list-like or dictionary-like arguments that aren't lists or dictionaries, e.g. ``ser.apply(np.array(["sum", "mean"]))``, which was already the case for :meth:`DataFrame.apply` (:issue:`39140`)
- :meth:`DataFrame.plot.scatter` can now accept a categorical column as the argument to ``c`` (:issue:`12380`, :issue:`31357`)
- :meth:`.Styler.set_tooltips` allows on hover tooltips to be added to styled HTML dataframes (:issue:`35643`, :issue:`21266`, :issue:`39317`)
- :meth:`.Styler.set_tooltips` allows on hover tooltips to be added to styled HTML dataframes (:issue:`35643`, :issue:`21266`, :issue:`39317`, :issue:`39708`)
- :meth:`.Styler.set_tooltips_class` and :meth:`.Styler.set_table_styles` amended to optionally allow certain css-string input arguments (:issue:`39564`)
- :meth:`.Styler.apply` now more consistently accepts ndarray function returns, i.e. in all cases for ``axis`` is ``0, 1 or None`` (:issue:`39359`)
- :meth:`.Styler.apply` and :meth:`.Styler.applymap` now raise errors if wrong format CSS is passed on render (:issue:`39660`)
Expand Down
17 changes: 15 additions & 2 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -785,16 +785,29 @@ def _copy(self, deepcopy: bool = False) -> Styler:
self.data,
precision=self.precision,
Copy link
Contributor

Choose a reason for hiding this comment

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

is there a reason we actually care about deepcopy, IOW why are we not just always a shallow copy? is this actually used somewhere?

Copy link
Contributor

Choose a reason for hiding this comment

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

Personally, I have never had use to copy a Styler. But I can see a use case where, in a notebook or report, you might have a dataframe that has some preliminary, shared styling and then some divergence where you highlight different properties as part of a narrative.

I think, off the top of my head, in this scenario you would need a deepcopy to prevent any updates from intermingling with each other, since much of the core styling I think comes from the attributes where the shallow copy will have only single shared pointed reference.

caption=self.caption,
uuid=self.uuid,
table_styles=self.table_styles,
table_attributes=self.table_attributes,
cell_ids=self.cell_ids,
na_rep=self.na_rep,
)

styler.uuid = self.uuid
styler.hidden_index = self.hidden_index

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

return styler

def __copy__(self) -> Styler:
Expand Down
111 changes: 92 additions & 19 deletions pandas/tests/io/formats/style/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,28 +72,101 @@ def test_update_ctx_flatten_multi_and_trailing_semi(self):
}
assert self.styler.ctx == expected

def test_copy(self):
s2 = copy.copy(self.styler)
assert self.styler is not s2
assert self.styler.ctx is s2.ctx # shallow
assert self.styler._todo is s2._todo

self.styler._update_ctx(self.attrs)
self.styler.highlight_max()
assert self.styler.ctx == s2.ctx
assert self.styler._todo == s2._todo

def test_deepcopy(self):
s2 = copy.deepcopy(self.styler)
assert self.styler is not s2
assert self.styler.ctx is not s2.ctx
assert self.styler._todo is not s2._todo
@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.hidden_index = not self.styler.hidden_index
self.styler.hide_columns("A")
classes = pd.DataFrame(
[["favorite-val red", ""], [None, "blue my-val"]],
index=self.df.index,
columns=self.df.columns,
)
self.styler.set_td_classes(classes)
ttips = pd.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 != s2.ctx
assert s2._todo == []
assert self.styler._todo != s2._todo
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",
"hidden_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
Expand Down