From d9aefbf9b75d7771dc79c53c2afbc921e45e0fb6 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Fri, 6 Oct 2023 15:04:06 +0200 Subject: [PATCH 1/9] CoW: add warning mode for cases that will change behaviour --- pandas/_config/__init__.py | 14 +++- pandas/conftest.py | 13 +++- pandas/core/config_init.py | 6 +- pandas/core/generic.py | 3 +- pandas/core/internals/managers.py | 19 ++++-- pandas/tests/copy_view/test_indexing.py | 68 +++++++++++++------ pandas/tests/copy_view/test_methods.py | 25 +++++-- pandas/tests/extension/conftest.py | 2 +- pandas/tests/frame/indexing/test_indexing.py | 6 +- pandas/tests/frame/indexing/test_setitem.py | 10 ++- pandas/tests/frame/indexing/test_xs.py | 11 +-- pandas/tests/frame/test_block_internals.py | 10 ++- pandas/tests/frame/test_constructors.py | 9 ++- pandas/tests/groupby/test_groupby.py | 2 +- .../series/accessors/test_dt_accessor.py | 6 +- pandas/tests/series/indexing/test_datetime.py | 18 ++--- pandas/tests/series/methods/test_copy.py | 1 + pandas/tests/series/methods/test_fillna.py | 4 +- pandas/tests/series/methods/test_rename.py | 6 +- pandas/tests/series/test_constructors.py | 6 +- pandas/util/_test_decorators.py | 4 +- 21 files changed, 175 insertions(+), 68 deletions(-) diff --git a/pandas/_config/__init__.py b/pandas/_config/__init__.py index daeb135f5bcf7..97784c924dab4 100644 --- a/pandas/_config/__init__.py +++ b/pandas/_config/__init__.py @@ -15,6 +15,7 @@ "option_context", "options", "using_copy_on_write", + "warn_copy_on_write", ] from pandas._config import config from pandas._config import dates # pyright: ignore[reportUnusedImport] # noqa: F401 @@ -32,7 +33,18 @@ def using_copy_on_write() -> bool: _mode_options = _global_config["mode"] - return _mode_options["copy_on_write"] and _mode_options["data_manager"] == "block" + return ( + _mode_options["copy_on_write"] is True + and _mode_options["data_manager"] == "block" + ) + + +def warn_copy_on_write() -> bool: + _mode_options = _global_config["mode"] + return ( + _mode_options["copy_on_write"] == "warn" + and _mode_options["data_manager"] == "block" + ) def using_nullable_dtypes() -> bool: diff --git a/pandas/conftest.py b/pandas/conftest.py index 62f22921f0482..92a6d9e033bc3 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -1994,7 +1994,18 @@ def using_copy_on_write() -> bool: Fixture to check if Copy-on-Write is enabled. """ return ( - pd.options.mode.copy_on_write + pd.options.mode.copy_on_write is True + and _get_option("mode.data_manager", silent=True) == "block" + ) + + +@pytest.fixture +def warn_copy_on_write() -> bool: + """ + Fixture to check if Copy-on-Write is enabled. + """ + return ( + pd.options.mode.copy_on_write == "warn" and _get_option("mode.data_manager", silent=True) == "block" ) diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py index 4652acdcae287..a8b63f97141c2 100644 --- a/pandas/core/config_init.py +++ b/pandas/core/config_init.py @@ -476,9 +476,11 @@ def use_inf_as_na_cb(key) -> None: "copy_on_write", # Get the default from an environment variable, if set, otherwise defaults # to False. This environment variable can be set for testing. - os.environ.get("PANDAS_COPY_ON_WRITE", "0") == "1", + "warn" + if os.environ.get("PANDAS_COPY_ON_WRITE", "0") == "warn" + else os.environ.get("PANDAS_COPY_ON_WRITE", "0") == "1", copy_on_write_doc, - validator=is_bool, + validator=is_one_of_factory([True, False, "warn"]), ) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index e131d689b6a40..8eb56eea3d3df 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -30,6 +30,7 @@ from pandas._config import ( config, using_copy_on_write, + warn_copy_on_write, ) from pandas._libs import lib @@ -4396,7 +4397,7 @@ def _check_setitem_copy(self, t: str = "setting", force: bool_t = False): df.iloc[0:5]['group'] = 'a' """ - if using_copy_on_write(): + if using_copy_on_write() or warn_copy_on_write(): return # return early if the check is not needed diff --git a/pandas/core/internals/managers.py b/pandas/core/internals/managers.py index b1db2d2e708e8..13c93c5b9b9a9 100644 --- a/pandas/core/internals/managers.py +++ b/pandas/core/internals/managers.py @@ -16,7 +16,10 @@ import numpy as np -from pandas._config import using_copy_on_write +from pandas._config import ( + using_copy_on_write, + warn_copy_on_write, +) from pandas._libs import ( internals as libinternals, @@ -1988,9 +1991,17 @@ def setitem_inplace(self, indexer, value) -> None: in place, not returning a new Manager (and Block), and thus never changing the dtype. """ - if using_copy_on_write() and not self._has_no_reference(0): - self.blocks = (self._block.copy(),) - self._cache.clear() + if not self._has_no_reference(0): + if using_copy_on_write(): + self.blocks = (self._block.copy(),) + self._cache.clear() + elif warn_copy_on_write(): + warnings.warn( + "Setting value on view: behaviour will change in pandas 3.0 " + "with Copy-on-Write ...", + FutureWarning, + stacklevel=find_stack_level(), + ) super().setitem_inplace(indexer, value) diff --git a/pandas/tests/copy_view/test_indexing.py b/pandas/tests/copy_view/test_indexing.py index ebb25bd5c57d3..711d1bd26363f 100644 --- a/pandas/tests/copy_view/test_indexing.py +++ b/pandas/tests/copy_view/test_indexing.py @@ -139,7 +139,9 @@ def test_subset_row_slice(backend, using_copy_on_write): @pytest.mark.parametrize( "dtype", ["int64", "float64"], ids=["single-block", "mixed-block"] ) -def test_subset_column_slice(backend, using_copy_on_write, using_array_manager, dtype): +def test_subset_column_slice( + backend, using_copy_on_write, warn_copy_on_write, using_array_manager, dtype +): # Case: taking a subset of the columns of a DataFrame using a slice # + afterwards modifying the subset dtype_backend, DataFrame, _ = backend @@ -159,10 +161,14 @@ def test_subset_column_slice(backend, using_copy_on_write, using_array_manager, subset.iloc[0, 0] = 0 assert not np.shares_memory(get_array(subset, "b"), get_array(df, "b")) - else: # we only get a warning in case of a single block - warn = SettingWithCopyWarning if single_block else None + # TODO + warn = ( + SettingWithCopyWarning + if (single_block and not warn_copy_on_write) + else None + ) with pd.option_context("chained_assignment", "warn"): with tm.assert_produces_warning(warn): subset.iloc[0, 0] = 0 @@ -303,7 +309,9 @@ def test_subset_iloc_rows_columns( [slice(0, 2), np.array([True, True, False]), np.array([0, 1])], ids=["slice", "mask", "array"], ) -def test_subset_set_with_row_indexer(backend, indexer_si, indexer, using_copy_on_write): +def test_subset_set_with_row_indexer( + backend, indexer_si, indexer, using_copy_on_write, warn_copy_on_write +): # Case: setting values with a row indexer on a viewing subset # subset[indexer] = value and subset.iloc[indexer] = value _, DataFrame, _ = backend @@ -318,7 +326,8 @@ def test_subset_set_with_row_indexer(backend, indexer_si, indexer, using_copy_on ): pytest.skip("setitem with labels selects on columns") - if using_copy_on_write: + # TODO + if using_copy_on_write or warn_copy_on_write: indexer_si(subset)[indexer] = 0 else: # INFO iloc no longer raises warning since pandas 1.4 @@ -340,7 +349,7 @@ def test_subset_set_with_row_indexer(backend, indexer_si, indexer, using_copy_on tm.assert_frame_equal(df, df_orig) -def test_subset_set_with_mask(backend, using_copy_on_write): +def test_subset_set_with_mask(backend, using_copy_on_write, warn_copy_on_write): # Case: setting values with a mask on a viewing subset: subset[mask] = value _, DataFrame, _ = backend df = DataFrame({"a": [1, 2, 3, 4], "b": [4, 5, 6, 7], "c": [0.1, 0.2, 0.3, 0.4]}) @@ -349,7 +358,8 @@ def test_subset_set_with_mask(backend, using_copy_on_write): mask = subset > 3 - if using_copy_on_write: + # TODO + if using_copy_on_write or warn_copy_on_write: subset[mask] = 0 else: with pd.option_context("chained_assignment", "warn"): @@ -370,7 +380,7 @@ def test_subset_set_with_mask(backend, using_copy_on_write): tm.assert_frame_equal(df, df_orig) -def test_subset_set_column(backend, using_copy_on_write): +def test_subset_set_column(backend, using_copy_on_write, warn_copy_on_write): # Case: setting a single column on a viewing subset -> subset[col] = value dtype_backend, DataFrame, _ = backend df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [0.1, 0.2, 0.3]}) @@ -382,7 +392,8 @@ def test_subset_set_column(backend, using_copy_on_write): else: arr = pd.array([10, 11], dtype="Int64") - if using_copy_on_write: + # TODO + if using_copy_on_write or warn_copy_on_write: subset["a"] = arr else: with pd.option_context("chained_assignment", "warn"): @@ -472,7 +483,7 @@ def test_subset_set_column_with_loc2(backend, using_copy_on_write, using_array_m @pytest.mark.parametrize( "dtype", ["int64", "float64"], ids=["single-block", "mixed-block"] ) -def test_subset_set_columns(backend, using_copy_on_write, dtype): +def test_subset_set_columns(backend, using_copy_on_write, warn_copy_on_write, dtype): # Case: setting multiple columns on a viewing subset # -> subset[[col1, col2]] = value dtype_backend, DataFrame, _ = backend @@ -482,7 +493,8 @@ def test_subset_set_columns(backend, using_copy_on_write, dtype): df_orig = df.copy() subset = df[1:3] - if using_copy_on_write: + # TODO + if using_copy_on_write or warn_copy_on_write: subset[["a", "c"]] = 0 else: with pd.option_context("chained_assignment", "warn"): @@ -879,7 +891,9 @@ def test_del_series(backend): # Accessing column as Series -def test_column_as_series(backend, using_copy_on_write, using_array_manager): +def test_column_as_series( + backend, using_copy_on_write, warn_copy_on_write, using_array_manager +): # Case: selecting a single column now also uses Copy-on-Write dtype_backend, DataFrame, Series = backend df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [0.1, 0.2, 0.3]}) @@ -892,10 +906,14 @@ def test_column_as_series(backend, using_copy_on_write, using_array_manager): if using_copy_on_write or using_array_manager: s[0] = 0 else: - warn = SettingWithCopyWarning if dtype_backend == "numpy" else None - with pd.option_context("chained_assignment", "warn"): - with tm.assert_produces_warning(warn): + if warn_copy_on_write: + with tm.assert_produces_warning(FutureWarning): s[0] = 0 + else: + warn = SettingWithCopyWarning if dtype_backend == "numpy" else None + with pd.option_context("chained_assignment", "warn"): + with tm.assert_produces_warning(warn): + s[0] = 0 expected = Series([0, 2, 3], name="a") tm.assert_series_equal(s, expected) @@ -910,7 +928,7 @@ def test_column_as_series(backend, using_copy_on_write, using_array_manager): def test_column_as_series_set_with_upcast( - backend, using_copy_on_write, using_array_manager + backend, using_copy_on_write, using_array_manager, warn_copy_on_write ): # Case: selecting a single column now also uses Copy-on-Write -> when # setting a value causes an upcast, we don't need to update the parent @@ -921,10 +939,12 @@ def test_column_as_series_set_with_upcast( s = df["a"] if dtype_backend == "nullable": - with pytest.raises(TypeError, match="Invalid value"): - s[0] = "foo" + warn = FutureWarning if warn_copy_on_write else None + with tm.assert_produces_warning(warn): + with pytest.raises(TypeError, match="Invalid value"): + s[0] = "foo" expected = Series([1, 2, 3], name="a") - elif using_copy_on_write or using_array_manager: + elif using_copy_on_write or warn_copy_on_write or using_array_manager: with tm.assert_produces_warning(FutureWarning, match="incompatible dtype"): s[0] = "foo" expected = Series(["foo", 2, 3], dtype=object, name="a") @@ -962,7 +982,12 @@ def test_column_as_series_set_with_upcast( ids=["getitem", "loc", "iloc"], ) def test_column_as_series_no_item_cache( - request, backend, method, using_copy_on_write, using_array_manager + request, + backend, + method, + using_copy_on_write, + warn_copy_on_write, + using_array_manager, ): # Case: selecting a single column (which now also uses Copy-on-Write to protect # the view) should always give a new object (i.e. not make use of a cache) @@ -979,7 +1004,8 @@ def test_column_as_series_no_item_cache( else: assert s1 is s2 - if using_copy_on_write or using_array_manager: + # TODO + if using_copy_on_write or warn_copy_on_write or using_array_manager: s1.iloc[0] = 0 else: warn = SettingWithCopyWarning if dtype_backend == "numpy" else None diff --git a/pandas/tests/copy_view/test_methods.py b/pandas/tests/copy_view/test_methods.py index 76b974330cbf1..91c62d04fd435 100644 --- a/pandas/tests/copy_view/test_methods.py +++ b/pandas/tests/copy_view/test_methods.py @@ -1651,7 +1651,7 @@ def test_isetitem_frame(using_copy_on_write): @pytest.mark.parametrize("key", ["a", ["a"]]) -def test_get(using_copy_on_write, key): +def test_get(using_copy_on_write, warn_copy_on_write, key): df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) df_orig = df.copy() @@ -1665,7 +1665,12 @@ def test_get(using_copy_on_write, key): else: # for non-CoW it depends on whether we got a Series or DataFrame if it # is a view or copy or triggers a warning or not - warn = SettingWithCopyWarning if isinstance(key, list) else None + # TODO(CoW) should warn + warn = ( + (None if warn_copy_on_write else SettingWithCopyWarning) + if isinstance(key, list) + else None + ) with pd.option_context("chained_assignment", "warn"): with tm.assert_produces_warning(warn): result.iloc[0] = 0 @@ -1680,7 +1685,9 @@ def test_get(using_copy_on_write, key): @pytest.mark.parametrize( "dtype", ["int64", "float64"], ids=["single-block", "mixed-block"] ) -def test_xs(using_copy_on_write, using_array_manager, axis, key, dtype): +def test_xs( + using_copy_on_write, warn_copy_on_write, using_array_manager, axis, key, dtype +): single_block = (dtype == "int64") and not using_array_manager is_view = single_block or (using_array_manager and axis == 1) df = DataFrame( @@ -1695,8 +1702,13 @@ def test_xs(using_copy_on_write, using_array_manager, axis, key, dtype): elif using_copy_on_write: assert result._mgr._has_no_reference(0) + # TODO(CoW) should warn in case of is_view if using_copy_on_write or is_view: result.iloc[0] = 0 + elif warn_copy_on_write: + warn = FutureWarning if single_block else None + with tm.assert_produces_warning(warn): + result.iloc[0] = 0 else: with pd.option_context("chained_assignment", "warn"): with tm.assert_produces_warning(SettingWithCopyWarning): @@ -1710,7 +1722,9 @@ def test_xs(using_copy_on_write, using_array_manager, axis, key, dtype): @pytest.mark.parametrize("axis", [0, 1]) @pytest.mark.parametrize("key, level", [("l1", 0), (2, 1)]) -def test_xs_multiindex(using_copy_on_write, using_array_manager, key, level, axis): +def test_xs_multiindex( + using_copy_on_write, warn_copy_on_write, using_array_manager, key, level, axis +): arr = np.arange(18).reshape(6, 3) index = MultiIndex.from_product([["l1", "l2"], [1, 2, 3]], names=["lev1", "lev2"]) df = DataFrame(arr, index=index, columns=list("abc")) @@ -1725,8 +1739,9 @@ def test_xs_multiindex(using_copy_on_write, using_array_manager, key, level, axi get_array(df, df.columns[0]), get_array(result, result.columns[0]) ) + # TODO(CoW) should warn warn = ( - SettingWithCopyWarning + (None if warn_copy_on_write else SettingWithCopyWarning) if not using_copy_on_write and not using_array_manager else None ) diff --git a/pandas/tests/extension/conftest.py b/pandas/tests/extension/conftest.py index a94f7de283d01..30b10541c8f66 100644 --- a/pandas/tests/extension/conftest.py +++ b/pandas/tests/extension/conftest.py @@ -215,6 +215,6 @@ def using_copy_on_write() -> bool: Fixture to check if Copy-on-Write is enabled. """ return ( - options.mode.copy_on_write + options.mode.copy_on_write is True and _get_option("mode.data_manager", silent=True) == "block" ) diff --git a/pandas/tests/frame/indexing/test_indexing.py b/pandas/tests/frame/indexing/test_indexing.py index de8df15a9d747..ba8d713df5787 100644 --- a/pandas/tests/frame/indexing/test_indexing.py +++ b/pandas/tests/frame/indexing/test_indexing.py @@ -288,7 +288,7 @@ def test_setattr_column(self): df.foobar = 5 assert (df.foobar == 5).all() - def test_setitem(self, float_frame, using_copy_on_write): + def test_setitem(self, float_frame, using_copy_on_write, warn_copy_on_write): # not sure what else to do here series = float_frame["A"][::2] float_frame["col5"] = series @@ -324,7 +324,7 @@ def test_setitem(self, float_frame, using_copy_on_write): smaller = float_frame[:2] msg = r"\nA value is trying to be set on a copy of a slice from a DataFrame" - if using_copy_on_write: + if using_copy_on_write or warn_copy_on_write: # With CoW, adding a new column doesn't raise a warning smaller["col10"] = ["1", "2"] else: @@ -1295,7 +1295,7 @@ def test_setting_mismatched_na_into_nullable_fails( ): # GH#44514 don't cast mismatched nulls to pd.NA df = DataFrame({"A": [1, 2, 3]}, dtype=any_numeric_ea_dtype) - ser = df["A"] + ser = df["A"].copy() arr = ser._values msg = "|".join( diff --git a/pandas/tests/frame/indexing/test_setitem.py b/pandas/tests/frame/indexing/test_setitem.py index ccc1249088f9a..5076767f2e438 100644 --- a/pandas/tests/frame/indexing/test_setitem.py +++ b/pandas/tests/frame/indexing/test_setitem.py @@ -1271,7 +1271,9 @@ def test_setitem_not_operating_inplace(self, value, set_value, indexer): tm.assert_frame_equal(view, expected) @td.skip_array_manager_invalid_test - def test_setitem_column_update_inplace(self, using_copy_on_write): + def test_setitem_column_update_inplace( + self, using_copy_on_write, warn_copy_on_write + ): # https://github.com/pandas-dev/pandas/issues/47172 labels = [f"c{i}" for i in range(10)] @@ -1279,8 +1281,10 @@ def test_setitem_column_update_inplace(self, using_copy_on_write): values = df._mgr.blocks[0].values if not using_copy_on_write: - for label in df.columns: - df[label][label] = 1 + warn = FutureWarning if warn_copy_on_write else None + with tm.assert_produces_warning(warn): + for label in df.columns: + df[label][label] = 1 # diagonal values all updated assert np.all(values[np.arange(10), np.arange(10)] == 1) diff --git a/pandas/tests/frame/indexing/test_xs.py b/pandas/tests/frame/indexing/test_xs.py index 492dd387971c8..772738ae460b9 100644 --- a/pandas/tests/frame/indexing/test_xs.py +++ b/pandas/tests/frame/indexing/test_xs.py @@ -198,14 +198,17 @@ def test_xs_level_eq_2(self): tm.assert_frame_equal(result, expected) def test_xs_setting_with_copy_error( - self, multiindex_dataframe_random_data, using_copy_on_write + self, + multiindex_dataframe_random_data, + using_copy_on_write, + warn_copy_on_write, ): # this is a copy in 0.14 df = multiindex_dataframe_random_data df_orig = df.copy() result = df.xs("two", level="second") - if using_copy_on_write: + if using_copy_on_write or warn_copy_on_write: result[:] = 10 else: # setting this will give a SettingWithCopyError @@ -216,14 +219,14 @@ def test_xs_setting_with_copy_error( tm.assert_frame_equal(df, df_orig) def test_xs_setting_with_copy_error_multiple( - self, four_level_index_dataframe, using_copy_on_write + self, four_level_index_dataframe, using_copy_on_write, warn_copy_on_write ): # this is a copy in 0.14 df = four_level_index_dataframe df_orig = df.copy() result = df.xs(("a", 4), level=["one", "four"]) - if using_copy_on_write: + if using_copy_on_write or warn_copy_on_write: result[:] = 10 else: # setting this will give a SettingWithCopyError diff --git a/pandas/tests/frame/test_block_internals.py b/pandas/tests/frame/test_block_internals.py index 9e8d92e832d01..c0fb3ccbd7c79 100644 --- a/pandas/tests/frame/test_block_internals.py +++ b/pandas/tests/frame/test_block_internals.py @@ -331,7 +331,7 @@ def test_is_mixed_type(self, float_frame, float_string_frame): assert not float_frame._is_mixed_type assert float_string_frame._is_mixed_type - def test_stale_cached_series_bug_473(self, using_copy_on_write): + def test_stale_cached_series_bug_473(self, using_copy_on_write, warn_copy_on_write): # this is chained, but ok with option_context("chained_assignment", None): Y = DataFrame( @@ -344,6 +344,9 @@ def test_stale_cached_series_bug_473(self, using_copy_on_write): if using_copy_on_write: with tm.raises_chained_assignment_error(): Y["g"]["c"] = np.nan + elif warn_copy_on_write: + with tm.assert_produces_warning(FutureWarning): + Y["g"]["c"] = np.nan else: Y["g"]["c"] = np.nan repr(Y) @@ -354,13 +357,16 @@ def test_stale_cached_series_bug_473(self, using_copy_on_write): else: assert pd.isna(Y["g"]["c"]) + @pytest.mark.filterwarnings("ignore:Setting value on view:FutureWarning") def test_strange_column_corruption_issue(self, using_copy_on_write): # TODO(wesm): Unclear how exactly this is related to internal matters df = DataFrame(index=[0, 1]) df[0] = np.nan wasCol = {} - with tm.assert_produces_warning(PerformanceWarning): + with tm.assert_produces_warning( + PerformanceWarning, raise_on_extra_warnings=False + ): for i, dt in enumerate(df.index): for col in range(100, 200): if col not in wasCol: diff --git a/pandas/tests/frame/test_constructors.py b/pandas/tests/frame/test_constructors.py index 543a5be9544cc..d8e384477e963 100644 --- a/pandas/tests/frame/test_constructors.py +++ b/pandas/tests/frame/test_constructors.py @@ -293,16 +293,21 @@ def test_constructor_dtype_copy(self): new_df["col1"] = 200.0 assert orig_df["col1"][0] == 1.0 - def test_constructor_dtype_nocast_view_dataframe(self, using_copy_on_write): + def test_constructor_dtype_nocast_view_dataframe( + self, using_copy_on_write, warn_copy_on_write + ): df = DataFrame([[1, 2]]) should_be_view = DataFrame(df, dtype=df[0].dtype) if using_copy_on_write: should_be_view.iloc[0, 0] = 99 assert df.values[0, 0] == 1 else: - should_be_view[0][0] = 99 + warn = FutureWarning if warn_copy_on_write else None + with tm.assert_produces_warning(warn): + should_be_view[0][0] = 99 assert df.values[0, 0] == 99 + @pytest.mark.filterwarnings("ignore:Setting value on view:FutureWarning") def test_constructor_dtype_nocast_view_2d_array( self, using_array_manager, using_copy_on_write ): diff --git a/pandas/tests/groupby/test_groupby.py b/pandas/tests/groupby/test_groupby.py index 4ca8b0e317bd2..4eb007bbac205 100644 --- a/pandas/tests/groupby/test_groupby.py +++ b/pandas/tests/groupby/test_groupby.py @@ -46,7 +46,7 @@ def test_groupby_std_datetimelike(): ser = Series(tdi) ser[::5] *= 2 # get different std for different groups - df = ser.to_frame("A") + df = ser.to_frame("A").copy() df["B"] = ser + Timestamp(0) df["C"] = ser + Timestamp(0, tz="UTC") diff --git a/pandas/tests/series/accessors/test_dt_accessor.py b/pandas/tests/series/accessors/test_dt_accessor.py index fba16749c026e..823ccd0c18e9a 100644 --- a/pandas/tests/series/accessors/test_dt_accessor.py +++ b/pandas/tests/series/accessors/test_dt_accessor.py @@ -281,7 +281,7 @@ def test_dt_accessor_ambiguous_freq_conversions(self): expected = Series(exp_values, name="xxx") tm.assert_series_equal(ser, expected) - def test_dt_accessor_not_writeable(self, using_copy_on_write): + def test_dt_accessor_not_writeable(self, using_copy_on_write, warn_copy_on_write): # no setting allowed ser = Series(date_range("20130101", periods=5, freq="D"), name="xxx") with pytest.raises(ValueError, match="modifications"): @@ -293,6 +293,10 @@ def test_dt_accessor_not_writeable(self, using_copy_on_write): if using_copy_on_write: with tm.raises_chained_assignment_error(): ser.dt.hour[0] = 5 + elif warn_copy_on_write: + # TODO should warn + # with tm.assert_produces_warning(FutureWarning): + ser.dt.hour[0] = 5 else: with pytest.raises(SettingWithCopyError, match=msg): ser.dt.hour[0] = 5 diff --git a/pandas/tests/series/indexing/test_datetime.py b/pandas/tests/series/indexing/test_datetime.py index 317967bcbb7ff..6799f13688027 100644 --- a/pandas/tests/series/indexing/test_datetime.py +++ b/pandas/tests/series/indexing/test_datetime.py @@ -427,10 +427,10 @@ def test_indexing(): # getting # GH 3070, make sure semantics work on Series/Frame - expected = ts["2001"] - expected.name = "A" + result = ts["2001"] + tm.assert_series_equal(result, ts.iloc[:12]) - df = DataFrame({"A": ts}) + df = DataFrame({"A": ts.copy()}) # GH#36179 pre-2.0 df["2001"] operated as slicing on rows. in 2.0 it behaves # like any other key, so raises @@ -438,14 +438,16 @@ def test_indexing(): df["2001"] # setting + ts = Series(np.random.default_rng(2).random(len(idx)), index=idx) + expected = ts.copy() + expected.iloc[:12] = 1 ts["2001"] = 1 - expected = ts["2001"] - expected.name = "A" + tm.assert_series_equal(ts, expected) + expected = df.copy() + expected.iloc[:12, 0] = 1 df.loc["2001", "A"] = 1 - - with pytest.raises(KeyError, match="2001"): - df["2001"] + tm.assert_frame_equal(df, expected) def test_getitem_str_month_with_datetimeindex(): diff --git a/pandas/tests/series/methods/test_copy.py b/pandas/tests/series/methods/test_copy.py index 77600e0e7d293..9531f1acd78f5 100644 --- a/pandas/tests/series/methods/test_copy.py +++ b/pandas/tests/series/methods/test_copy.py @@ -38,6 +38,7 @@ def test_copy(self, deep, using_copy_on_write): assert np.isnan(ser2[0]) assert np.isnan(ser[0]) + @pytest.mark.filterwarnings("ignore:Setting value on view:FutureWarning") @pytest.mark.parametrize("deep", ["default", None, False, True]) def test_copy_tzaware(self, deep, using_copy_on_write): # GH#11794 diff --git a/pandas/tests/series/methods/test_fillna.py b/pandas/tests/series/methods/test_fillna.py index 46bc14da59eb0..5d0ef893d5723 100644 --- a/pandas/tests/series/methods/test_fillna.py +++ b/pandas/tests/series/methods/test_fillna.py @@ -248,7 +248,7 @@ def test_timedelta_fillna(self, frame_or_series): ] ) td = ser.diff() - obj = frame_or_series(td) + obj = frame_or_series(td).copy() # reg fillna result = obj.fillna(Timedelta(seconds=0)) @@ -321,7 +321,7 @@ def test_timedelta_fillna(self, frame_or_series): # ffill td[2] = np.nan - obj = frame_or_series(td) + obj = frame_or_series(td).copy() result = obj.ffill() expected = td.fillna(Timedelta(seconds=0)) expected[0] = np.nan diff --git a/pandas/tests/series/methods/test_rename.py b/pandas/tests/series/methods/test_rename.py index 93c4fbb7f3c46..a119d82711276 100644 --- a/pandas/tests/series/methods/test_rename.py +++ b/pandas/tests/series/methods/test_rename.py @@ -162,12 +162,14 @@ def test_rename_error_arg(self): with pytest.raises(KeyError, match=match): ser.rename({2: 9}, errors="raise") - def test_rename_copy_false(self, using_copy_on_write): + def test_rename_copy_false(self, using_copy_on_write, warn_copy_on_write): # GH 46889 ser = Series(["foo", "bar"]) ser_orig = ser.copy() shallow_copy = ser.rename({1: 9}, copy=False) - ser[0] = "foobar" + warn = FutureWarning if warn_copy_on_write else None + with tm.assert_produces_warning(warn): + ser[0] = "foobar" if using_copy_on_write: assert ser_orig[0] == shallow_copy[0] assert ser_orig[1] == shallow_copy[9] diff --git a/pandas/tests/series/test_constructors.py b/pandas/tests/series/test_constructors.py index 5a05a1840b644..54c97230b761c 100644 --- a/pandas/tests/series/test_constructors.py +++ b/pandas/tests/series/test_constructors.py @@ -887,12 +887,14 @@ def test_constructor_invalid_coerce_ints_with_float_nan(self, any_int_numpy_dtyp with pytest.raises(IntCastingNaNError, match=msg): Series(np.array(vals), dtype=any_int_numpy_dtype) - def test_constructor_dtype_no_cast(self, using_copy_on_write): + def test_constructor_dtype_no_cast(self, using_copy_on_write, warn_copy_on_write): # see gh-1572 s = Series([1, 2, 3]) s2 = Series(s, dtype=np.int64) - s2[1] = 5 + warn = FutureWarning if warn_copy_on_write else None + with tm.assert_produces_warning(warn): + s2[1] = 5 if using_copy_on_write: assert s[1] == 2 else: diff --git a/pandas/util/_test_decorators.py b/pandas/util/_test_decorators.py index 9be0c3edaa998..a6b7dc74a0e4c 100644 --- a/pandas/util/_test_decorators.py +++ b/pandas/util/_test_decorators.py @@ -243,11 +243,11 @@ def mark_array_manager_not_yet_implemented(request) -> None: ) skip_copy_on_write_not_yet_implemented = pytest.mark.xfail( - get_option("mode.copy_on_write"), + get_option("mode.copy_on_write") is True, reason="Not yet implemented/adapted for Copy-on-Write mode", ) skip_copy_on_write_invalid_test = pytest.mark.skipif( - get_option("mode.copy_on_write"), + get_option("mode.copy_on_write") is True, reason="Test not valid for Copy-on-Write mode", ) From 5f00be7281a94ee82666ee5229f21175293ee30a Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Fri, 13 Oct 2023 18:19:22 +0200 Subject: [PATCH 2/9] add CI build --- .github/workflows/unit-tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 96010a4a0227d..9f84acdf175c1 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -69,6 +69,10 @@ jobs: env_file: actions-311.yaml pattern: "not slow and not network and not single_cpu" pandas_copy_on_write: "1" + - name: "Copy-on-Write (warnings)" + env_file: actions-311.yaml + pattern: "not slow and not network and not single_cpu" + pandas_copy_on_write: "warn" - name: "Pypy" env_file: actions-pypy-39.yaml pattern: "not slow and not network and not single_cpu" From 55b483bb0430d941a8b5069a62f34d9617293d0f Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Fri, 13 Oct 2023 20:58:43 +0200 Subject: [PATCH 3/9] avoid warning with inplace operators --- pandas/core/generic.py | 8 +++++++- pandas/core/internals/base.py | 2 +- pandas/core/internals/managers.py | 4 ++-- pandas/tests/copy_view/test_setitem.py | 16 ++++++++++++++++ pandas/tests/frame/test_arithmetic.py | 6 ++++-- pandas/tests/frame/test_subclass.py | 2 ++ 6 files changed, 32 insertions(+), 6 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 8eb56eea3d3df..388f1c1b47b51 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -12375,6 +12375,12 @@ def _inplace_method(self, other, op) -> Self: """ Wrap arithmetic method to operate inplace. """ + warn = True + if not PYPY and warn_copy_on_write(): + if sys.getrefcount(self) <= 5: + # we are probably in an inplace setitem context (e.g. df['a'] += 1) + warn = False + result = op(self, other) if self.ndim == 1 and result._indexed_same(self) and result.dtype == self.dtype: @@ -12382,7 +12388,7 @@ def _inplace_method(self, other, op) -> Self: # Item "ArrayManager" of "Union[ArrayManager, SingleArrayManager, # BlockManager, SingleBlockManager]" has no attribute "setitem_inplace" self._mgr.setitem_inplace( # type: ignore[union-attr] - slice(None), result._values + slice(None), result._values, warn=warn ) return self diff --git a/pandas/core/internals/base.py b/pandas/core/internals/base.py index 677dd369fa4ee..fe366c7375c6a 100644 --- a/pandas/core/internals/base.py +++ b/pandas/core/internals/base.py @@ -307,7 +307,7 @@ def array(self) -> ArrayLike: # error: "SingleDataManager" has no attribute "arrays"; maybe "array" return self.arrays[0] # type: ignore[attr-defined] - def setitem_inplace(self, indexer, value) -> None: + def setitem_inplace(self, indexer, value, warn: bool = True) -> None: """ Set values with indexer. diff --git a/pandas/core/internals/managers.py b/pandas/core/internals/managers.py index 13c93c5b9b9a9..5758ba72d4b66 100644 --- a/pandas/core/internals/managers.py +++ b/pandas/core/internals/managers.py @@ -1981,7 +1981,7 @@ def get_numeric_data(self, copy: bool = False) -> Self: def _can_hold_na(self) -> bool: return self._block._can_hold_na - def setitem_inplace(self, indexer, value) -> None: + def setitem_inplace(self, indexer, value, warn: bool = True) -> None: """ Set values with indexer. @@ -1995,7 +1995,7 @@ def setitem_inplace(self, indexer, value) -> None: if using_copy_on_write(): self.blocks = (self._block.copy(),) self._cache.clear() - elif warn_copy_on_write(): + elif warn and warn_copy_on_write(): warnings.warn( "Setting value on view: behaviour will change in pandas 3.0 " "with Copy-on-Write ...", diff --git a/pandas/tests/copy_view/test_setitem.py b/pandas/tests/copy_view/test_setitem.py index 5016b57bdd0b7..fe5416500e424 100644 --- a/pandas/tests/copy_view/test_setitem.py +++ b/pandas/tests/copy_view/test_setitem.py @@ -140,3 +140,19 @@ def test_setitem_series_column_midx_broadcasting(using_copy_on_write): assert not np.shares_memory(get_array(rhs), df._get_column_array(0)) if using_copy_on_write: assert df._mgr._has_no_reference(0) + + +def test_set_column_with_inplace_operator(using_copy_on_write, warn_copy_on_write): + df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + + # this should not raise any warning + with tm.assert_produces_warning(None): + df["a"] += 1 + + # when it is not in a chain, then it should produce a warning + warn = FutureWarning if warn_copy_on_write else None + + df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + ser = df["a"] + with tm.assert_produces_warning(warn): + ser += 1 diff --git a/pandas/tests/frame/test_arithmetic.py b/pandas/tests/frame/test_arithmetic.py index 2c3b732fe7196..1f2f34512b7f9 100644 --- a/pandas/tests/frame/test_arithmetic.py +++ b/pandas/tests/frame/test_arithmetic.py @@ -2029,14 +2029,16 @@ def test_arith_list_of_arraylike_raise(to_add): to_add + df -def test_inplace_arithmetic_series_update(using_copy_on_write): +def test_inplace_arithmetic_series_update(using_copy_on_write, warn_copy_on_write): # https://github.com/pandas-dev/pandas/issues/36373 df = DataFrame({"A": [1, 2, 3]}) df_orig = df.copy() series = df["A"] vals = series._values - series += 1 + warn = FutureWarning if warn_copy_on_write else None + with tm.assert_produces_warning(warn): + series += 1 if using_copy_on_write: assert series._values is not vals tm.assert_frame_equal(df, df_orig) diff --git a/pandas/tests/frame/test_subclass.py b/pandas/tests/frame/test_subclass.py index 37ca52eba6451..8e146b88a18e8 100644 --- a/pandas/tests/frame/test_subclass.py +++ b/pandas/tests/frame/test_subclass.py @@ -520,6 +520,8 @@ def test_subclassed_wide_to_long(self): tm.assert_frame_equal(long_frame, expected) + # TODO(CoW) should not need to warn + @pytest.mark.filterwarnings("ignore:Setting value on view:FutureWarning") def test_subclassed_apply(self): # GH 19822 From 78366011ccd7dfee254544d67d0fc40043f7c275 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Sat, 14 Oct 2023 22:17:57 +0200 Subject: [PATCH 4/9] update chained assignment tests to silence current failures --- .../multiindex/test_chaining_and_caching.py | 5 +- .../tests/indexing/multiindex/test_setitem.py | 23 +++-- .../indexing/test_chaining_and_caching.py | 89 ++++++++++++++----- 3 files changed, 91 insertions(+), 26 deletions(-) diff --git a/pandas/tests/indexing/multiindex/test_chaining_and_caching.py b/pandas/tests/indexing/multiindex/test_chaining_and_caching.py index 7adc697610f3c..c3a2c582854f3 100644 --- a/pandas/tests/indexing/multiindex/test_chaining_and_caching.py +++ b/pandas/tests/indexing/multiindex/test_chaining_and_caching.py @@ -12,7 +12,7 @@ import pandas._testing as tm -def test_detect_chained_assignment(using_copy_on_write): +def test_detect_chained_assignment(using_copy_on_write, warn_copy_on_write): # Inplace ops, originally from: # https://stackoverflow.com/questions/20508968/series-fillna-in-a-multiindex-dataframe-does-not-fill-is-this-a-bug a = [12, 23] @@ -32,6 +32,9 @@ def test_detect_chained_assignment(using_copy_on_write): if using_copy_on_write: with tm.raises_chained_assignment_error(): zed["eyes"]["right"].fillna(value=555, inplace=True) + elif warn_copy_on_write: + # TODO(CoW-warn) should warn + 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): diff --git a/pandas/tests/indexing/multiindex/test_setitem.py b/pandas/tests/indexing/multiindex/test_setitem.py index 51b94c3beeb6f..84b13c85f065e 100644 --- a/pandas/tests/indexing/multiindex/test_setitem.py +++ b/pandas/tests/indexing/multiindex/test_setitem.py @@ -273,16 +273,21 @@ def test_groupby_example(self): new_vals = np.arange(df2.shape[0]) df.loc[name, "new_col"] = new_vals - def test_series_setitem(self, multiindex_year_month_day_dataframe_random_data): + def test_series_setitem( + self, multiindex_year_month_day_dataframe_random_data, warn_copy_on_write + ): ymd = multiindex_year_month_day_dataframe_random_data s = ymd["A"] - s[2000, 3] = np.nan + warn = FutureWarning if warn_copy_on_write else None + with tm.assert_produces_warning(warn, match="Setting value on view"): + s[2000, 3] = np.nan assert isna(s.values[42:65]).all() assert notna(s.values[:42]).all() assert notna(s.values[65:]).all() - s[2000, 3, 10] = np.nan + with tm.assert_produces_warning(warn, match="Setting value on view"): + s[2000, 3, 10] = np.nan assert isna(s.iloc[49]) with pytest.raises(KeyError, match="49"): @@ -527,13 +532,17 @@ def test_frame_setitem_view_direct( def test_frame_setitem_copy_raises( - multiindex_dataframe_random_data, using_copy_on_write + multiindex_dataframe_random_data, using_copy_on_write, warn_copy_on_write ): # will raise/warn as its chained assignment df = multiindex_dataframe_random_data.T if using_copy_on_write: with tm.raises_chained_assignment_error(): df["foo"]["one"] = 2 + elif warn_copy_on_write: + # TODO(CoW-warn) should warn + with tm.assert_produces_warning(None): + df["foo"]["one"] = 2 else: msg = "A value is trying to be set on a copy of a slice from a DataFrame" with pytest.raises(SettingWithCopyError, match=msg): @@ -541,7 +550,7 @@ def test_frame_setitem_copy_raises( def test_frame_setitem_copy_no_write( - multiindex_dataframe_random_data, using_copy_on_write + multiindex_dataframe_random_data, using_copy_on_write, warn_copy_on_write ): frame = multiindex_dataframe_random_data.T expected = frame @@ -549,6 +558,10 @@ def test_frame_setitem_copy_no_write( if using_copy_on_write: with tm.raises_chained_assignment_error(): df["foo"]["one"] = 2 + elif warn_copy_on_write: + # TODO(CoW-warn) should warn + with tm.assert_produces_warning(None): + df["foo"]["one"] = 2 else: msg = "A value is trying to be set on a copy of a slice from a DataFrame" with pytest.raises(SettingWithCopyError, match=msg): diff --git a/pandas/tests/indexing/test_chaining_and_caching.py b/pandas/tests/indexing/test_chaining_and_caching.py index 7353b5ef76ba3..46b3091f26f78 100644 --- a/pandas/tests/indexing/test_chaining_and_caching.py +++ b/pandas/tests/indexing/test_chaining_and_caching.py @@ -201,7 +201,7 @@ def test_setitem_chained_setfault(self, using_copy_on_write): tm.assert_frame_equal(result, expected) @pytest.mark.arm_slow - def test_detect_chained_assignment(self, using_copy_on_write): + def test_detect_chained_assignment(self, using_copy_on_write, warn_copy_on_write): with option_context("chained_assignment", "raise"): # work with the chain expected = DataFrame([[-5, 1], [-6, 3]], columns=list("AB")) @@ -218,13 +218,16 @@ def test_detect_chained_assignment(self, using_copy_on_write): df["A"][1] = -6 tm.assert_frame_equal(df, df_original) else: - df["A"][0] = -5 - df["A"][1] = -6 + warn = FutureWarning if warn_copy_on_write else None + with tm.assert_produces_warning(warn): + df["A"][0] = -5 + with tm.assert_produces_warning(warn): + df["A"][1] = -6 tm.assert_frame_equal(df, expected) @pytest.mark.arm_slow def test_detect_chained_assignment_raises( - self, using_array_manager, using_copy_on_write + self, using_array_manager, using_copy_on_write, warn_copy_on_write ): # test with the chaining df = DataFrame( @@ -242,6 +245,11 @@ def test_detect_chained_assignment_raises( with tm.raises_chained_assignment_error(): df["A"][1] = -6 tm.assert_frame_equal(df, df_original) + elif warn_copy_on_write: + with tm.assert_produces_warning(FutureWarning): + df["A"][0] = -5 + with tm.assert_produces_warning(FutureWarning): + df["A"][1] = np.nan elif not using_array_manager: with pytest.raises(SettingWithCopyError, match=msg): df["A"][0] = -5 @@ -260,7 +268,9 @@ def test_detect_chained_assignment_raises( tm.assert_frame_equal(df, expected) @pytest.mark.arm_slow - def test_detect_chained_assignment_fails(self, using_copy_on_write): + def test_detect_chained_assignment_fails( + self, using_copy_on_write, warn_copy_on_write + ): # Using a copy (the chain), fails df = DataFrame( { @@ -272,12 +282,18 @@ def test_detect_chained_assignment_fails(self, using_copy_on_write): if using_copy_on_write: with tm.raises_chained_assignment_error(): df.loc[0]["A"] = -5 + elif warn_copy_on_write: + # TODO(CoW) should warn + with tm.assert_produces_warning(None): + df.loc[0]["A"] = -5 else: with pytest.raises(SettingWithCopyError, match=msg): df.loc[0]["A"] = -5 @pytest.mark.arm_slow - def test_detect_chained_assignment_doc_example(self, using_copy_on_write): + def test_detect_chained_assignment_doc_example( + self, using_copy_on_write, warn_copy_on_write + ): # Doc example df = DataFrame( { @@ -287,24 +303,27 @@ def test_detect_chained_assignment_doc_example(self, using_copy_on_write): ) assert df._is_copy is None + indexer = df.a.str.startswith("o") if using_copy_on_write: - indexer = df.a.str.startswith("o") with tm.raises_chained_assignment_error(): df[indexer]["c"] = 42 + elif warn_copy_on_write: + # TODO(CoW) should warn + with tm.assert_produces_warning(None): + df[indexer]["c"] = 42 else: with pytest.raises(SettingWithCopyError, match=msg): - indexer = df.a.str.startswith("o") df[indexer]["c"] = 42 @pytest.mark.arm_slow def test_detect_chained_assignment_object_dtype( - self, using_array_manager, using_copy_on_write + self, using_array_manager, using_copy_on_write, warn_copy_on_write ): expected = DataFrame({"A": [111, "bbb", "ccc"], "B": [1, 2, 3]}) df = DataFrame({"A": ["aaa", "bbb", "ccc"], "B": [1, 2, 3]}) df_original = df.copy() - if not using_copy_on_write: + if not using_copy_on_write and not warn_copy_on_write: with pytest.raises(SettingWithCopyError, match=msg): df.loc[0]["A"] = 111 @@ -312,6 +331,11 @@ def test_detect_chained_assignment_object_dtype( with tm.raises_chained_assignment_error(): df["A"][0] = 111 tm.assert_frame_equal(df, df_original) + elif warn_copy_on_write: + # TODO(CoW-warn) should give different message + with tm.assert_produces_warning(FutureWarning): + df["A"][0] = 111 + tm.assert_frame_equal(df, expected) elif not using_array_manager: with pytest.raises(SettingWithCopyError, match=msg): df["A"][0] = 111 @@ -367,8 +391,10 @@ def test_detect_chained_assignment_implicit_take(self): df["letters"] = df["letters"].apply(str.lower) @pytest.mark.arm_slow - def test_detect_chained_assignment_implicit_take2(self, using_copy_on_write): - if using_copy_on_write: + def test_detect_chained_assignment_implicit_take2( + self, using_copy_on_write, warn_copy_on_write + ): + if using_copy_on_write or warn_copy_on_write: pytest.skip("_is_copy is not always set for CoW") # Implicitly take 2 df = random_text(100000) @@ -422,7 +448,9 @@ def test_detect_chained_assignment_false_positives(self): str(df) @pytest.mark.arm_slow - def test_detect_chained_assignment_undefined_column(self, using_copy_on_write): + def test_detect_chained_assignment_undefined_column( + self, using_copy_on_write, warn_copy_on_write + ): # from SO: # https://stackoverflow.com/questions/24054495/potential-bug-setting-value-for-undefined-column-using-iloc df = DataFrame(np.arange(0, 9), columns=["count"]) @@ -433,13 +461,17 @@ def test_detect_chained_assignment_undefined_column(self, using_copy_on_write): with tm.raises_chained_assignment_error(): df.iloc[0:5]["group"] = "a" tm.assert_frame_equal(df, df_original) + elif warn_copy_on_write: + # TODO(CoW-warn) should warn + with tm.assert_produces_warning(None): + df.iloc[0:5]["group"] = "a" else: with pytest.raises(SettingWithCopyError, match=msg): df.iloc[0:5]["group"] = "a" @pytest.mark.arm_slow def test_detect_chained_assignment_changing_dtype( - self, using_array_manager, using_copy_on_write + self, using_array_manager, using_copy_on_write, warn_copy_on_write ): # Mixed type setting but same dtype & changing dtype df = DataFrame( @@ -460,8 +492,14 @@ def test_detect_chained_assignment_changing_dtype( with tm.raises_chained_assignment_error(extra_warnings=(FutureWarning,)): df["C"][2] = "foo" tm.assert_frame_equal(df, df_original) - - if not using_copy_on_write: + elif warn_copy_on_write: + # TODO(CoW-warn) should warn + with tm.assert_produces_warning(None): + df.loc[2]["D"] = "foo" + # TODO(CoW-warn) should give different message + with tm.assert_produces_warning(FutureWarning): + df["C"][2] = "foo" + else: with pytest.raises(SettingWithCopyError, match=msg): df.loc[2]["D"] = "foo" @@ -477,7 +515,7 @@ def test_detect_chained_assignment_changing_dtype( df["C"][2] = "foo" assert df.loc[2, "C"] == "foo" - def test_setting_with_copy_bug(self, using_copy_on_write): + def test_setting_with_copy_bug(self, using_copy_on_write, warn_copy_on_write): # operating on a copy df = DataFrame( {"a": list(range(4)), "b": list("ab.."), "c": ["a", "b", np.nan, "d"]} @@ -489,6 +527,10 @@ def test_setting_with_copy_bug(self, using_copy_on_write): with tm.raises_chained_assignment_error(): df[["c"]][mask] = df[["b"]][mask] tm.assert_frame_equal(df, df_original) + elif warn_copy_on_write: + # TODO(CoW-warn) should warn + with tm.assert_produces_warning(None): + df[["c"]][mask] = df[["b"]][mask] else: with pytest.raises(SettingWithCopyError, match=msg): df[["c"]][mask] = df[["b"]][mask] @@ -502,12 +544,19 @@ def test_setting_with_copy_bug_no_warning(self): # this should not raise df2["y"] = ["g", "h", "i"] - def test_detect_chained_assignment_warnings_errors(self, using_copy_on_write): + def test_detect_chained_assignment_warnings_errors( + self, using_copy_on_write, warn_copy_on_write + ): df = DataFrame({"A": ["aaa", "bbb", "ccc"], "B": [1, 2, 3]}) if using_copy_on_write: with tm.raises_chained_assignment_error(): df.loc[0]["A"] = 111 return + elif warn_copy_on_write: + # TODO(CoW-warn) should warn + with tm.assert_produces_warning(None): + df.loc[0]["A"] = 111 + return with option_context("chained_assignment", "warn"): with tm.assert_produces_warning(SettingWithCopyWarning): @@ -519,14 +568,14 @@ def test_detect_chained_assignment_warnings_errors(self, using_copy_on_write): @pytest.mark.parametrize("rhs", [3, DataFrame({0: [1, 2, 3, 4]})]) def test_detect_chained_assignment_warning_stacklevel( - self, rhs, using_copy_on_write + self, rhs, using_copy_on_write, warn_copy_on_write ): # GH#42570 df = DataFrame(np.arange(25).reshape(5, 5)) df_original = df.copy() chained = df.loc[:3] with option_context("chained_assignment", "warn"): - if not using_copy_on_write: + if not using_copy_on_write and not warn_copy_on_write: with tm.assert_produces_warning(SettingWithCopyWarning) as t: chained[2] = rhs assert t[0].filename == __file__ From e16abb46c23c189295699e04d0a3f0618ad96221 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Sat, 14 Oct 2023 22:24:07 +0200 Subject: [PATCH 5/9] address feedback --- pandas/core/internals/managers.py | 8 +++++--- pandas/tests/copy_view/test_indexing.py | 12 ++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/pandas/core/internals/managers.py b/pandas/core/internals/managers.py index 5758ba72d4b66..15551c7f88f8c 100644 --- a/pandas/core/internals/managers.py +++ b/pandas/core/internals/managers.py @@ -1991,11 +1991,13 @@ def setitem_inplace(self, indexer, value, warn: bool = True) -> None: in place, not returning a new Manager (and Block), and thus never changing the dtype. """ - if not self._has_no_reference(0): - if using_copy_on_write(): + using_cow = using_copy_on_write() + warn_cow = warn_copy_on_write() + if using_cow or warn_cow and not self._has_no_reference(0): + if using_cow: self.blocks = (self._block.copy(),) self._cache.clear() - elif warn and warn_copy_on_write(): + elif warn and warn_cow: warnings.warn( "Setting value on view: behaviour will change in pandas 3.0 " "with Copy-on-Write ...", diff --git a/pandas/tests/copy_view/test_indexing.py b/pandas/tests/copy_view/test_indexing.py index 711d1bd26363f..92d2b7271aa88 100644 --- a/pandas/tests/copy_view/test_indexing.py +++ b/pandas/tests/copy_view/test_indexing.py @@ -163,7 +163,7 @@ def test_subset_column_slice( assert not np.shares_memory(get_array(subset, "b"), get_array(df, "b")) else: # we only get a warning in case of a single block - # TODO + # TODO(CoW-warn) should warn warn = ( SettingWithCopyWarning if (single_block and not warn_copy_on_write) @@ -326,7 +326,7 @@ def test_subset_set_with_row_indexer( ): pytest.skip("setitem with labels selects on columns") - # TODO + # TODO(CoW-warn) should warn if using_copy_on_write or warn_copy_on_write: indexer_si(subset)[indexer] = 0 else: @@ -358,7 +358,7 @@ def test_subset_set_with_mask(backend, using_copy_on_write, warn_copy_on_write): mask = subset > 3 - # TODO + # TODO(CoW-warn) should warn if using_copy_on_write or warn_copy_on_write: subset[mask] = 0 else: @@ -392,7 +392,7 @@ def test_subset_set_column(backend, using_copy_on_write, warn_copy_on_write): else: arr = pd.array([10, 11], dtype="Int64") - # TODO + # TODO(CoW-warn) should warn if using_copy_on_write or warn_copy_on_write: subset["a"] = arr else: @@ -493,7 +493,7 @@ def test_subset_set_columns(backend, using_copy_on_write, warn_copy_on_write, dt df_orig = df.copy() subset = df[1:3] - # TODO + # TODO(CoW-warn) should warn if using_copy_on_write or warn_copy_on_write: subset[["a", "c"]] = 0 else: @@ -1004,7 +1004,7 @@ def test_column_as_series_no_item_cache( else: assert s1 is s2 - # TODO + # TODO(CoW-warn) should warn if using_copy_on_write or warn_copy_on_write or using_array_manager: s1.iloc[0] = 0 else: From 4bf974cf4640c42a21876b60f54d6148666c156a Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Sat, 14 Oct 2023 22:42:39 +0200 Subject: [PATCH 6/9] update warning message --- pandas/core/internals/managers.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pandas/core/internals/managers.py b/pandas/core/internals/managers.py index 15551c7f88f8c..8abf952403536 100644 --- a/pandas/core/internals/managers.py +++ b/pandas/core/internals/managers.py @@ -99,6 +99,20 @@ from pandas.api.extensions import ExtensionArray +COW_WARNING_SETITEM_MSG = """\ +Setting a value on a view: behaviour will change in pandas 3.0. +Currently, the mutation will also have effect on the object that shares data +with this object. For example, when setting a value in a Series that was +extracted from a column of a DataFrame, that DataFrame will also be updated: + + ser = df["col"] + ser[0] = 0 <--- in pandas 2, this also updates `df` + +In pandas 3.0 (with Copy-on-Write), updating one Series/DataFrame will never +modify another, and thus in the example above, `df` will not be changed. +""" + + class BaseBlockManager(DataManager): """ Core internal data structure to implement DataFrame, Series, etc. @@ -1999,8 +2013,7 @@ def setitem_inplace(self, indexer, value, warn: bool = True) -> None: self._cache.clear() elif warn and warn_cow: warnings.warn( - "Setting value on view: behaviour will change in pandas 3.0 " - "with Copy-on-Write ...", + COW_WARNING_SETITEM_MSG, FutureWarning, stacklevel=find_stack_level(), ) From 92c0898c2b7d777ee43e4a4dad634d0aca9d5b4d Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Tue, 17 Oct 2023 11:09:28 +0200 Subject: [PATCH 7/9] fixup tests for changed warning text --- pandas/tests/apply/test_invalid_arg.py | 2 ++ pandas/tests/frame/test_block_internals.py | 2 +- pandas/tests/frame/test_constructors.py | 7 ++++--- pandas/tests/frame/test_subclass.py | 4 ++-- pandas/tests/indexing/multiindex/test_setitem.py | 4 ++-- pandas/tests/series/methods/test_copy.py | 2 +- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pandas/tests/apply/test_invalid_arg.py b/pandas/tests/apply/test_invalid_arg.py index a3d9de5e78afb..44829c598253d 100644 --- a/pandas/tests/apply/test_invalid_arg.py +++ b/pandas/tests/apply/test_invalid_arg.py @@ -147,6 +147,8 @@ def test_transform_axis_1_raises(): Series([1]).transform("sum", axis=1) +# TODO(CoW-warn) should not need to warn +@pytest.mark.filterwarnings("ignore:Setting a value on a view:FutureWarning") def test_apply_modify_traceback(): data = DataFrame( { diff --git a/pandas/tests/frame/test_block_internals.py b/pandas/tests/frame/test_block_internals.py index c0fb3ccbd7c79..d3741fb85af98 100644 --- a/pandas/tests/frame/test_block_internals.py +++ b/pandas/tests/frame/test_block_internals.py @@ -357,7 +357,7 @@ def test_stale_cached_series_bug_473(self, using_copy_on_write, warn_copy_on_wri else: assert pd.isna(Y["g"]["c"]) - @pytest.mark.filterwarnings("ignore:Setting value on view:FutureWarning") + @pytest.mark.filterwarnings("ignore:Setting a value on a view:FutureWarning") def test_strange_column_corruption_issue(self, using_copy_on_write): # TODO(wesm): Unclear how exactly this is related to internal matters df = DataFrame(index=[0, 1]) diff --git a/pandas/tests/frame/test_constructors.py b/pandas/tests/frame/test_constructors.py index 38477867e37eb..72c376528f068 100644 --- a/pandas/tests/frame/test_constructors.py +++ b/pandas/tests/frame/test_constructors.py @@ -307,14 +307,15 @@ def test_constructor_dtype_nocast_view_dataframe( should_be_view[0][0] = 99 assert df.values[0, 0] == 99 - @pytest.mark.filterwarnings("ignore:Setting value on view:FutureWarning") def test_constructor_dtype_nocast_view_2d_array( - self, using_array_manager, using_copy_on_write + self, using_array_manager, using_copy_on_write, warn_copy_on_write ): df = DataFrame([[1, 2], [3, 4]], dtype="int64") if not using_array_manager and not using_copy_on_write: should_be_view = DataFrame(df.values, dtype=df[0].dtype) - should_be_view[0][0] = 97 + warn = FutureWarning if warn_copy_on_write else None + with tm.assert_produces_warning(warn): + should_be_view[0][0] = 97 assert df.values[0, 0] == 97 else: # INFO(ArrayManager) DataFrame(ndarray) doesn't necessarily preserve diff --git a/pandas/tests/frame/test_subclass.py b/pandas/tests/frame/test_subclass.py index 8e146b88a18e8..02fa4222ce252 100644 --- a/pandas/tests/frame/test_subclass.py +++ b/pandas/tests/frame/test_subclass.py @@ -520,8 +520,8 @@ def test_subclassed_wide_to_long(self): tm.assert_frame_equal(long_frame, expected) - # TODO(CoW) should not need to warn - @pytest.mark.filterwarnings("ignore:Setting value on view:FutureWarning") + # TODO(CoW-warn) should not need to warn + @pytest.mark.filterwarnings("ignore:Setting a value on a view:FutureWarning") def test_subclassed_apply(self): # GH 19822 diff --git a/pandas/tests/indexing/multiindex/test_setitem.py b/pandas/tests/indexing/multiindex/test_setitem.py index 84b13c85f065e..562552dab1b52 100644 --- a/pandas/tests/indexing/multiindex/test_setitem.py +++ b/pandas/tests/indexing/multiindex/test_setitem.py @@ -280,13 +280,13 @@ def test_series_setitem( s = ymd["A"] warn = FutureWarning if warn_copy_on_write else None - with tm.assert_produces_warning(warn, match="Setting value on view"): + with tm.assert_produces_warning(warn, match="Setting a value on a view"): s[2000, 3] = np.nan assert isna(s.values[42:65]).all() assert notna(s.values[:42]).all() assert notna(s.values[65:]).all() - with tm.assert_produces_warning(warn, match="Setting value on view"): + with tm.assert_produces_warning(warn, match="Setting a value on a view"): s[2000, 3, 10] = np.nan assert isna(s.iloc[49]) diff --git a/pandas/tests/series/methods/test_copy.py b/pandas/tests/series/methods/test_copy.py index 9531f1acd78f5..ea439fb5a3263 100644 --- a/pandas/tests/series/methods/test_copy.py +++ b/pandas/tests/series/methods/test_copy.py @@ -38,7 +38,7 @@ def test_copy(self, deep, using_copy_on_write): assert np.isnan(ser2[0]) assert np.isnan(ser[0]) - @pytest.mark.filterwarnings("ignore:Setting value on view:FutureWarning") + @pytest.mark.filterwarnings("ignore:Setting a value on a view:FutureWarning") @pytest.mark.parametrize("deep", ["default", None, False, True]) def test_copy_tzaware(self, deep, using_copy_on_write): # GH#11794 From 07dfbec63ee3f3e998fc28c1c1040d9a46efde66 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Tue, 17 Oct 2023 11:23:18 +0200 Subject: [PATCH 8/9] fix COW setitem --- pandas/core/internals/managers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/internals/managers.py b/pandas/core/internals/managers.py index a98b399464dc1..d0d78732d76b6 100644 --- a/pandas/core/internals/managers.py +++ b/pandas/core/internals/managers.py @@ -2016,7 +2016,7 @@ def setitem_inplace(self, indexer, value, warn: bool = True) -> None: """ using_cow = using_copy_on_write() warn_cow = warn_copy_on_write() - if using_cow or warn_cow and not self._has_no_reference(0): + if (using_cow or warn_cow) and not self._has_no_reference(0): if using_cow: self.blocks = (self._block.copy(),) self._cache.clear() From f7fa78e45f7a793c3d5fee5d1ff466ba3526ebc8 Mon Sep 17 00:00:00 2001 From: Joris Van den Bossche Date: Fri, 27 Oct 2023 17:46:51 +0200 Subject: [PATCH 9/9] add tm.assert_cow_warning and use everywhere --- pandas/_testing/__init__.py | 2 ++ pandas/_testing/contexts.py | 26 +++++++++++++++++ pandas/tests/copy_view/test_indexing.py | 5 ++-- pandas/tests/copy_view/test_methods.py | 3 +- pandas/tests/copy_view/test_setitem.py | 4 +-- pandas/tests/frame/indexing/test_setitem.py | 3 +- pandas/tests/frame/test_arithmetic.py | 3 +- pandas/tests/frame/test_block_internals.py | 2 +- pandas/tests/frame/test_constructors.py | 6 ++-- .../tests/indexing/multiindex/test_setitem.py | 9 +++--- .../indexing/test_chaining_and_caching.py | 29 +++++++++---------- .../series/accessors/test_dt_accessor.py | 6 ++-- pandas/tests/series/methods/test_rename.py | 3 +- 13 files changed, 59 insertions(+), 42 deletions(-) diff --git a/pandas/_testing/__init__.py b/pandas/_testing/__init__.py index 160e2964896ba..81cd504119c38 100644 --- a/pandas/_testing/__init__.py +++ b/pandas/_testing/__init__.py @@ -88,6 +88,7 @@ get_obj, ) from pandas._testing.contexts import ( + assert_cow_warning, decompress_file, ensure_clean, raises_chained_assignment_error, @@ -1097,6 +1098,7 @@ def shares_memory(left, right) -> bool: "assert_series_equal", "assert_sp_array_equal", "assert_timedelta_array_equal", + "assert_cow_warning", "at", "BOOL_DTYPES", "box_expected", diff --git a/pandas/_testing/contexts.py b/pandas/_testing/contexts.py index b2bb8e71fdf5c..6edbd047699ed 100644 --- a/pandas/_testing/contexts.py +++ b/pandas/_testing/contexts.py @@ -214,3 +214,29 @@ def raises_chained_assignment_error(extra_warnings=(), extra_match=()): (ChainedAssignmentError, *extra_warnings), match="|".join((match, *extra_match)), ) + + +def assert_cow_warning(warn=True, match=None, **kwargs): + """ + Assert that a warning is raised in the CoW warning mode. + + Parameters + ---------- + warn : bool, default True + By default, check that a warning is raised. Can be turned off by passing False. + match : str + The warning message to match against, if different from the default. + kwargs + Passed through to assert_produces_warning + """ + from pandas._testing import assert_produces_warning + + if not warn: + from contextlib import nullcontext + + return nullcontext() + + if not match: + match = "Setting a value on a view" + + return assert_produces_warning(FutureWarning, match=match, **kwargs) diff --git a/pandas/tests/copy_view/test_indexing.py b/pandas/tests/copy_view/test_indexing.py index 92d2b7271aa88..ad55f9d561fe0 100644 --- a/pandas/tests/copy_view/test_indexing.py +++ b/pandas/tests/copy_view/test_indexing.py @@ -907,7 +907,7 @@ def test_column_as_series( s[0] = 0 else: if warn_copy_on_write: - with tm.assert_produces_warning(FutureWarning): + with tm.assert_cow_warning(): s[0] = 0 else: warn = SettingWithCopyWarning if dtype_backend == "numpy" else None @@ -939,8 +939,7 @@ def test_column_as_series_set_with_upcast( s = df["a"] if dtype_backend == "nullable": - warn = FutureWarning if warn_copy_on_write else None - with tm.assert_produces_warning(warn): + with tm.assert_cow_warning(warn_copy_on_write): with pytest.raises(TypeError, match="Invalid value"): s[0] = "foo" expected = Series([1, 2, 3], name="a") diff --git a/pandas/tests/copy_view/test_methods.py b/pandas/tests/copy_view/test_methods.py index ccc09c78a16a1..60ab21f48e910 100644 --- a/pandas/tests/copy_view/test_methods.py +++ b/pandas/tests/copy_view/test_methods.py @@ -1706,8 +1706,7 @@ def test_xs( if using_copy_on_write or is_view: result.iloc[0] = 0 elif warn_copy_on_write: - warn = FutureWarning if single_block else None - with tm.assert_produces_warning(warn): + with tm.assert_cow_warning(single_block): result.iloc[0] = 0 else: with pd.option_context("chained_assignment", "warn"): diff --git a/pandas/tests/copy_view/test_setitem.py b/pandas/tests/copy_view/test_setitem.py index fe5416500e424..bc3b939734534 100644 --- a/pandas/tests/copy_view/test_setitem.py +++ b/pandas/tests/copy_view/test_setitem.py @@ -150,9 +150,7 @@ def test_set_column_with_inplace_operator(using_copy_on_write, warn_copy_on_writ df["a"] += 1 # when it is not in a chain, then it should produce a warning - warn = FutureWarning if warn_copy_on_write else None - df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) ser = df["a"] - with tm.assert_produces_warning(warn): + with tm.assert_cow_warning(warn_copy_on_write): ser += 1 diff --git a/pandas/tests/frame/indexing/test_setitem.py b/pandas/tests/frame/indexing/test_setitem.py index 37a26c4d880f6..1317e3767efba 100644 --- a/pandas/tests/frame/indexing/test_setitem.py +++ b/pandas/tests/frame/indexing/test_setitem.py @@ -1292,8 +1292,7 @@ def test_setitem_column_update_inplace( values = df._mgr.blocks[0].values if not using_copy_on_write: - warn = FutureWarning if warn_copy_on_write else None - with tm.assert_produces_warning(warn): + with tm.assert_cow_warning(warn_copy_on_write): for label in df.columns: df[label][label] = 1 diff --git a/pandas/tests/frame/test_arithmetic.py b/pandas/tests/frame/test_arithmetic.py index 6f86e35cc70a5..fdace8662e2a6 100644 --- a/pandas/tests/frame/test_arithmetic.py +++ b/pandas/tests/frame/test_arithmetic.py @@ -2036,8 +2036,7 @@ def test_inplace_arithmetic_series_update(using_copy_on_write, warn_copy_on_writ series = df["A"] vals = series._values - warn = FutureWarning if warn_copy_on_write else None - with tm.assert_produces_warning(warn): + with tm.assert_cow_warning(warn_copy_on_write): series += 1 if using_copy_on_write: assert series._values is not vals diff --git a/pandas/tests/frame/test_block_internals.py b/pandas/tests/frame/test_block_internals.py index 8850fe050c86c..295f457bcaeab 100644 --- a/pandas/tests/frame/test_block_internals.py +++ b/pandas/tests/frame/test_block_internals.py @@ -348,7 +348,7 @@ def test_stale_cached_series_bug_473(self, using_copy_on_write, warn_copy_on_wri with tm.raises_chained_assignment_error(): Y["g"]["c"] = np.nan elif warn_copy_on_write: - with tm.assert_produces_warning(FutureWarning): + with tm.assert_cow_warning(): Y["g"]["c"] = np.nan else: Y["g"]["c"] = np.nan diff --git a/pandas/tests/frame/test_constructors.py b/pandas/tests/frame/test_constructors.py index 7a7eeea5ffc1c..5530fa336971c 100644 --- a/pandas/tests/frame/test_constructors.py +++ b/pandas/tests/frame/test_constructors.py @@ -302,8 +302,7 @@ def test_constructor_dtype_nocast_view_dataframe( should_be_view.iloc[0, 0] = 99 assert df.values[0, 0] == 1 else: - warn = FutureWarning if warn_copy_on_write else None - with tm.assert_produces_warning(warn): + with tm.assert_cow_warning(warn_copy_on_write): should_be_view[0][0] = 99 assert df.values[0, 0] == 99 @@ -313,8 +312,7 @@ def test_constructor_dtype_nocast_view_2d_array( df = DataFrame([[1, 2], [3, 4]], dtype="int64") if not using_array_manager and not using_copy_on_write: should_be_view = DataFrame(df.values, dtype=df[0].dtype) - warn = FutureWarning if warn_copy_on_write else None - with tm.assert_produces_warning(warn): + with tm.assert_cow_warning(warn_copy_on_write): should_be_view[0][0] = 97 assert df.values[0, 0] == 97 else: diff --git a/pandas/tests/indexing/multiindex/test_setitem.py b/pandas/tests/indexing/multiindex/test_setitem.py index 562552dab1b52..642286433754a 100644 --- a/pandas/tests/indexing/multiindex/test_setitem.py +++ b/pandas/tests/indexing/multiindex/test_setitem.py @@ -279,14 +279,13 @@ def test_series_setitem( ymd = multiindex_year_month_day_dataframe_random_data s = ymd["A"] - warn = FutureWarning if warn_copy_on_write else None - with tm.assert_produces_warning(warn, match="Setting a value on a view"): + with tm.assert_cow_warning(warn_copy_on_write): s[2000, 3] = np.nan assert isna(s.values[42:65]).all() assert notna(s.values[:42]).all() assert notna(s.values[65:]).all() - with tm.assert_produces_warning(warn, match="Setting a value on a view"): + with tm.assert_cow_warning(warn_copy_on_write): s[2000, 3, 10] = np.nan assert isna(s.iloc[49]) @@ -541,7 +540,7 @@ def test_frame_setitem_copy_raises( df["foo"]["one"] = 2 elif warn_copy_on_write: # TODO(CoW-warn) should warn - with tm.assert_produces_warning(None): + with tm.assert_cow_warning(False): df["foo"]["one"] = 2 else: msg = "A value is trying to be set on a copy of a slice from a DataFrame" @@ -560,7 +559,7 @@ def test_frame_setitem_copy_no_write( df["foo"]["one"] = 2 elif warn_copy_on_write: # TODO(CoW-warn) should warn - with tm.assert_produces_warning(None): + with tm.assert_cow_warning(False): df["foo"]["one"] = 2 else: msg = "A value is trying to be set on a copy of a slice from a DataFrame" diff --git a/pandas/tests/indexing/test_chaining_and_caching.py b/pandas/tests/indexing/test_chaining_and_caching.py index 46b3091f26f78..562638f7058c6 100644 --- a/pandas/tests/indexing/test_chaining_and_caching.py +++ b/pandas/tests/indexing/test_chaining_and_caching.py @@ -218,10 +218,9 @@ def test_detect_chained_assignment(self, using_copy_on_write, warn_copy_on_write df["A"][1] = -6 tm.assert_frame_equal(df, df_original) else: - warn = FutureWarning if warn_copy_on_write else None - with tm.assert_produces_warning(warn): + with tm.assert_cow_warning(warn_copy_on_write): df["A"][0] = -5 - with tm.assert_produces_warning(warn): + with tm.assert_cow_warning(warn_copy_on_write): df["A"][1] = -6 tm.assert_frame_equal(df, expected) @@ -246,9 +245,9 @@ def test_detect_chained_assignment_raises( df["A"][1] = -6 tm.assert_frame_equal(df, df_original) elif warn_copy_on_write: - with tm.assert_produces_warning(FutureWarning): + with tm.assert_cow_warning(): df["A"][0] = -5 - with tm.assert_produces_warning(FutureWarning): + with tm.assert_cow_warning(): df["A"][1] = np.nan elif not using_array_manager: with pytest.raises(SettingWithCopyError, match=msg): @@ -283,8 +282,8 @@ def test_detect_chained_assignment_fails( with tm.raises_chained_assignment_error(): df.loc[0]["A"] = -5 elif warn_copy_on_write: - # TODO(CoW) should warn - with tm.assert_produces_warning(None): + # TODO(CoW-warn) should warn + with tm.assert_cow_warning(False): df.loc[0]["A"] = -5 else: with pytest.raises(SettingWithCopyError, match=msg): @@ -308,8 +307,8 @@ def test_detect_chained_assignment_doc_example( with tm.raises_chained_assignment_error(): df[indexer]["c"] = 42 elif warn_copy_on_write: - # TODO(CoW) should warn - with tm.assert_produces_warning(None): + # TODO(CoW-warn) should warn + with tm.assert_cow_warning(False): df[indexer]["c"] = 42 else: with pytest.raises(SettingWithCopyError, match=msg): @@ -333,7 +332,7 @@ def test_detect_chained_assignment_object_dtype( tm.assert_frame_equal(df, df_original) elif warn_copy_on_write: # TODO(CoW-warn) should give different message - with tm.assert_produces_warning(FutureWarning): + with tm.assert_cow_warning(): df["A"][0] = 111 tm.assert_frame_equal(df, expected) elif not using_array_manager: @@ -463,7 +462,7 @@ def test_detect_chained_assignment_undefined_column( tm.assert_frame_equal(df, df_original) elif warn_copy_on_write: # TODO(CoW-warn) should warn - with tm.assert_produces_warning(None): + with tm.assert_cow_warning(False): df.iloc[0:5]["group"] = "a" else: with pytest.raises(SettingWithCopyError, match=msg): @@ -494,10 +493,10 @@ def test_detect_chained_assignment_changing_dtype( tm.assert_frame_equal(df, df_original) elif warn_copy_on_write: # TODO(CoW-warn) should warn - with tm.assert_produces_warning(None): + with tm.assert_cow_warning(False): df.loc[2]["D"] = "foo" # TODO(CoW-warn) should give different message - with tm.assert_produces_warning(FutureWarning): + with tm.assert_cow_warning(): df["C"][2] = "foo" else: with pytest.raises(SettingWithCopyError, match=msg): @@ -529,7 +528,7 @@ def test_setting_with_copy_bug(self, using_copy_on_write, warn_copy_on_write): tm.assert_frame_equal(df, df_original) elif warn_copy_on_write: # TODO(CoW-warn) should warn - with tm.assert_produces_warning(None): + with tm.assert_cow_warning(False): df[["c"]][mask] = df[["b"]][mask] else: with pytest.raises(SettingWithCopyError, match=msg): @@ -554,7 +553,7 @@ def test_detect_chained_assignment_warnings_errors( return elif warn_copy_on_write: # TODO(CoW-warn) should warn - with tm.assert_produces_warning(None): + with tm.assert_cow_warning(False): df.loc[0]["A"] = 111 return diff --git a/pandas/tests/series/accessors/test_dt_accessor.py b/pandas/tests/series/accessors/test_dt_accessor.py index 5e57ef5bb1aba..53ac7fbf40af1 100644 --- a/pandas/tests/series/accessors/test_dt_accessor.py +++ b/pandas/tests/series/accessors/test_dt_accessor.py @@ -294,9 +294,9 @@ def test_dt_accessor_not_writeable(self, using_copy_on_write, warn_copy_on_write with tm.raises_chained_assignment_error(): ser.dt.hour[0] = 5 elif warn_copy_on_write: - # TODO should warn - # with tm.assert_produces_warning(FutureWarning): - ser.dt.hour[0] = 5 + # TODO(CoW-warn) should warn + with tm.assert_cow_warning(False): + ser.dt.hour[0] = 5 else: with pytest.raises(SettingWithCopyError, match=msg): ser.dt.hour[0] = 5 diff --git a/pandas/tests/series/methods/test_rename.py b/pandas/tests/series/methods/test_rename.py index a119d82711276..83adff08b758e 100644 --- a/pandas/tests/series/methods/test_rename.py +++ b/pandas/tests/series/methods/test_rename.py @@ -167,8 +167,7 @@ def test_rename_copy_false(self, using_copy_on_write, warn_copy_on_write): ser = Series(["foo", "bar"]) ser_orig = ser.copy() shallow_copy = ser.rename({1: 9}, copy=False) - warn = FutureWarning if warn_copy_on_write else None - with tm.assert_produces_warning(warn): + with tm.assert_cow_warning(warn_copy_on_write): ser[0] = "foobar" if using_copy_on_write: assert ser_orig[0] == shallow_copy[0]