8000 BUG: fix handling of copy keyword argument when calling __array__ · numpy/numpy@acf49fe · GitHub
[go: up one dir, main page]

Skip to content

Commit acf49fe

Browse files
committed
BUG: fix handling of copy keyword argument when calling __array__
1 parent 8f22d5a commit acf49fe

File tree

8 files changed

+93
-34
lines changed

8 files changed

+93
-34
lines changed

doc/release/upcoming_changes/25168.change.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,15 @@ Now `numpy.array` and `numpy.asarray` support three values for ``copy`` paramete
77
* ``False`` - Never make a copy. If a copy is required a ``ValueError`` is raised.
88

99
The meaning of ``False`` changed as it now raises an exception if a copy is needed.
10+
11+
The ``__array__`` special method now takes a ``copy`` keyword argument.
12+
-----------------------------------------------------------------------
13+
14+
NumPy will pass ``copy`` to the ``__array__`` special method in situations where
15+
it would be set to a non-default value (e.g. in a call to
16+
``np.asarray(some_object, copy=False)``). Currently, if an
17+
unexpected keyword argument error is raised after this, NumPy will print a
18+
warning and re-try without the ``copy`` keyword argument. Implementations of
19+
objects implementing the ``__array__`` protocol should accept a ``copy`` keyword
20+
argument with the same meaning as when passed to `numpy.array` or
21+
`numpy.asarray`.

numpy/_core/src/multiarray/array_coercion.c

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -958,7 +958,8 @@ PyArray_AdaptDescriptorToArray(
958958
* (Initially it is a pointer to the user-provided head pointer).
959959
* @param fixed_DType User provided fixed DType class
960960
* @param flags Discovery flags (reporting and behaviour flags, see def.)
961-
* @param never_copy Specifies if a copy is allowed during array creation.
961+
* @param copy Specifies the copy behavior. -1 is corresponds to copy=None,
962+
* 0 to copy=False, and 1 to copy=True in the Python API.
962963
* @return The updated number of maximum dimensions (i.e. scalars will set
963964
* this to the current dimensions).
964965
*/
@@ -968,7 +969,7 @@ PyArray_DiscoverDTypeAndShape_Recursive(
968969
npy_intp out_shape[NPY_MAXDIMS],
969970
coercion_cache_obj ***coercion_cache_tail_ptr,
970971
PyArray_DTypeMeta *fixed_DType, enum _dtype_discovery_flags *flags,
971-
int never_copy)
972+
int copy)
972973
{
973974
PyArrayObject *arr = NULL;
974975
PyObject *seq;
@@ -1026,7 +1027,7 @@ PyArray_DiscoverDTypeAndShape_Recursive(
10261027
requested_descr = *out_descr;
10271028
}
10281029
arr = (PyArrayObject *)_array_from_array_like(obj,
1029-
requested_descr, 0, NULL, never_copy);
1030+
requested_descr, 0, NULL, copy);
10301031
if (arr == NULL) {
10311032
return -1;
10321033
}
@@ -1173,7 +1174,7 @@ PyArray_DiscoverDTypeAndShape_Recursive(
11731174
max_dims = PyArray_DiscoverDTypeAndShape_Recursive(
11741175
objects[i], curr_dims + 1, max_dims,
11751176
out_descr, out_shape, coercion_cache_tail_ptr, fixed_DType,
1176-
flags, never_copy);
1177+
flags, copy);
11771178

