Repository: pandas-dev/pandas
Pull request #56633 (2023-12-27 15:50:11)

CoW: Enable CoW by default and remove warning build
I'd prefer to go this way in enabling CoW. Having it on makes it easier to enforce the change step by step rather than doing it all at once.

github-actions[bot]: This pull request is stale because it has been open for thirty days with no activity. Please [update](https://pandas.pydata.org/pandas-docs/stable/development/contributing.html#updating-your-pull-request) and respond to this comment if you're still interested in working on this.
phofl: cc @mroeschke 

this should be green now and ready to merge hopefully. That allows us to move forward with the deprecations here
mroeschke: Just a merge conflict post ArrayManager merge. Feel free to merge after that

Patch:
diff --git a/asv_bench/benchmarks/algos/isin.py b/asv_bench/benchmarks/algos/isin.py
index f9ea3d5684..a17732c70c 100644
--- a/asv_bench/benchmarks/algos/isin.py
+++ b/asv_bench/benchmarks/algos/isin.py
@@ -59,7 +59,8 @@ class IsIn:
         elif dtype in ["str", "string[python]", "string[pyarrow]"]:
             try:
                 self.series = Series(
-                    Index([f"i-{i}" for i in range(N)], dtype=object), dtype=dtype
+                    Index([f"i-{i}" for i in range(N)], dtype=object)._values,
+                    dtype=dtype,
                 )
             except ImportError as err:
                 raise NotImplementedError from err
diff --git a/asv_bench/benchmarks/strings.py b/asv_bench/benchmarks/strings.py
index e6842fbc13..467fab857d 100644
--- a/asv_bench/benchmarks/strings.py
+++ b/asv_bench/benchmarks/strings.py
@@ -19,7 +19,8 @@ class Dtypes:
     def setup(self, dtype):
         try:
             self.s = Series(
-                Index([f"i-{i}" for i in range(10000)], dtype=object), dtype=dtype
+                Index([f"i-{i}" for i in range(10000)], dtype=object)._values,
+                dtype=dtype,
             )
         except ImportError as err:
             raise NotImplementedError from err
diff --git a/pandas/_config/__init__.py b/pandas/_config/__init__.py
index 5b2bac2e8d..0594d1c190 100644
--- a/pandas/_config/__init__.py
+++ b/pandas/_config/__init__.py
@@ -32,13 +32,11 @@ from pandas._config.display import detect_console_encoding
 
 
 def using_copy_on_write() -> bool:
-    _mode_options = _global_config["mode"]
-    return _mode_options["copy_on_write"] is True
+    return True
 
 
 def warn_copy_on_write() -> bool:
-    _mode_options = _global_config["mode"]
-    return _mode_options["copy_on_write"] == "warn"
+    return False
 
 
 def using_nullable_dtypes() -> bool:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
index 037f809eaa..73f9481e53 100644
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -6704,8 +6704,7 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         :ref:`gotchas <gotchas.thread-safety>` when copying in a threading
         environment.
 
-        When ``copy_on_write`` in pandas config is set to ``True``, the
-        ``copy_on_write`` config takes effect even when ``deep=False``.
+        Copy-on-Write protects shallow copies against accidental modifications.
         This means that any changes to the copied data would make a new copy
         of the data upon write (and vice versa). Changes made to either the
         original or copied variable would not be reflected in the counterpart.
@@ -6731,12 +6730,15 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         >>> deep = s.copy()
         >>> shallow = s.copy(deep=False)
 
-        Shallow copy shares data and index with original.
+        Shallow copy shares index with original, the data is a
+        view of the original.
 
         >>> s is shallow
         False
-        >>> s.values is shallow.values and s.index is shallow.index
-        True
+        >>> s.values is shallow.values
+        False
+        >>> s.index is shallow.index
+        False
 
         Deep copy has own copy of data and index.
 
@@ -6745,18 +6747,17 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         >>> s.values is deep.values or s.index is deep.index
         False
 
-        Updates to the data shared by shallow copy and original is reflected
-        in both (NOTE: this will no longer be true for pandas >= 3.0);
-        deep copy remains unchanged.
+        The shallow copy is protected against updating the original object
+        as well. Thus, updates will only reflect in one of both objects.
 
         >>> s.iloc[0] = 3
         >>> shallow.iloc[1] = 4
         >>> s
         a    3
-        b    4
+        b    2
         dtype: int64
         >>> shallow
-        a    3
+        a    1
         b    4
         dtype: int64
         >>> deep
@@ -6779,22 +6780,6 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         0    [10, 2]
         1     [3, 4]
         dtype: object
-
-        **Copy-on-Write is set to true**, the shallow copy is not modified
-        when the original data is changed:
-
-        >>> with pd.option_context("mode.copy_on_write", True):
-        ...     s = pd.Series([1, 2], index=["a", "b"])
-        ...     copy = s.copy(deep=False)
-        ...     s.iloc[0] = 100
-        ...     s
-        a    100
-        b      2
-        dtype: int64
-        >>> copy
-        a    1
-        b    2
-        dtype: int64
         """
         data = self._mgr.copy(deep=deep)
         self._clear_item_cache()
diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py
index a80ee421a1..0495f23508 100644
--- a/pandas/core/indexes/multi.py
+++ b/pandas/core/indexes/multi.py
@@ -3492,6 +3492,8 @@ class MultiIndex(Index):
                         "cannot index with a boolean indexer that "
                         "is not the same length as the index"
                     )
+                if isinstance(k, (ABCSeries, Index)):
+                    k = k._values
                 lvl_indexer = np.asarray(k)
                 if indexer is None:
                     lvl_indexer = lvl_indexer.copy()


Test patch:
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index a3cffb4b03..2b09aa9343 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -73,18 +73,6 @@ jobs:
             env_file: actions-312.yaml
             pattern: "not slow and not network and not single_cpu"
             pandas_copy_on_write: "1"
-          - name: "Copy-on-Write 3.11 (warnings)"
-            env_file: actions-311.yaml
-            pattern: "not slow and not network and not single_cpu"
-            pandas_copy_on_write: "warn"
-          - name: "Copy-on-Write 3.10 (warnings)"
-            env_file: actions-310.yaml
-            pattern: "not slow and not network and not single_cpu"
-            pandas_copy_on_write: "warn"
-          - name: "Copy-on-Write 3.9 (warnings)"
-            env_file: actions-39.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"
diff --git a/pandas/conftest.py b/pandas/conftest.py
index 26e03ca30d..db251a07ae 100644
--- a/pandas/conftest.py
+++ b/pandas/conftest.py
@@ -1963,7 +1963,7 @@ def using_copy_on_write() -> bool:
     """
     Fixture to check if Copy-on-Write is enabled.
     """
-    return pd.options.mode.copy_on_write is True
+    return True
 
 
 @pytest.fixture
@@ -1971,7 +1971,7 @@ def warn_copy_on_write() -> bool:
     """
     Fixture to check if Copy-on-Write is in warning mode.
     """
-    return pd.options.mode.copy_on_write == "warn"
+    return False
 
 
 @pytest.fixture
diff --git a/pandas/tests/copy_view/test_internals.py b/pandas/tests/copy_view/test_internals.py
index 400fb8e03c..f1a4decce6 100644
--- a/pandas/tests/copy_view/test_internals.py
+++ b/pandas/tests/copy_view/test_internals.py
@@ -1,7 +1,6 @@
 import numpy as np
 import pytest
 
-import pandas as pd
 from pandas import DataFrame
 import pandas._testing as tm
 from pandas.tests.copy_view.util import get_array
@@ -42,35 +41,6 @@ def test_consolidate(using_copy_on_write):
         assert df.loc[0, "b"] == 0.1
 
 
-@pytest.mark.single_cpu
-def test_switch_options():
-    # ensure we can switch the value of the option within one session
-    # (assuming data is constructed after switching)
-
-    # using the option_context to ensure we set back to global option value
-    # after running the test
-    with pd.option_context("mode.copy_on_write", False):
-        df = DataFrame({"a": [1, 2, 3], "b": [0.1, 0.2, 0.3]})
-        subset = df[:]
-        subset.iloc[0, 0] = 0
-        # df updated with CoW disabled
-        assert df.iloc[0, 0] == 0
-
-        pd.options.mode.copy_on_write = True
-        df = DataFrame({"a": [1, 2, 3], "b": [0.1, 0.2, 0.3]})
-        subset = df[:]
-        subset.iloc[0, 0] = 0
-        # df not updated with CoW enabled
-        assert df.iloc[0, 0] == 1
-
-        pd.options.mode.copy_on_write = False
-        df = DataFrame({"a": [1, 2, 3], "b": [0.1, 0.2, 0.3]})
-        subset = df[:]
-        subset.iloc[0, 0] = 0
-        # df updated with CoW disabled
-        assert df.iloc[0, 0] == 0
-
-
 @pytest.mark.parametrize("dtype", [np.intp, np.int8])
 @pytest.mark.parametrize(
     "locs, arr",
diff --git a/pandas/tests/extension/conftest.py b/pandas/tests/extension/conftest.py
index 77f1dd2a8e..3a3844d5a8 100644
--- a/pandas/tests/extension/conftest.py
+++ b/pandas/tests/extension/conftest.py
@@ -2,10 +2,7 @@ import operator
 
 import pytest
 
-from pandas import (
-    Series,
-    options,
-)
+from pandas import Series
 
 
 @pytest.fixture
@@ -222,4 +219,4 @@ def using_copy_on_write() -> bool:
     """
     Fixture to check if Copy-on-Write is enabled.
     """
-    return options.mode.copy_on_write is True
+    return True
diff --git a/pandas/tests/frame/methods/test_copy.py b/pandas/tests/frame/methods/test_copy.py
index 6208d0256a..5b72a84320 100644
--- a/pandas/tests/frame/methods/test_copy.py
+++ b/pandas/tests/frame/methods/test_copy.py
@@ -1,10 +1,7 @@
 import numpy as np
 import pytest
 
-import pandas.util._test_decorators as td
-
 from pandas import DataFrame
-import pandas._testing as tm
 
 
 class TestCopy:
@@ -18,25 +15,6 @@ class TestCopy:
         getattr(cp, attr).name = "foo"
         assert getattr(float_frame, attr).name is None
 
-    @td.skip_copy_on_write_invalid_test
-    def test_copy_cache(self):
-        # GH#31784 _item_cache not cleared on copy causes incorrect reads after updates
-        df = DataFrame({"a": [1]})
-
-        df["x"] = [0]
-        df["a"]
-
-        df.copy()
-
-        df["a"].values[0] = -1
-
-        tm.assert_frame_equal(df, DataFrame({"a": [-1], "x": [0]}))
-
-        df["y"] = [0]
-
-        assert df["a"].values[0] == -1
-        tm.assert_frame_equal(df, DataFrame({"a": [-1], "x": [0], "y": [0]}))
-
     def test_copy(self, float_frame, float_string_frame):
         cop = float_frame.copy()
         cop["E"] = cop["A"]
diff --git a/pandas/tests/series/test_ufunc.py b/pandas/tests/series/test_ufunc.py
index 3ef3191743..94a6910509 100644
--- a/pandas/tests/series/test_ufunc.py
+++ b/pandas/tests/series/test_ufunc.py
@@ -5,8 +5,6 @@ import string
 import numpy as np
 import pytest
 
-import pandas.util._test_decorators as td
-
 import pandas as pd
 import pandas._testing as tm
 from pandas.arrays import SparseArray
@@ -456,8 +454,7 @@ def test_array_ufuncs_for_many_arguments():
         ufunc(ser, ser, df)
 
 
-# TODO(CoW) see https://github.com/pandas-dev/pandas/pull/51082
-@td.skip_copy_on_write_not_yet_implemented
+@pytest.mark.xfail(reason="see https://github.com/pandas-dev/pandas/pull/51082")
 def test_np_fix():
     # np.fix is not a ufunc but is composed of several ufunc calls under the hood
     # with `out` and `where` keywords
diff --git a/pandas/tests/test_downstream.py b/pandas/tests/test_downstream.py
index 10776fe5d0..feba0e86c6 100644
--- a/pandas/tests/test_downstream.py
+++ b/pandas/tests/test_downstream.py
@@ -58,8 +58,8 @@ def test_dask_ufunc():
         s = Series([1.5, 2.3, 3.7, 4.0])
         ds = dd.from_pandas(s, npartitions=2)
 
-        result = da.fix(ds).compute()
-        expected = np.fix(s)
+        result = da.log(ds).compute()
+        expected = np.log(s)
         tm.assert_series_equal(result, expected)
     finally:
         pd.set_option("compute.use_numexpr", olduse)
diff --git a/pandas/util/_test_decorators.py b/pandas/util/_test_decorators.py
index 37908c9ac2..7862678128 100644
--- a/pandas/util/_test_decorators.py
+++ b/pandas/util/_test_decorators.py
@@ -33,12 +33,9 @@ from typing import (
 
 import pytest
 
-from pandas._config import get_option
-
 if TYPE_CHECKING:
     from pandas._typing import F
 
-
 from pandas.compat import (
     IS64,
     is_platform_windows,
@@ -144,14 +141,3 @@ def parametrize_fixture_doc(*args) -> Callable[[F], F]:
         return fixture
 
     return documented_fixture
-
-
-skip_copy_on_write_not_yet_implemented = pytest.mark.xfail(
-    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") is True,
-    reason="Test not valid for Copy-on-Write mode",
-)