diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst index 1119117c411d3..c9ee055b4a6c7 100644 --- a/doc/source/whatsnew/v2.1.0.rst +++ b/doc/source/whatsnew/v2.1.0.rst @@ -33,6 +33,7 @@ Copy-on-Write improvements operating inplace like this will never work, since the selection behaves as a temporary copy. This holds true for: + - DataFrame.update / Series.update - DataFrame.fillna / Series.fillna .. _whatsnew_210.enhancements.enhancement2: diff --git a/pandas/compat/_constants.py b/pandas/compat/_constants.py index 1d7fe23b3d2ea..7ef427604ee06 100644 --- a/pandas/compat/_constants.py +++ b/pandas/compat/_constants.py @@ -17,7 +17,7 @@ PY311 = sys.version_info >= (3, 11) PYPY = platform.python_implementation() == "PyPy" ISMUSL = "musl" in (sysconfig.get_config_var("HOST_GNU_TYPE") or "") - +REF_COUNT = 2 if PY311 else 3 __all__ = [ "IS64", diff --git a/pandas/core/frame.py b/pandas/core/frame.py index ae43a44d68f1c..3ce30d2ace73f 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -49,6 +49,7 @@ from pandas._libs.hashtable import duplicated from pandas._libs.lib import is_range_indexer from pandas.compat import PYPY +from pandas.compat._constants import REF_COUNT from pandas.compat._optional import import_optional_dependency from pandas.compat.numpy import ( function as nv, @@ -57,6 +58,7 @@ from pandas.errors import ( ChainedAssignmentError, InvalidIndexError, + _chained_assignment_method_msg, _chained_assignment_msg, ) from pandas.util._decorators import ( @@ -8501,6 +8503,14 @@ def update( 1 2 500 2 3 6 """ + if not PYPY and using_copy_on_write(): + if sys.getrefcount(self) <= REF_COUNT: + warnings.warn( + _chained_assignment_method_msg, + ChainedAssignmentError, + stacklevel=2, + ) + from pandas.core.computation import expressions # TODO: Support other joins diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 68e5fbd696ab9..bcb25061eeec8 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -90,10 +90,8 @@ WriteExcelBuffer, npt, ) -from pandas.compat import ( - PY311, - PYPY, -) +from pandas.compat import PYPY +from pandas.compat._constants import REF_COUNT from pandas.compat._optional import import_optional_dependency from pandas.compat.numpy import function as nv from pandas.errors import ( @@ -7092,8 +7090,7 @@ def fillna( inplace = validate_bool_kwarg(inplace, "inplace") if inplace: if not PYPY and using_copy_on_write(): - refcount = 2 if PY311 else 3 - if sys.getrefcount(self) <= refcount: + if sys.getrefcount(self) <= REF_COUNT: warnings.warn( _chained_assignment_method_msg, ChainedAssignmentError, diff --git a/pandas/core/series.py b/pandas/core/series.py index 164b1a61b006c..43782261d3c58 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -37,10 +37,12 @@ ) from pandas._libs.lib import is_range_indexer from pandas.compat import PYPY +from pandas.compat._constants import REF_COUNT from pandas.compat.numpy import function as nv from pandas.errors import ( ChainedAssignmentError, InvalidIndexError, + _chained_assignment_method_msg, _chained_assignment_msg, ) from pandas.util._decorators import ( @@ -3435,6 +3437,13 @@ def update(self, other: Series | Sequence | Mapping) -> None: 2 3 dtype: int64 """ + if not PYPY and using_copy_on_write(): + if sys.getrefcount(self) <= REF_COUNT: + warnings.warn( + _chained_assignment_method_msg, + ChainedAssignmentError, + stacklevel=2, + ) if not isinstance(other, Series): other = Series(other) diff --git a/pandas/tests/copy_view/test_methods.py b/pandas/tests/copy_view/test_methods.py index 9e7ae9942ea90..e9952e5f4d977 100644 --- a/pandas/tests/copy_view/test_methods.py +++ b/pandas/tests/copy_view/test_methods.py @@ -1731,6 +1731,20 @@ def test_update_series(using_copy_on_write): tm.assert_series_equal(view, expected) +def test_update_chained_assignment(using_copy_on_write): + df = DataFrame({"a": [1, 2, 3]}) + ser2 = Series([100.0], index=[1]) + df_orig = df.copy() + if using_copy_on_write: + with tm.raises_chained_assignment_error(): + df["a"].update(ser2) + tm.assert_frame_equal(df, df_orig) + + with tm.raises_chained_assignment_error(): + df[["a"]].update(ser2.to_frame()) + tm.assert_frame_equal(df, df_orig) + + def test_inplace_arithmetic_series(): ser = Series([1, 2, 3]) data = get_array(ser) diff --git a/pandas/tests/series/indexing/test_indexing.py b/pandas/tests/series/indexing/test_indexing.py index 83cae8d148feb..dfc8afbdf3acb 100644 --- a/pandas/tests/series/indexing/test_indexing.py +++ b/pandas/tests/series/indexing/test_indexing.py @@ -284,11 +284,13 @@ def test_underlying_data_conversion(using_copy_on_write): df["val"] = 0 df_original = df.copy() df - df["val"].update(s) if using_copy_on_write: + with tm.raises_chained_assignment_error(): + df["val"].update(s) expected = df_original else: + df["val"].update(s) expected = DataFrame( {"a": [1, 2, 3], "b": [1, 2, 3], "c": [1, 2, 3], "val": [0, 1, 0]} ) diff --git a/pandas/tests/series/methods/test_update.py b/pandas/tests/series/methods/test_update.py index af7e629f74227..5bf134fbeeb86 100644 --- a/pandas/tests/series/methods/test_update.py +++ b/pandas/tests/series/methods/test_update.py @@ -29,10 +29,12 @@ def test_update(self, using_copy_on_write): df["c"] = df["c"].astype(object) df_orig = df.copy() - df["c"].update(Series(["foo"], index=[0])) if using_copy_on_write: + with tm.raises_chained_assignment_error(): + df["c"].update(Series(["foo"], index=[0])) expected = df_orig else: + df["c"].update(Series(["foo"], index=[0])) expected = DataFrame( [[1, np.nan, "foo"], [3, 2.0, np.nan]], columns=["a", "b", "c"] )