diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 847f514451add..c9da273b99ce9 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -8903,7 +8903,7 @@ def update( ChainedAssignmentError, stacklevel=2, ) - elif not PYPY and not using_copy_on_write(): + elif not PYPY and not using_copy_on_write() and self._is_view_after_cow_rules(): if sys.getrefcount(self) <= REF_COUNT: warnings.warn( _chained_assignment_warning_method_msg, diff --git a/pandas/core/generic.py b/pandas/core/generic.py index fb9f987a29bbf..44954e3781f5d 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -668,6 +668,14 @@ def _get_cleaned_column_resolvers(self) -> dict[Hashable, Series]: def _info_axis(self) -> Index: return getattr(self, self._info_axis_name) + def _is_view_after_cow_rules(self): + # Only to be used in cases of chained assignment checks, this is a + # simplified check that assumes that either the whole object is a view + # or a copy + if len(self._mgr.blocks) == 0: # type: ignore[union-attr] + return False + return self._mgr.blocks[0].refs.has_reference() # type: ignore[union-attr] + @property def shape(self) -> tuple[int, ...]: """ @@ -7268,7 +7276,11 @@ def fillna( ChainedAssignmentError, stacklevel=2, ) - elif not PYPY and not using_copy_on_write(): + elif ( + not PYPY + and not using_copy_on_write() + and self._is_view_after_cow_rules() + ): ctr = sys.getrefcount(self) ref_count = REF_COUNT if isinstance(self, ABCSeries) and _check_cacher(self): @@ -7550,7 +7562,11 @@ def ffill( ChainedAssignmentError, stacklevel=2, ) - elif not PYPY and not using_copy_on_write(): + elif ( + not PYPY + and not using_copy_on_write() + and self._is_view_after_cow_rules() + ): ctr = sys.getrefcount(self) ref_count = REF_COUNT if isinstance(self, ABCSeries) and _check_cacher(self): @@ -7733,7 +7749,11 @@ def bfill( ChainedAssignmentError, stacklevel=2, ) - elif not PYPY and not using_copy_on_write(): + elif ( + not PYPY + and not using_copy_on_write() + and self._is_view_after_cow_rules() + ): ctr = sys.getrefcount(self) ref_count = REF_COUNT if isinstance(self, ABCSeries) and _check_cacher(self): @@ -7899,7 +7919,11 @@ def replace( ChainedAssignmentError, stacklevel=2, ) - elif not PYPY and not using_copy_on_write(): + elif ( + not PYPY + and not using_copy_on_write() + and self._is_view_after_cow_rules() + ): ctr = sys.getrefcount(self) ref_count = REF_COUNT if isinstance(self, ABCSeries) and _check_cacher(self): @@ -8340,7 +8364,11 @@ def interpolate( ChainedAssignmentError, stacklevel=2, ) - elif not PYPY and not using_copy_on_write(): + elif ( + not PYPY + and not using_copy_on_write() + and self._is_view_after_cow_rules() + ): ctr = sys.getrefcount(self) ref_count = REF_COUNT if isinstance(self, ABCSeries) and _check_cacher(self): @@ -8978,7 +9006,11 @@ def clip( ChainedAssignmentError, stacklevel=2, ) - elif not PYPY and not using_copy_on_write(): + elif ( + not PYPY + and not using_copy_on_write() + and self._is_view_after_cow_rules() + ): ctr = sys.getrefcount(self) ref_count = REF_COUNT if isinstance(self, ABCSeries) and hasattr(self, "_cacher"): @@ -10926,7 +10958,11 @@ def where( ChainedAssignmentError, stacklevel=2, ) - elif not PYPY and not using_copy_on_write(): + elif ( + not PYPY + and not using_copy_on_write() + and self._is_view_after_cow_rules() + ): ctr = sys.getrefcount(self) ref_count = REF_COUNT if isinstance(self, ABCSeries) and hasattr(self, "_cacher"): @@ -11005,7 +11041,11 @@ def mask( ChainedAssignmentError, stacklevel=2, ) - elif not PYPY and not using_copy_on_write(): + elif ( + not PYPY + and not using_copy_on_write() + and self._is_view_after_cow_rules() + ): ctr = sys.getrefcount(self) ref_count = REF_COUNT if isinstance(self, ABCSeries) and hasattr(self, "_cacher"): diff --git a/pandas/core/series.py b/pandas/core/series.py index ff03b5071e3b1..207f30b68fde2 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -3580,7 +3580,7 @@ def update(self, other: Series | Sequence | Mapping) -> None: ChainedAssignmentError, stacklevel=2, ) - elif not PYPY and not using_copy_on_write(): + elif not PYPY and not using_copy_on_write() and self._is_view_after_cow_rules(): ctr = sys.getrefcount(self) ref_count = REF_COUNT if _check_cacher(self): diff --git a/pandas/tests/copy_view/test_clip.py b/pandas/tests/copy_view/test_clip.py index 7ed6a1f803ead..7c87646424e2f 100644 --- a/pandas/tests/copy_view/test_clip.py +++ b/pandas/tests/copy_view/test_clip.py @@ -92,10 +92,10 @@ def test_clip_chained_inplace(using_copy_on_write): with tm.assert_produces_warning(FutureWarning, match="inplace method"): df["a"].clip(1, 2, inplace=True) - with tm.assert_produces_warning(FutureWarning, match="inplace method"): + with tm.assert_produces_warning(None): with option_context("mode.chained_assignment", None): df[["a"]].clip(1, 2, inplace=True) - with tm.assert_produces_warning(FutureWarning, match="inplace method"): + with tm.assert_produces_warning(None): with option_context("mode.chained_assignment", None): df[df["a"] > 1].clip(1, 2, inplace=True) diff --git a/pandas/tests/copy_view/test_interp_fillna.py b/pandas/tests/copy_view/test_interp_fillna.py index cdb06de90a568..5091fd9aed8a4 100644 --- a/pandas/tests/copy_view/test_interp_fillna.py +++ b/pandas/tests/copy_view/test_interp_fillna.py @@ -366,11 +366,11 @@ def test_fillna_chained_assignment(using_copy_on_write): df[["a"]].fillna(100, inplace=True) tm.assert_frame_equal(df, df_orig) else: - with tm.assert_produces_warning(FutureWarning, match="inplace method"): + with tm.assert_produces_warning(None): with option_context("mode.chained_assignment", None): df[["a"]].fillna(100, inplace=True) - with tm.assert_produces_warning(FutureWarning, match="inplace method"): + with tm.assert_produces_warning(None): with option_context("mode.chained_assignment", None): df[df.a > 5].fillna(100, inplace=True) @@ -394,10 +394,10 @@ def test_interpolate_chained_assignment(using_copy_on_write, func): with tm.assert_produces_warning(FutureWarning, match="inplace method"): getattr(df["a"], func)(inplace=True) - with tm.assert_produces_warning(FutureWarning, match="inplace method"): + with tm.assert_produces_warning(None): with option_context("mode.chained_assignment", None): getattr(df[["a"]], func)(inplace=True) - with tm.assert_produces_warning(FutureWarning, match="inplace method"): + with tm.assert_produces_warning(None): with option_context("mode.chained_assignment", None): getattr(df[df["a"] > 1], func)(inplace=True) diff --git a/pandas/tests/copy_view/test_methods.py b/pandas/tests/copy_view/test_methods.py index ba8e4bd684198..62214293df912 100644 --- a/pandas/tests/copy_view/test_methods.py +++ b/pandas/tests/copy_view/test_methods.py @@ -1562,11 +1562,11 @@ def test_chained_where_mask(using_copy_on_write, func): with tm.assert_produces_warning(FutureWarning, match="inplace method"): getattr(df["a"], func)(df["a"] > 2, 5, inplace=True) - with tm.assert_produces_warning(FutureWarning, match="inplace method"): + with tm.assert_produces_warning(None): with option_context("mode.chained_assignment", None): getattr(df[["a"]], func)(df["a"] > 2, 5, inplace=True) - with tm.assert_produces_warning(FutureWarning, match="inplace method"): + with tm.assert_produces_warning(None): with option_context("mode.chained_assignment", None): getattr(df[df["a"] > 1], func)(df["a"] > 2, 5, inplace=True) @@ -1840,11 +1840,11 @@ def test_update_chained_assignment(using_copy_on_write): with tm.assert_produces_warning(FutureWarning, match="inplace method"): df["a"].update(ser2) - with tm.assert_produces_warning(FutureWarning, match="inplace method"): + with tm.assert_produces_warning(None): with option_context("mode.chained_assignment", None): df[["a"]].update(ser2.to_frame()) - with tm.assert_produces_warning(FutureWarning, match="inplace method"): + with tm.assert_produces_warning(None): with option_context("mode.chained_assignment", None): df[df["a"] > 1].update(ser2.to_frame()) diff --git a/pandas/tests/copy_view/test_replace.py b/pandas/tests/copy_view/test_replace.py index 3d8559a1905fc..eb3b1a5ef68e8 100644 --- a/pandas/tests/copy_view/test_replace.py +++ b/pandas/tests/copy_view/test_replace.py @@ -401,11 +401,11 @@ def test_replace_chained_assignment(using_copy_on_write): df[["a"]].replace(1, 100, inplace=True) tm.assert_frame_equal(df, df_orig) else: - with tm.assert_produces_warning(FutureWarning, match="inplace method"): + with tm.assert_produces_warning(None): with option_context("mode.chained_assignment", None): df[["a"]].replace(1, 100, inplace=True) - with tm.assert_produces_warning(FutureWarning, match="inplace method"): + with tm.assert_produces_warning(None): with option_context("mode.chained_assignment", None): df[df.a > 5].replace(1, 100, inplace=True) diff --git a/pandas/tests/indexing/multiindex/test_chaining_and_caching.py b/pandas/tests/indexing/multiindex/test_chaining_and_caching.py index 01e5db87ce456..0dd1a56890fee 100644 --- a/pandas/tests/indexing/multiindex/test_chaining_and_caching.py +++ b/pandas/tests/indexing/multiindex/test_chaining_and_caching.py @@ -34,12 +34,12 @@ def test_detect_chained_assignment(using_copy_on_write, warn_copy_on_write): with tm.raises_chained_assignment_error(): zed["eyes"]["right"].fillna(value=555, inplace=True) elif warn_copy_on_write: - with tm.assert_produces_warning(FutureWarning, match="inplace method"): + with tm.assert_produces_warning(None): zed["eyes"]["right"].fillna(value=555, inplace=True) else: msg = "A value is trying to be set on a copy of a slice from a DataFrame" with pytest.raises(SettingWithCopyError, match=msg): - with tm.assert_produces_warning(FutureWarning, match="inplace method"): + with tm.assert_produces_warning(None): zed["eyes"]["right"].fillna(value=555, inplace=True)