Skip to content

BUG: Incomplete Styler copy methods fix (#39708) #39975

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 10 commits into from
Mar 5, 2021
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 @@ -63,7 +63,7 @@ Other enhancements
- :meth:`DataFrame.apply` can now accept non-callable DataFrame properties as strings, e.g. ``df.apply("size")``, which was already the case for :meth:`Series.apply` (:issue:`39116`)
- :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 @@ -781,16 +781,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