11781179
if (max_dims < 0) {
11791180
return -1;
@@ -1213,7 +1214,8 @@ PyArray_DiscoverDTypeAndShape_Recursive(
12131214
* The result may be unchanged (remain NULL) when converting a
12141215
* sequence with no elements. In this case it is callers responsibility
12151216
* to choose a default.
1216-
* @param never_copy Specifies that a copy is not allowed.
1217+
* @param copy Specifies the copy behavior. -1 is corresponds to copy=None,
1218+
* 0 to copy=False, and 1 to copy=True in the Python API.
12171219
* @return dimensions of the discovered object or -1 on error.
12181220
* WARNING: If (and only if) the output is a single array, the ndim
12191221
* returned _can_ exceed the maximum allowed number of dimensions.
@@ -1226,7 +1228,7 @@ PyArray_DiscoverDTypeAndShape(
12261228
npy_intp out_shape[NPY_MAXDIMS],
12271229
coercion_cache_obj **coercion_cache,
12281230
PyArray_DTypeMeta *fixed_DType, PyArray_Descr *requested_descr,
1229-
PyArray_Descr **out_descr, int never_copy)
1231+
PyArray_Descr **out_descr, int copy)
12301232
{
12311233
coercion_cache_obj **coercion_cache_head = coercion_cache;
12321234
*coercion_cache = NULL;
@@ -1273,7 +1275,7 @@ PyArray_DiscoverDTypeAndShape(
12731275

12741276
int ndim = PyArray_DiscoverDTypeAndShape_Recursive(
12751277
obj, 0, max_dims, out_descr, out_shape, &coercion_cache,
1276-
fixed_DType, &flags, never_copy);
1278+
fixed_DType, &flags, copy);
12771279
if (ndim < 0) {
12781280
goto fail;
12791281
}

numpy/_core/src/multiarray/array_coercion.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ PyArray_DiscoverDTypeAndShape(
4040
npy_intp out_shape[NPY_MAXDIMS],
4141
coercion_cache_obj **coercion_cache,
4242
PyArray_DTypeMeta *fixed_DType, PyArray_Descr *requested_descr,
43-
PyArray_Descr **out_descr, int never_copy);
43+
PyArray_Descr **out_descr, int copy);
4444

4545
NPY_NO_EXPORT PyObject *
4646
_discover_array_parameters(PyObject *NPY_UNUSED(self),

numpy/_core/src/multiarray/arrayobject.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ PyArray_CopyObject(PyArrayObject *dest, PyObject *src_object)
251251
*/
252252
ndim = PyArray_DiscoverDTypeAndShape(src_object,
253253
PyArray_NDIM(dest), dims, &cache,
254-
NPY_DTYPE(PyArray_DESCR(dest)), PyArray_DESCR(dest), &dtype, 0);
254+
NPY_DTYPE(PyArray_DESCR(dest)), PyArray_DESCR(dest), &dtype, 1);
255255
if (ndim < 0) {
256256
return -1;
257257
}

numpy/_core/src/multiarray/common.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ PyArray_DTypeFromObject(PyObject *obj, int maxdims, PyArray_Descr **out_dtype)
119119
int ndim;
120120

121121
ndim = PyArray_DiscoverDTypeAndShape(
122-
obj, maxdims, shape, &cache, NULL, NULL, out_dtype, 0);
122+
obj, maxdims, shape, &cache, NULL, NULL, out_dtype, 1);
123123
if (ndim < 0) {
124124
return -1;
125125
}

