Skip to content

Commit e90bb0e

Browse files
DEPR: Raise FutureWarning about raising an error in __array__ when copy=False cannot be honored (pandas-dev#60395)
Co-authored-by: Joris Van den Bossche <[email protected]>
1 parent 564c7b2 commit e90bb0e

File tree

18 files changed

+151
-42
lines changed

18 files changed

+151
-42
lines changed

doc/source/whatsnew/v2.3.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Other enhancements
3434

3535
- The semantics for the ``copy`` keyword in ``__array__`` methods (i.e. called
3636
when using ``np.array()`` or ``np.asarray()`` on pandas objects) has been
37-
updated to work correctly with NumPy >= 2 (:issue:`57739`)
37+
updated to raise FutureWarning with NumPy >= 2 (:issue:`60340`)
3838
- The :meth:`~Series.sum` reduction is now implemented for ``StringDtype`` columns (:issue:`59853`)
3939
-
4040

pandas/core/arrays/arrow/array.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
cast,
1313
)
1414
import unicodedata
15+
import warnings
1516

1617
import numpy as np
1718

@@ -28,6 +29,7 @@
2829
pa_version_under13p0,
2930
)
3031
from pandas.util._decorators import doc
32+
from pandas.util._exceptions import find_stack_level
3133
from pandas.util._validators import validate_fillna_kwargs
3234

