Skip to content

Commit cd9d391

Browse files
authored
API / CoW: Add ChainedAssignmentError for inplace ops (#54313)
1 parent 0e196b0 commit cd9d391

File tree

6 files changed

+118
-5
lines changed

6 files changed

+118
-5
lines changed

doc/source/whatsnew/v2.1.0.rst

+6
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ Copy-on-Write improvements
121121
- DataFrame.update / Series.update
122122
- DataFrame.fillna / Series.fillna
123123
- DataFrame.replace / Series.replace
124+
- DataFrame.clip / Series.clip
125+
- DataFrame.where / Series.where
126+
- DataFrame.mask / Series.mask
127+
- DataFrame.interpolate / Series.interpolate
128+
- DataFrame.ffill / Series.ffill
129+
- DataFrame.bfill / Series.bfill
124130

125131
.. _whatsnew_210.enhancements.map_na_action:
126132

pandas/core/generic.py

+55
Original file line numberDiff line numberDiff line change
@@ -7385,6 +7385,15 @@ def ffill(
73857385
dtype: float64
73867386
"""
73877387
downcast = self._deprecate_downcast(downcast, "ffill")
7388+
inplace = validate_bool_kwarg(inplace, "inplace")
7389+
if inplace:
7390+
if not PYPY and using_copy_on_write():
7391+
if sys.getrefcount(self) <= REF_COUNT:
7392+
warnings.warn(
7393+
_chained_assignment_method_msg,
7394+
ChainedAssignmentError,
7395+
stacklevel=2,
7396+
)
73887397

73897398
return self._pad_or_backfill(
73907399
"ffill",
@@ -7523,6 +7532,15 @@ def bfill(
75237532
3 4.0 7.0
75247533
"""
75257534
downcast = self._deprecate_downcast(downcast, "bfill")
7535+
inplace = validate_bool_kwarg(inplace, "inplace")
7536+
if inplace:
7537+
if not PYPY and using_copy_on_write():
7538+
if sys.getrefcount(self) <= REF_COUNT:
7539+
warnings.warn(
7540+
_chained_assignment_method_msg,
7541+
ChainedAssignmentError,
7542+
stacklevel=2,
7543+
)
75267544
return self._pad_or_backfill(
75277545
"bfill",
75287546
axis=axis,
@@ -8047,6 +8065,16 @@ def interpolate(
80478065
raise ValueError("downcast must be either None or 'infer'")
80488066

80498067
inplace = validate_bool_kwarg(inplace, "inplace")
8068+
8069+
if inplace:
8070+
if not PYPY and using_copy_on_write():
8071+
if sys.getrefcount(self) <= REF_COUNT:
8072+
warnings.warn(
8073+
_chained_assignment_method_msg,
8074+
ChainedAssignmentError,
8075+
stacklevel=2,
8076+
)
8077+
80508078
axis = self._get_axis_number(axis)
80518079

80528080
if self.empty:
@@ -8619,6 +8647,15 @@ def clip(
86198647
"""
86208648
inplace = validate_bool_kwarg(inplace, "inplace")
86218649

8650+
if inplace:
8651+
if not PYPY and using_copy_on_write():
8652+
if sys.getrefcount(self) <= REF_COUNT:
8653+
warnings.warn(
8654+
_chained_assignment_method_msg,
8655+
ChainedAssignmentError,
8656+
stacklevel=2,
8657+
)
8658+
86228659
axis = nv.validate_clip_with_axis(axis, (), kwargs)
86238660
if axis is not None:
86248661
axis = self._get_axis_number(axis)
@@ -10500,6 +10537,15 @@ def where(
1050010537
3 True True
1050110538
4 True True
1050210539
"""
10540+
inplace = validate_bool_kwarg(inplace, "inplace")
10541+
if inplace:
10542+
if not PYPY and using_copy_on_write():
10543+
if sys.getrefcount(self) <= REF_COUNT:
10544+
warnings.warn(
10545+
_chained_assignment_method_msg,
10546+
ChainedAssignmentError,
10547+
stacklevel=2,
10548+
)
1050310549
other = common.apply_if_callable(other, self)
1050410550
return self._where(cond, other, inplace, axis, level)
1050510551

@@ -10558,6 +10604,15 @@ def mask(
1055810604
level: Level | None = None,
1055910605
) -> Self | None:
1056010606
inplace = validate_bool_kwarg(inplace, "inplace")
10607+
if inplace:
10608+
if not PYPY and using_copy_on_write():
10609+
if sys.getrefcount(self) <= REF_COUNT:
10610+
warnings.warn(
10611+
_chained_assignment_method_msg,
10612+
ChainedAssignmentError,
10613+
stacklevel=2,
10614+
)
10615+
1056110616
cond = common.apply_if_callable(cond, self)
1056210617

1056310618
# see gh-21891

pandas/tests/copy_view/test_clip.py

+13
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,16 @@ def test_clip_no_op(using_copy_on_write):
6868
assert np.shares_memory(get_array(df2, "a"), get_array(df, "a"))
6969
else:
7070
assert not np.shares_memory(get_array(df2, "a"), get_array(df, "a"))
71+
72+
73+
def test_clip_chained_inplace(using_copy_on_write):
74+
df = DataFrame({"a": [1, 4, 2], "b": 1})
75+
df_orig = df.copy()
76+
if using_copy_on_write:
77+
with tm.raises_chained_assignment_error():
78+
df["a"].clip(1, 2, inplace=True)
79+
tm.assert_frame_equal(df, df_orig)
80+
81+
with tm.raises_chained_assignment_error():
82+
df[["a"]].clip(1, 2, inplace=True)
83+
tm.assert_frame_equal(df, df_orig)

pandas/tests/copy_view/test_interp_fillna.py

+14
Original file line numberDiff line numberDiff line change
@@ -361,3 +361,17 @@ def test_fillna_chained_assignment(using_copy_on_write):
361361
with tm.raises_chained_assignment_error():
362362
df[["a"]].fillna(100, inplace=True)
363363
tm.assert_frame_equal(df, df_orig)
364+
365+
366+
@pytest.mark.parametrize("func", ["interpolate", "ffill", "bfill"])
367+
def test_interpolate_chained_assignment(using_copy_on_write, func):
368+
df = DataFrame({"a": [1, np.nan, 2], "b": 1})
369+
df_orig = df.copy()
370+
if using_copy_on_write:
371+
with tm.raises_chained_assignment_error():
372+
getattr(df["a"], func)(inplace=True)
373+
tm.assert_frame_equal(df, df_orig)
374+
375+
with tm.raises_chained_assignment_error():
376+
getattr(df[["a"]], func)(inplace=True)
377+
tm.assert_frame_equal(df, df_orig)

pandas/tests/copy_view/test_methods.py

+14
Original file line numberDiff line numberDiff line change
@@ -1518,6 +1518,20 @@ def test_where_mask_noop_on_single_column(using_copy_on_write, dtype, val, func)
15181518
tm.assert_frame_equal(df, df_orig)
15191519

15201520

1521+
@pytest.mark.parametrize("func", ["mask", "where"])
1522+
def test_chained_where_mask(using_copy_on_write, func):
1523+
df = DataFrame({"a": [1, 4, 2], "b": 1})
1524+
df_orig = df.copy()
1525+
if using_copy_on_write:
1526+
with tm.raises_chained_assignment_error():
1527+
getattr(df["a"], func)(df["a"] > 2, 5, inplace=True)
1528+
tm.assert_frame_equal(df, df_orig)
1529+
1530+
with tm.raises_chained_assignment_error():
1531+
getattr(df[["a"]], func)(df["a"] > 2, 5, inplace=True)
1532+
tm.assert_frame_equal(df, df_orig)
1533+
1534+
15211535
def test_asfreq_noop(using_copy_on_write):
15221536
df = DataFrame(
15231537
{"a": [0.0, None, 2.0, 3.0]},

pandas/tests/frame/methods/test_interpolate.py

+16-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import numpy as np
22
import pytest
33

4+
from pandas.errors import ChainedAssignmentError
45
import pandas.util._test_decorators as td
56

67
from pandas import (
@@ -370,21 +371,31 @@ def test_interp_inplace(self, using_copy_on_write):
370371
expected = DataFrame({"a": [1.0, 2.0, 3.0, 4.0]})
371372
expected_cow = df.copy()
372373
result = df.copy()
373-
return_value = result["a"].interpolate(inplace=True)
374-
assert return_value is None
374+
375375
if using_copy_on_write:
376+
with tm.raises_chained_assignment_error():
377+
return_value = result["a"].interpolate(inplace=True)
378+
assert return_value is None
376379
tm.assert_frame_equal(result, expected_cow)
377380
else:
381+
return_value = result["a"].interpolate(inplace=True)
382+
assert return_value is None
378383
tm.assert_frame_equal(result, expected)
379384

380385
result = df.copy()
381386
msg = "The 'downcast' keyword in Series.interpolate is deprecated"
382-
with tm.assert_produces_warning(FutureWarning, match=msg):
383-
return_value = result["a"].interpolate(inplace=True, downcast="infer")
384-
assert return_value is None
387+
385388
if using_copy_on_write:
389+
with tm.assert_produces_warning(
390+
(FutureWarning, ChainedAssignmentError), match=msg
391+
):
392+
return_value = result["a"].interpolate(inplace=True, downcast="infer")
393+
assert return_value is None
386394
tm.assert_frame_equal(result, expected_cow)
387395
else:
396+
with tm.assert_produces_warning(FutureWarning, match=msg):
397+
return_value = result["a"].interpolate(inplace=True, downcast="infer")
398+
assert return_value is None
388399
tm.assert_frame_equal(result, expected.astype("int64"))
389400

390401
def test_interp_inplace_row(self):

0 commit comments

Comments
 (0)