numpy/_core/src/multiarray/ctors.c

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1421,7 +1421,7 @@ _array_from_buffer_3118(PyObject *memoryview)
14211421
* DType may be used, but is not enforced.
14221422
* @param writeable whether the result must be writeable.
14231423
* @param context Unused parameter, must be NULL (should be removed later).
1424-
* @param never_copy Specifies that a copy is not allowed.
1424+
* @param copy Specifies the copy behavior.
14251425
*
14261426
* @returns The array object, Py_NotImplemented if op is not array-like,
14271427
* or NULL with an error set. (A new reference to Py_NotImplemented
@@ -1430,7 +1430,7 @@ _array_from_buffer_3118(PyObject *memoryview)
14301430
NPY_NO_EXPORT PyObject *
14311431
_array_from_array_like(PyObject *op,
14321432
PyArray_Descr *requested_dtype, npy_bool writeable, PyObject *context,
1433-
int never_copy) {
1433+
int copy) {
14341434
PyObject* tmp;
14351435

14361436
/*
@@ -1478,7 +1478,7 @@ _array_from_array_like(PyObject *op,
14781478
}
14791479

14801480
if (tmp == Py_NotImplemented) {
1481-
tmp = PyArray_FromArrayAttr_int(op, requested_dtype, never_copy);
1481+
tmp = PyArray_FromArrayAttr_int(op, requested_dtype, copy);
14821482
if (tmp == NULL) {
14831483
return NULL;
14841484
}
@@ -1563,9 +1563,15 @@ PyArray_FromAny_int(PyObject *op, PyArray_Descr *in_descr,
15631563
return NULL;
15641564
}
15651565

1566-
ndim = PyArray_DiscoverDTypeAndShape(op,
1567-
NPY_MAXDIMS, dims, &cache, in_DType, in_descr, &dtype,
1568-
flags & NPY_ARRAY_ENSURENOCOPY);
1566+
// Default is copy = None
1567+
int copy = -1;
1568+
1569+
if (flags & NPY_ARRAY_ENSURENOCOPY) {
1570+
copy = 0;
1571+
}
1572+
1573+
ndim = PyArray_DiscoverDTypeAndShape(
1574+
op, NPY_MAXDIMS, dims, &cache, in_DType, in_descr, &dtype, copy);
15691575

15701576
if (ndim < 0) {
15711577
return NULL;
@@ -2408,15 +2414,16 @@ PyArray_FromInterface(PyObject *origin)
24082414
* @param op The Python object to convert to an array.
24092415
* @param descr The desired `arr.dtype`, passed into the `__array__` call,
24102416
* as information but is not checked/enforced!
2411-
* @param never_copy Specifies that a copy is not allowed.
2412-
* NOTE: For false it passes `op.__array__(copy=None)`,
2413-
* for true: `op.__array__(copy=False)`.
2417+
* @param copy Specifies the copy behavior
2418+
* NOTE: For copy == -1 it passes `op.__array__(copy=None)`,
2419+
* for copy == 0, `op.__array__(copy=False)`, and
2420+
* for copy == 1, `op.__array__(copy=True).
24142421
* @returns NotImplemented if `__array__` is not defined or a NumPy array
24152422
* (or subclass). On error, return NULL.
24162423
*/
24172424
NPY_NO_EXPORT PyObject *
24182425
PyArray_FromArrayAttr_int(
2419-
PyObject *op, PyArray_Descr *descr, int never_copy)
2426+
PyObject *op, PyArray_Descr *descr, int copy)
24202427
{
24212428
PyObject *new;
24222429
PyObject *array_meth;
@@ -2440,36 +2447,59 @@ PyArray_FromArrayAttr_int(
24402447
return Py_NotImplemented;
24412448
}
24422449

2443-
PyObject *copy = never_copy ? Py_False : Py_None;
24442450
PyObject *kwargs = PyDict_New();
2445-
PyDict_SetItemString(kwargs, "copy", copy);
2451+
2452+
/*
2453+
* Only if the value of `copy` isn't the default one, we try to pass it
2454+
* along; for backwards compatibility we then retry if it fails because the
2455+
* signature of the __array__ method being called does not have `copy`.
2456+
*/
2457+
int copy_passed = 0;
2458+
if (copy != -1) {
2459+
copy_passed = 1;
2460+
PyObject *copy_obj = copy == 1 ? Py_True : Py_False;
2461+
PyDict_SetItemString(kwargs, "copy", copy_obj);
2462+
}
24462463
PyObject *args = descr != NULL ? PyTuple_Pack(1, descr) : PyTuple_New(0);
24472464

24482465
new = PyObject_Call(array_meth, args, kwargs);
24492466

2450-
if (PyErr_Occurred()) {
2467+
if (new == NULL) {
2468+
PyObject *errmsg_substr = PyUnicode_FromString(
2469+
"__array__() got an unexpected keyword argument 'copy'");
2470+
if (errmsg_substr == NULL) {
2471+
return NULL;
2472+
}
24512473
PyObject *type, *value, *traceback;
24522474
PyErr_Fetch(&type, &value, &traceback);
2453-
if (PyUnicode_Check(value) && PyUnicode_CompareWithASCIIString(value,
2454-
"__array__() got an unexpected keyword argument 'copy'") == 0) {
2475+
if (PyUnicode_Check(value) && PyUnicode_Contains(value, errmsg_substr) > 0) {
24552476
Py_DECREF(type);
24562477
Py_XDECREF(value);
24572478
Py_XDECREF(traceback);
2479+
Py_DECREF(errmsg_substr);
24582480
if (PyErr_WarnEx(PyExc_UserWarning,
24592481
"__array__ should implement 'dtype' and 'copy' keywords", 1) < 0) {
24602482
return NULL;
24612483
}
2462-
Py_SETREF(new, PyObject_Call(array_meth, args, NULL));
2484+
if (copy_passed) { /* try again */
2485+
PyDict_DelItemString(kwargs, "copy");
2486+
new = PyObject_Call(array_meth, args, kwargs);
2487+
if (new == NULL) {
2488+
Py_DECREF(kwargs);
2489+
return NULL;
2490+
}
2491+
}
24632492
} else {
24642493
PyErr_Restore(type, value, traceback);
2494+
Py_DECREF(errmsg_substr);
2495+
Py_DECREF(kwargs);
24652496
return NULL;
24662497
}
24672498
}
24682499

2500+
Py_DECREF(kwargs);
24692501
Py_DECREF(array_meth);
2470-
if (new == NULL) {
2471-
return NULL;
2472-
}
2502+
24732503
if (!PyArray_Check(new)) {
24742504
PyErr_SetString(PyExc_ValueError,
24752505
"object __array__ method not " \

numpy/_core/src/multiarray/ctors.h

+2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ PyArray_New(
5151
NPY_NO_EXPORT PyObject *
5252
_array_from_array_like(PyObject *op,
5353
PyArray_Descr *requested_dtype, npy_bool writeable, PyObject *context,
54-
int never_copy);
54+
int copy);
5555

5656
NPY_NO_EXPORT PyObject *
5757
PyArray_FromAny_int(PyObject *op, PyArray_Descr *in_descr,
@@ -82,7 +82,7 @@ PyArray_FromInterface(PyObject *input);
8282

8383
NPY_NO_EXPORT PyObject *
8484
PyArray_FromArrayAttr_int(
85-
PyObject *op, PyArray_Descr *descr, int never_copy);
85+
PyObject *op, PyArray_Descr *descr, int copy);
8686

8787
NPY_NO_EXPORT PyObject *
8888
PyArray_FromArrayAttr(PyObject *op, PyArray_Descr *typecode,

numpy/_core/tests/test_multiarray.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8414,9 +8414,9 @@ def test_buffer_interface(self):
84148414
assert np.may_share_memory(arr, res)
84158415

84168416
def test_array_interfaces(self):
8417-
# Array interface gives direct memory access (much like a memoryview)
84188417
base_arr = np.arange(10)
84198418

8419+
# Array interface gives direct memory access (much like a memoryview)
84208420
class ArrayLike:
84218421
__array_interface__ = base_arr.__array_interface__
84228422

@@ -8433,8 +8433,6 @@ def test___array__(self):
84338433

84348434
class ArrayLike:
84358435
def __array__(self, dtype=None, copy=None):
8436-
# __array__ should return a copy, numpy cannot know this
8437-
# however.
84388436
return base_arr
84398437

84408438
arr = ArrayLike()
@@ -8465,6 +8463,23 @@ def test___array__copy_arg(self):
84658463
with pytest.raises(ValueError):
84668464
np.shares_memory(a, a.__array__(float, copy=False))
84678465

8466+
base_arr = np.arange(10)
8467+
8468+
class ArrayLikeNoCopy:
8469+
def __array__(self, dtype=None):
8470+
return base_arr
8471+
8472+
a = ArrayLikeNoCopy()
8473+
8474+
# explicitly passing copy=None shouldn't raise a warning
8475+
arr = np.array(a, copy=None)
8476+
assert_array_equal(arr, base_arr)
8477+
assert arr is base_arr
8478+
8479+
with pytest.warns(UserWarning, match=("should implement 'dtype' "
8480+
"and 'copy' keywords")):
8481+
np.array(a, copy=False)
8482+
84688483
@pytest.mark.parametrize(
84698484
"arr", [np.ones(()), np.arange(81).reshape((9, 9))])
84708485
@pytest.mark.parametrize("order1", ["C", "F", None])

0 commit comments

Comments
 (0)
0