3335
from pandas.core.dtypes.cast import (
@@ -663,9 +665,15 @@ def __array__(
663665
) -> np.ndarray:
664666
"""Correctly construct numpy arrays when passed to `np.asarray()`."""
665667
if copy is False:
666-
# TODO: By using `zero_copy_only` it may be possible to implement this
667-
raise ValueError(
668-
"Unable to avoid copy while creating an array as requested."
668+
warnings.warn(
669+
"Starting with NumPy 2.0, the behavior of the 'copy' keyword has "
670+
"changed and passing 'copy=False' raises an error when returning "
671+
"a zero-copy NumPy array is not possible. pandas will follow "
672+
"this behavior starting with pandas 3.0.\nThis conversion to "
673+
"NumPy requires a copy, but 'copy=False' was passed. Consider "
674+
"using 'np.asarray(..)' instead.",
675+
FutureWarning,
676+
stacklevel=find_stack_level(),
669677
)
670678
elif copy is None:
671679
# `to_numpy(copy=False)` has the meaning of NumPy `copy=None`.

pandas/core/arrays/categorical.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -1672,8 +1672,15 @@ def __array__(
16721672
array(['a', 'b'], dtype=object)
16731673
"""
16741674
if copy is False:
1675-
raise ValueError(
1676-
"Unable to avoid copy while creating an array as requested."
1675+
warnings.warn(
1676+
"Starting with NumPy 2.0, the behavior of the 'copy' keyword has "
1677+
"changed and passing 'copy=False' raises an error when returning "
1678+
"a zero-copy NumPy array is not possible. pandas will follow "
1679+
"this behavior starting with pandas 3.0.\nThis conversion to "
1680+
"NumPy requires a copy, but 'copy=False' was passed. Consider "
1681+
"using 'np.asarray(..)' instead.",
1682+
FutureWarning,
1683+
stacklevel=find_stack_level(),
16771684
)
16781685

16791686
ret = take_nd(self.categories._values, self._codes)

pandas/core/arrays/datetimelike.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -359,9 +359,17 @@ def __array__(
359359
# used for Timedelta/DatetimeArray, overwritten by PeriodArray
360360
if is_object_dtype(dtype):
361361
if copy is False:
362-
raise ValueError(
363-
"Unable to avoid copy while creating an array as requested."
362+
warnings.warn(
363+
"Starting with NumPy 2.0, the behavior of the 'copy' keyword has "
364+
"changed and passing 'copy=False' raises an error when returning "
365+
"a zero-copy NumPy array is not possible. pandas will follow this "
366+
"behavior starting with pandas 3.0.\nThis conversion to NumPy "
367+
"requires a copy, but 'copy=False' was passed. Consider using "
368+
"'np.asarray(..)' instead.",
369+
FutureWarning,
370+
stacklevel=find_stack_level(),
364371
)
372+
365373
return np.array(list(self), dtype=object)
366374

367375
if copy is True:

pandas/core/arrays/interval.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from pandas.compat.numpy import function as nv
4343
from pandas.errors import IntCastingNaNError
4444
from pandas.util._decorators import Appender
45+
from pandas.util._exceptions import find_stack_level
4546

4647
from pandas.core.dtypes.cast import (
4748
LossySetitemError,
@@ -1575,8 +1576,15 @@ def __array__(
15751576
objects (with dtype='object')
15761577
"""
15771578
if copy is False:
1578-
raise ValueError(
1579-
"Unable to avoid copy while creating an array as requested."
1579+
warnings.warn(
1580+
"Starting with NumPy 2.0, the behavior of the 'copy' keyword has "
1581+
"changed and passing 'copy=False' raises an error when returning "
1582+
"a zero-copy NumPy array is not possible. pandas will follow "
1583+
"this behavior starting with pandas 3.0.\nThis conversion to "
1584+
"NumPy requires a copy, but 'copy=False' was passed. Consider "
1585+
"using 'np.asarray(..)' instead.",
1586+
FutureWarning,
1587+
stacklevel=find_stack_level(),
15801588
)
15811589

15821590
left = self._left

pandas/core/arrays/masked.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
)
3939
from pandas.errors import AbstractMethodError
4040
from pandas.util._decorators import doc
41+
from pandas.util._exceptions import find_stack_level
4142
from pandas.util._validators import validate_fillna_kwargs
4243

4344
from pandas.core.dtypes.base import ExtensionDtype
@@ -604,8 +605,16 @@ def __array__(
604605
if not self._hasna:
605606
# special case, here we can simply return the underlying data
606607
return np.array(self._data, dtype=dtype, copy=copy)
607-
raise ValueError(
608-
"Unable to avoid copy while creating an array as requested."
608+
609+
warnings.warn(
610+
"Starting with NumPy 2.0, the behavior of the 'copy' keyword has "
611+
"changed and passing 'copy=False' raises an error when returning "
612+
"a zero-copy NumPy array is not possible. pandas will follow "
613+
"this behavior starting with pandas 3.0.\nThis conversion to "
614+
"NumPy requires a copy, but 'copy=False' was passed. Consider "
615+
"using 'np.asarray(..)' instead.",
616+
FutureWarning,
617+
stacklevel=find_stack_level(),
609618
)
610619

611620
if copy is None:

pandas/core/arrays/period.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -415,8 +415,15 @@ def __array__(
415415
return np.array(self.asi8, dtype=dtype)
416416

417417
if copy is False:
418-
raise ValueError(
419-
"Unable to avoid copy while creating an array as requested."
418+
warnings.warn(
419+
"Starting with NumPy 2.0, the behavior of the 'copy' keyword has "
420+
"changed and passing 'copy=False' raises an error when returning "
421+
"a zero-copy NumPy array is not possible. pandas will follow "
422+
"this behavior starting with pandas 3.0.\nThis conversion to "
423+
"NumPy requires a copy, but 'copy=False' was passed. Consider "
424+
"using 'np.asarray(..)' instead.",
425+
FutureWarning,
426+
stacklevel=find_stack_level(),
420427
)
421428

422429
if dtype == bool:

pandas/core/arrays/sparse/array.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -562,8 +562,15 @@ def __array__(
562562
return self.sp_values
563563

564564
if copy is False:
565-
raise ValueError(
566-
"Unable to avoid copy while creating an array as requested."
565+
warnings.warn(
566+
"Starting with NumPy 2.0, the behavior of the 'copy' keyword has "
567+
"changed and passing 'copy=False' raises an error when returning "
568+
"a zero-copy NumPy array is not possible. pandas will follow "
569+
"this behavior starting with pandas 3.0.\nThis conversion to "
570+
"NumPy requires a copy, but 'copy=False' was passed. Consider "
571+
"using 'np.asarray(..)' instead.",
572+
FutureWarning,
573+
stacklevel=find_stack_level(),
567574
)
568575

569576
fill_value = self.fill_value

pandas/core/generic.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -2151,9 +2151,16 @@ def __array__(
21512151
) -> np.ndarray:
21522152
if copy is False and not self._mgr.is_single_block and not self.empty:
21532153
# check this manually, otherwise ._values will already return a copy
2154-
# and np.array(values, copy=False) will not raise an error
2155-
raise ValueError(
2156-
"Unable to avoid copy while creating an array as requested."
2154+
# and np.array(values, copy=False) will not raise a warning
2155+
warnings.warn(
2156+
"Starting with NumPy 2.0, the behavior of the 'copy' keyword has "
2157+
"changed and passing 'copy=False' raises an error when returning "
2158+
"a zero-copy NumPy array is not possible. pandas will follow "
2159+
"this behavior starting with pandas 3.0.\nThis conversion to "
2160+
"NumPy requires a copy, but 'copy=False' was passed. Consider "
2161+
"using 'np.asarray(..)' instead.",
2162+
FutureWarning,
2163+
stacklevel=find_stack_level(),
21572164
)
21582165
values = self._values
21592166
if copy is None:

pandas/core/indexes/multi.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -1314,8 +1314,15 @@ def __array__(self, dtype=None, copy=None) -> np.ndarray:
13141314
"""the array interface, return my values"""
13151315
if copy is False:
13161316
# self.values is always a newly construct array, so raise.
1317-
raise ValueError(
1318-
"Unable to avoid copy while creating an array as requested."
1317+
warnings.warn(
1318+
"Starting with NumPy 2.0, the behavior of the 'copy' keyword has "
1319+
"changed and passing 'copy=False' raises an error when returning "
1320+
"a zero-copy NumPy array is not possible. pandas will follow "
1321+
"this behavior starting with pandas 3.0.\nThis conversion to "
1322+
"NumPy requires a copy, but 'copy=False' was passed. Consider "
1323+
"using 'np.asarray(..)' instead.",
1324+
FutureWarning,
1325+
stacklevel=find_stack_level(),
13191326
)
13201327
if copy is True:
13211328
# explicit np.array call to ensure a copy is made and unique objects

pandas/tests/arrays/sparse/test_array.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -500,8 +500,8 @@ def test_array_interface(arr_data, arr):
500500
# copy=False semantics are only supported in NumPy>=2.
501501
return
502502

503-
# for sparse arrays, copy=False is never allowed
504-
with pytest.raises(ValueError, match="Unable to avoid copy while creating"):
503+
msg = "Starting with NumPy 2.0, the behavior of the 'copy' keyword has changed"
504+
with tm.assert_produces_warning(FutureWarning, match=msg):
505505
np.array(arr, copy=False)
506506

507507
# except when there are actually no sparse filled values

pandas/tests/base/test_conversion.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -378,8 +378,8 @@ def test_to_numpy(arr, expected, zero_copy, index_or_series_or_array):
378378
return
379379

380380
if not zero_copy:
381-
with pytest.raises(ValueError, match="Unable to avoid copy while creating"):
382-
# An error is always acceptable for `copy=False`
381+
msg = "Starting with NumPy 2.0, the behavior of the 'copy' keyword has changed"
382+
with tm.assert_produces_warning(FutureWarning, match=msg):
383383
np.array(thing, copy=False)
384384

385385
else:

pandas/tests/copy_view/test_array.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,8 @@ def test_dataframe_multiple_numpy_dtypes():
187187
if np_version_gt2:
188188
# copy=False semantics are only supported in NumPy>=2.
189189

190-
with pytest.raises(ValueError, match="Unable to avoid copy while creating"):
190+
msg = "Starting with NumPy 2.0, the behavior of the 'copy' keyword has changed"
191+
with pytest.raises(FutureWarning, match=msg):
191192
arr = np.array(df, copy=False)
192193

193194
arr = np.array(df, copy=True)

pandas/tests/extension/base/interface.py

+22-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import warnings
2+
13
import numpy as np
24
import pytest
35

@@ -82,15 +84,27 @@ def test_array_interface_copy(self, data):
8284
# copy=False semantics are only supported in NumPy>=2.
8385
return
8486

85-
try:
87+
warning_raised = False
88+
msg = "Starting with NumPy 2.0, the behavior of the 'copy' keyword has changed"
89+
with warnings.catch_warnings(record=True) as w:
90+
warnings.simplefilter("always")
8691
result_nocopy1 = np.array(data, copy=False)
87-
except ValueError:
88-
# An error is always acceptable for `copy=False`
89-
return
90-
91-
result_nocopy2 = np.array(data, copy=False)
92-
# If copy=False was given and did not raise, these must share the same data
93-
assert np.may_share_memory(result_nocopy1, result_nocopy2)
92+
assert len(w) <= 1
93+
if len(w):
94+
warning_raised = True
95+
assert msg in str(w[0].message)
96+
97+
with warnings.catch_warnings(record=True) as w:
98+
warnings.simplefilter("always")
99+
result_nocopy2 = np.array(data, copy=False)
100+
assert len(w) <= 1
101+
if len(w):
102+
warning_raised = True
103+
assert msg in str(w[0].message)
104+
105+
if not warning_raised:
106+
# If copy=False was given and did not raise, these must share the same data
107+
assert np.may_share_memory(result_nocopy1, result_nocopy2)
94108

95109
def test_is_extension_array_dtype(self, data):
96110
assert is_extension_array_dtype(data)

pandas/tests/extension/decimal/test_decimal.py

+20
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import numpy as np
77
import pytest
88

9+
from pandas.compat.numpy import np_version_gt2
10+
911
import pandas as pd
1012
import pandas._testing as tm
1113
from pandas.tests.extension import base
@@ -289,6 +291,24 @@ def test_series_repr(self, data):
289291
def test_unary_ufunc_dunder_equivalence(self, data, ufunc):
290292
super().test_unary_ufunc_dunder_equivalence(data, ufunc)
291293

294+
def test_array_interface_copy(self, data):
295+
result_copy1 = np.array(data, copy=True)
296+
result_copy2 = np.array(data, copy=True)
297+
assert not np.may_share_memory(result_copy1, result_copy2)
298+
if not np_version_gt2:
299+
# copy=False semantics are only supported in NumPy>=2.
300+
return
301+
302+
try:
303+
result_nocopy1 = np.array(data, copy=False)
304+
except ValueError:
305+
# An error is always acceptable for `copy=False`
306+
return
307+
308+
result_nocopy2 = np.array(data, copy=False)
309+
# If copy=False was given and did not raise, these must share the same data
310+
assert np.may_share_memory(result_nocopy1, result_nocopy2)
311+
292312

293313
def test_take_na_value_other_decimal():
294314
arr = DecimalArray([decimal.Decimal("1.0"), decimal.Decimal("2.0")])

pandas/tests/extension/json/array.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@
2525
TYPE_CHECKING,
2626
Any,
2727
)
28+
import warnings
2829

2930
import numpy as np
3031

32+
from pandas.util._exceptions import find_stack_level
33+
3134
from pandas.core.dtypes.cast import construct_1d_object_array_from_listlike
3235
from pandas.core.dtypes.common import (
3336
is_bool_dtype,
@@ -148,8 +151,15 @@ def __ne__(self, other):
148151

149152
def __array__(self, dtype=None, copy=None):
150153
if copy is False:
151-
raise ValueError(
152-
"Unable to avoid copy while creating an array as requested."
154+
warnings.warn(
155+
"Starting with NumPy 2.0, the behavior of the 'copy' keyword has "
156+
"changed and passing 'copy=False' raises an error when returning "
157+
"a zero-copy NumPy array is not possible. pandas will follow "
158+
"this behavior starting with pandas 3.0.\nThis conversion to "
159+
"NumPy requires a copy, but 'copy=False' was passed. Consider "
160+
"using 'np.asarray(..)' instead.",
161+
FutureWarning,
162+
stacklevel=find_stack_level(),
153163
)
154164

155165
if dtype is None:

pandas/tests/indexes/multi/test_conversion.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ def test_array_interface(idx):
4747
return
4848

4949
# for MultiIndex, copy=False is never allowed
50-
with pytest.raises(ValueError, match="Unable to avoid copy while creating"):
50+
msg = "Starting with NumPy 2.0, the behavior of the 'copy' keyword has changed"
51+
with tm.assert_produces_warning(FutureWarning, match=msg):
5152
np.array(idx, copy=False)
5253

5354

pandas/tests/io/test_fsspec.py

-5
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
from pandas._config import using_string_dtype
77

8-
from pandas.compat import HAS_PYARROW
9-
108
from pandas import (
119
DataFrame,
1210
date_range,
@@ -170,9 +168,6 @@ def test_excel_options(fsspectest):
170168
assert fsspectest.test[0] == "read"
171169

172170

173-
@pytest.mark.xfail(
174-
using_string_dtype() and HAS_PYARROW, reason="TODO(infer_string) fastparquet"
175-
)
176171
def test_to_parquet_new_file(cleared_fs, df1):
177172
"""Regression test for writing to a not-yet-existent GCS Parquet file."""
178173
pytest.importorskip("fastparquet")

0 commit comments

Comments
 (0)