From c8cdda22367ad96e719a916eb30418cb72e7c1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Sok=C3=B3=C5=82?= Date: Fri, 5 Apr 2024 13:04:57 +0200 Subject: [PATCH 1/4] API: Enforce one copy for __array__ when copy=True --- numpy/_core/src/multiarray/array_coercion.c | 26 +++++++++-- numpy/_core/src/multiarray/array_coercion.h | 2 +- numpy/_core/src/multiarray/arrayobject.c | 2 +- numpy/_core/src/multiarray/common.c | 2 +- numpy/_core/src/multiarray/ctors.c | 30 +++++++++---- numpy/_core/src/multiarray/ctors.h | 4 +- numpy/_core/tests/test_array_coercion.py | 4 +- numpy/_core/tests/test_multiarray.py | 48 +++++++++++++++++---- numpy/_core/tests/test_protocols.py | 4 +- 9 files changed, 94 insertions(+), 28 deletions(-) diff --git a/numpy/_core/src/multiarray/array_coercion.c b/numpy/_core/src/multiarray/array_coercion.c index f63dbbc77e1f..3d4174c0a87f 100644 --- a/numpy/_core/src/multiarray/array_coercion.c +++ b/numpy/_core/src/multiarray/array_coercion.c @@ -99,6 +99,7 @@ enum _dtype_discovery_flags { DISCOVER_TUPLES_AS_ELEMENTS = 1 << 4, MAX_DIMS_WAS_REACHED = 1 << 5, DESCRIPTOR_WAS_SET = 1 << 6, + COPY_WAS_CREATED = 1 << 7, }; @@ -1027,8 +1028,9 @@ PyArray_DiscoverDTypeAndShape_Recursive( /* __array__ may be passed the requested descriptor if provided */ requested_descr = *out_descr; } + int was_copied = 0; arr = (PyArrayObject *)_array_from_array_like(obj, - requested_descr, 0, NULL, copy); + requested_descr, 0, NULL, copy, &was_copied); if (arr == NULL) { return -1; } @@ -1036,6 +1038,9 @@ PyArray_DiscoverDTypeAndShape_Recursive( Py_DECREF(arr); arr = NULL; } + if (was_copied == 1) { + *flags |= COPY_WAS_CREATED; + } } if (arr != NULL) { /* @@ -1170,6 +1175,15 @@ PyArray_DiscoverDTypeAndShape_Recursive( return -1; } + /* + * For a sequence we need to make a copy of the final aggreate anyway. + * There's no need to pass explicit `copy=True`, so we switch + * to `copy=None` (copy if needed). + */ + if (copy == 1) { + copy = -1; + } + /* Recursive call for each sequence item */ for (Py_ssize_t i = 0; i < size; i++) { max_dims = PyArray_DiscoverDTypeAndShape_Recursive( @@ -1217,6 +1231,8 @@ PyArray_DiscoverDTypeAndShape_Recursive( * to choose a default. * @param copy Specifies the copy behavior. -1 is corresponds to copy=None, * 0 to copy=False, and 1 to copy=True in the Python API. + * @param was_copied Set to 1 if it can be assumed that a copy was made + * by implementor. * @return dimensions of the discovered object or -1 on error. * WARNING: If (and only if) the output is a single array, the ndim * returned _can_ exceed the maximum allowed number of dimensions. @@ -1229,7 +1245,7 @@ PyArray_DiscoverDTypeAndShape( npy_intp out_shape[NPY_MAXDIMS], coercion_cache_obj **coercion_cache, PyArray_DTypeMeta *fixed_DType, PyArray_Descr *requested_descr, - PyArray_Descr **out_descr, int copy) + PyArray_Descr **out_descr, int copy, int *was_copied) { coercion_cache_obj **coercion_cache_head = coercion_cache; *coercion_cache = NULL; @@ -1282,6 +1298,10 @@ PyArray_DiscoverDTypeAndShape( goto fail; } + if (was_copied != NULL && flags & COPY_WAS_CREATED) { + *was_copied = 1; + } + if (NPY_UNLIKELY(flags & FOUND_RAGGED_ARRAY)) { /* * If max-dims was reached and the dimensions reduced, this is ragged. @@ -1396,7 +1416,7 @@ _discover_array_parameters(PyObject *NPY_UNUSED(self), int ndim = PyArray_DiscoverDTypeAndShape( obj, NPY_MAXDIMS, shape, &coercion_cache, - dt_info.dtype, dt_info.descr, (PyArray_Descr **)&out_dtype, 0); + dt_info.dtype, dt_info.descr, (PyArray_Descr **)&out_dtype, 0, NULL); Py_XDECREF(dt_info.dtype); Py_XDECREF(dt_info.descr); if (ndim < 0) { diff --git a/numpy/_core/src/multiarray/array_coercion.h b/numpy/_core/src/multiarray/array_coercion.h index e6639ba1bba9..0fca0702641b 100644 --- a/numpy/_core/src/multiarray/array_coercion.h +++ b/numpy/_core/src/multiarray/array_coercion.h @@ -40,7 +40,7 @@ PyArray_DiscoverDTypeAndShape( npy_intp out_shape[NPY_MAXDIMS], coercion_cache_obj **coercion_cache, PyArray_DTypeMeta *fixed_DType, PyArray_Descr *requested_descr, - PyArray_Descr **out_descr, int copy); + PyArray_Descr **out_descr, int copy, int *was_copied); NPY_NO_EXPORT PyObject * _discover_array_parameters(PyObject *NPY_UNUSED(self), diff --git a/numpy/_core/src/multiarray/arrayobject.c b/numpy/_core/src/multiarray/arrayobject.c index 3001f84edf05..5139bc8b4f00 100644 --- a/numpy/_core/src/multiarray/arrayobject.c +++ b/numpy/_core/src/multiarray/arrayobject.c @@ -251,7 +251,7 @@ PyArray_CopyObject(PyArrayObject *dest, PyObject *src_object) */ ndim = PyArray_DiscoverDTypeAndShape(src_object, PyArray_NDIM(dest), dims, &cache, - NPY_DTYPE(PyArray_DESCR(dest)), PyArray_DESCR(dest), &dtype, 1); + NPY_DTYPE(PyArray_DESCR(dest)), PyArray_DESCR(dest), &dtype, 1, NULL); if (ndim < 0) { return -1; } diff --git a/numpy/_core/src/multiarray/common.c b/numpy/_core/src/multiarray/common.c index c7fcbd42b46a..655122ff7f09 100644 --- a/numpy/_core/src/multiarray/common.c +++ b/numpy/_core/src/multiarray/common.c @@ -119,7 +119,7 @@ PyArray_DTypeFromObject(PyObject *obj, int maxdims, PyArray_Descr **out_dtype) int ndim; ndim = PyArray_DiscoverDTypeAndShape( - obj, maxdims, shape, &cache, NULL, NULL, out_dtype, 1); + obj, maxdims, shape, &cache, NULL, NULL, out_dtype, 1, NULL); if (ndim < 0) { return -1; } diff --git a/numpy/_core/src/multiarray/ctors.c b/numpy/_core/src/multiarray/ctors.c index b5871c8b04f8..1c98ca33e2b6 100644 --- a/numpy/_core/src/multiarray/ctors.c +++ b/numpy/_core/src/multiarray/ctors.c @@ -1429,6 +1429,8 @@ _array_from_buffer_3118(PyObject *memoryview) * @param writeable whether the result must be writeable. * @param context Unused parameter, must be NULL (should be removed later). * @param copy Specifies the copy behavior. + * @param was_copied Set to 1 if it can be assumed that a copy was made + * by implementor. * * @returns The array object, Py_NotImplemented if op is not array-like, * or NULL with an error set. (A new reference to Py_NotImplemented @@ -1437,7 +1439,7 @@ _array_from_buffer_3118(PyObject *memoryview) NPY_NO_EXPORT PyObject * _array_from_array_like(PyObject *op, PyArray_Descr *requested_dtype, npy_bool writeable, PyObject *context, - int copy) { + int copy, int *was_copied) { PyObject* tmp; /* @@ -1485,7 +1487,7 @@ _array_from_array_like(PyObject *op, } if (tmp == Py_NotImplemented) { - tmp = PyArray_FromArrayAttr_int(op, requested_dtype, copy); + tmp = PyArray_FromArrayAttr_int(op, requested_dtype, copy, was_copied); if (tmp == NULL) { return NULL; } @@ -1572,13 +1574,16 @@ PyArray_FromAny_int(PyObject *op, PyArray_Descr *in_descr, // Default is copy = None int copy = -1; + int was_copied = 0; if (flags & NPY_ARRAY_ENSURENOCOPY) { copy = 0; + } else if (flags & NPY_ARRAY_ENSURECOPY) { + copy = 1; } ndim = PyArray_DiscoverDTypeAndShape( - op, NPY_MAXDIMS, dims, &cache, in_DType, in_descr, &dtype, copy); + op, NPY_MAXDIMS, dims, &cache, in_DType, in_descr, &dtype, copy, &was_copied); if (ndim < 0) { return NULL; @@ -1615,6 +1620,10 @@ PyArray_FromAny_int(PyObject *op, PyArray_Descr *in_descr, assert(cache->converted_obj == op); arr = (PyArrayObject *)(cache->arr_or_sequence); /* we may need to cast or assert flags (e.g. copy) */ + if (was_copied == 1 && flags & NPY_ARRAY_ENSURECOPY) { + flags = flags & ~NPY_ARRAY_ENSURECOPY; + flags = flags | NPY_ARRAY_ENSURENOCOPY; + } PyObject *res = PyArray_FromArray(arr, dtype, flags); npy_unlink_coercion_cache(cache); return res; @@ -1937,7 +1946,7 @@ PyArray_FromArray(PyArrayObject *arr, PyArray_Descr *newtype, int flags) } if (copy) { - if (flags & NPY_ARRAY_ENSURENOCOPY ) { + if (flags & NPY_ARRAY_ENSURENOCOPY) { PyErr_SetString(PyExc_ValueError, npy_no_copy_err_msg); Py_DECREF(newtype); return NULL; @@ -2486,12 +2495,14 @@ check_or_clear_and_warn_error_if_due_to_copy_kwarg(PyObject *kwnames) * NOTE: For copy == -1 it passes `op.__array__(copy=None)`, * for copy == 0, `op.__array__(copy=False)`, and * for copy == 1, `op.__array__(copy=True). + * @param was_copied Set to 1 if it can be assumed that a copy was made + * by implementor. * @returns NotImplemented if `__array__` is not defined or a NumPy array * (or subclass). On error, return NULL. */ NPY_NO_EXPORT PyObject * PyArray_FromArrayAttr_int( - PyObject *op, PyArray_Descr *descr, int copy) + PyObject *op, PyArray_Descr *descr, int copy, int *was_copied) { PyObject *new; PyObject *array_meth; @@ -2578,10 +2589,11 @@ PyArray_FromArrayAttr_int( Py_DECREF(new); return NULL; } - if (must_copy_but_copy_kwarg_unimplemented) { - /* TODO: As of NumPy 2.0 this path is only reachable by C-API. */ - Py_SETREF(new, PyArray_NewCopy((PyArrayObject *)new, NPY_KEEPORDER)); + if (was_copied != NULL && copy == 1 && must_copy_but_copy_kwarg_unimplemented == 0) { + /* We can assume that a copy was made */ + *was_copied = 1; } + return new; } @@ -2596,7 +2608,7 @@ PyArray_FromArrayAttr(PyObject *op, PyArray_Descr *typecode, PyObject *context) return NULL; } - return PyArray_FromArrayAttr_int(op, typecode, 0); + return PyArray_FromArrayAttr_int(op, typecode, 0, NULL); } diff --git a/numpy/_core/src/multiarray/ctors.h b/numpy/_core/src/multiarray/ctors.h index fa1cd72e1478..a629a4ee1c44 100644 --- a/numpy/_core/src/multiarray/ctors.h +++ b/numpy/_core/src/multiarray/ctors.h @@ -54,7 +54,7 @@ PyArray_New( NPY_NO_EXPORT PyObject * _array_from_array_like(PyObject *op, PyArray_Descr *requested_dtype, npy_bool writeable, PyObject *context, - int copy); + int copy, int *was_copied); NPY_NO_EXPORT PyObject * PyArray_FromAny_int(PyObject *op, PyArray_Descr *in_descr, @@ -85,7 +85,7 @@ PyArray_FromInterface(PyObject *input); NPY_NO_EXPORT PyObject * PyArray_FromArrayAttr_int( - PyObject *op, PyArray_Descr *descr, int copy); + PyObject *op, PyArray_Descr *descr, int copy, int *was_copied); NPY_NO_EXPORT PyObject * PyArray_FromArrayAttr(PyObject *op, PyArray_Descr *typecode, diff --git a/numpy/_core/tests/test_array_coercion.py b/numpy/_core/tests/test_array_coercion.py index 726e8d8252a8..a88873fb7fc5 100644 --- a/numpy/_core/tests/test_array_coercion.py +++ b/numpy/_core/tests/test_array_coercion.py @@ -54,7 +54,9 @@ def __init__(self, a): self.a = a def __array__(self, dtype=None, copy=None): - return self.a + if dtype is None: + return self.a + return self.a.astype(dtype) yield param(ArrayDunder, id="__array__") diff --git a/numpy/_core/tests/test_multiarray.py b/numpy/_core/tests/test_multiarray.py index e0b8593604a2..896c7dfb23ba 100644 --- a/numpy/_core/tests/test_multiarray.py +++ b/numpy/_core/tests/test_multiarray.py @@ -8452,10 +8452,9 @@ def __array__(self, dtype=None, copy=None): for copy in self.true_vals: res = np.array(arr, copy=copy) assert_array_equal(res, base_arr) - # An additional copy is currently forced by numpy in this case, - # you could argue, numpy does not trust the ArrayLike. This - # may be open for change: - assert res is not base_arr + # An additional copy is no longer forced by NumPy in this case. + # NumPy trusts the ArrayLike made a copy: + assert res is base_arr for copy in self.if_needed_vals + self.false_vals: res = np.array(arr, copy=copy) @@ -8488,9 +8487,11 @@ def __array__(self, dtype=None): assert_array_equal(arr, base_arr) assert arr is base_arr - # As of NumPy 2, explicitly passing copy=True does not trigger passing - # it to __array__ (deprecation warning is not triggered). - arr = np.array(a, copy=True) + # As of NumPy 2.1, explicitly passing copy=True does trigger passing + # it to __array__ (deprecation warning is triggered). + with pytest.warns(DeprecationWarning, + match="__array__.*should implement.*'copy'"): + arr = np.array(a, copy=True) assert_array_equal(arr, base_arr) assert arr is not base_arr @@ -8501,10 +8502,41 @@ def __array__(self, dtype=None): match=r"Unable to avoid copy(.|\n)*numpy_2_0_migration_guide.html"): np.array(a, copy=False) + @pytest.mark.skipif(IS_PYPY, reason="PyPy copies differently") + def test___array__copy_once(self): + size = 100 + base_arr = np.zeros((size, size)) + copy_arr = np.zeros((size, size)) + + class ArrayRandom: + def __init__(self): + self.true_passed = False + + def __array__(self, dtype=None, copy=None): + if copy: + self.true_passed = True + return copy_arr + else: + return base_arr + + arr_random = ArrayRandom() + first_copy = np.array(arr_random, copy=True) + assert arr_random.true_passed + assert first_copy is copy_arr + + arr_random = ArrayRandom() + no_copy = np.array(arr_random, copy=False) + assert not arr_random.true_passed + assert no_copy is base_arr + + arr_random = ArrayRandom() + _ = np.array([arr_random], copy=True) + assert not arr_random.true_passed + @pytest.mark.skipif(not HAS_REFCOUNT, reason="Python lacks refcounts") def test__array__reference_leak(self): class NotAnArray: - def __array__(self): + def __array__(self, dtype=None, copy=None): raise NotImplementedError() x = NotAnArray() diff --git a/numpy/_core/tests/test_protocols.py b/numpy/_core/tests/test_protocols.py index 7cab1223bfe1..1709629fa89b 100644 --- a/numpy/_core/tests/test_protocols.py +++ b/numpy/_core/tests/test_protocols.py @@ -35,8 +35,8 @@ def test_array_called(): class Wrapper: val = '0' * 100 - def __array__(self, result=None, copy=None): - return np.array([self.val], dtype=object) + def __array__(self, dtype=None, copy=None): + return np.array([self.val], dtype=dtype, copy=copy) wrapped = Wrapper() From ea983c6dd2612b6e9a313942a13a03c08233faf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Sok=C3=B3=C5=82?= Date: Wed, 10 Apr 2024 16:58:09 +0200 Subject: [PATCH 2/4] Adjust was_copied variable names --- numpy/_core/src/multiarray/array_coercion.c | 20 +++++++------- numpy/_core/src/multiarray/array_coercion.h | 2 +- numpy/_core/src/multiarray/ctors.c | 29 ++++++++++++--------- numpy/_core/src/multiarray/ctors.h | 6 ++--- numpy/_core/tests/test_multiarray.py | 1 - 5 files changed, 30 insertions(+), 28 deletions(-) diff --git a/numpy/_core/src/multiarray/array_coercion.c b/numpy/_core/src/multiarray/array_coercion.c index 3d4174c0a87f..1b36deca95c2 100644 --- a/numpy/_core/src/multiarray/array_coercion.c +++ b/numpy/_core/src/multiarray/array_coercion.c @@ -99,7 +99,7 @@ enum _dtype_discovery_flags { DISCOVER_TUPLES_AS_ELEMENTS = 1 << 4, MAX_DIMS_WAS_REACHED = 1 << 5, DESCRIPTOR_WAS_SET = 1 << 6, - COPY_WAS_CREATED = 1 << 7, + COPY_WAS_CREATED_BY__ARRAY__ = 1 << 7, }; @@ -1028,9 +1028,9 @@ PyArray_DiscoverDTypeAndShape_Recursive( /* __array__ may be passed the requested descriptor if provided */ requested_descr = *out_descr; } - int was_copied = 0; + int was_copied_by__array__ = 0; arr = (PyArrayObject *)_array_from_array_like(obj, - requested_descr, 0, NULL, copy, &was_copied); + requested_descr, 0, NULL, copy, &was_copied_by__array__); if (arr == NULL) { return -1; } @@ -1038,8 +1038,8 @@ PyArray_DiscoverDTypeAndShape_Recursive( Py_DECREF(arr); arr = NULL; } - if (was_copied == 1) { - *flags |= COPY_WAS_CREATED; + if (was_copied_by__array__ == 1) { + *flags |= COPY_WAS_CREATED_BY__ARRAY__; } } if (arr != NULL) { @@ -1231,8 +1231,8 @@ PyArray_DiscoverDTypeAndShape_Recursive( * to choose a default. * @param copy Specifies the copy behavior. -1 is corresponds to copy=None, * 0 to copy=False, and 1 to copy=True in the Python API. - * @param was_copied Set to 1 if it can be assumed that a copy was made - * by implementor. + * @param was_copied_by__array__ Set to 1 if it can be assumed that a copy was + * made by implementor. * @return dimensions of the discovered object or -1 on error. * WARNING: If (and only if) the output is a single array, the ndim * returned _can_ exceed the maximum allowed number of dimensions. @@ -1245,7 +1245,7 @@ PyArray_DiscoverDTypeAndShape( npy_intp out_shape[NPY_MAXDIMS], coercion_cache_obj **coercion_cache, PyArray_DTypeMeta *fixed_DType, PyArray_Descr *requested_descr, - PyArray_Descr **out_descr, int copy, int *was_copied) + PyArray_Descr **out_descr, int copy, int *was_copied_by__array__) { coercion_cache_obj **coercion_cache_head = coercion_cache; *coercion_cache = NULL; @@ -1298,8 +1298,8 @@ PyArray_DiscoverDTypeAndShape( goto fail; } - if (was_copied != NULL && flags & COPY_WAS_CREATED) { - *was_copied = 1; + if (was_copied_by__array__ != NULL && flags & COPY_WAS_CREATED_BY__ARRAY__) { + *was_copied_by__array__ = 1; } if (NPY_UNLIKELY(flags & FOUND_RAGGED_ARRAY)) { diff --git a/numpy/_core/src/multiarray/array_coercion.h b/numpy/_core/src/multiarray/array_coercion.h index 0fca0702641b..d8f72903a67c 100644 --- a/numpy/_core/src/multiarray/array_coercion.h +++ b/numpy/_core/src/multiarray/array_coercion.h @@ -40,7 +40,7 @@ PyArray_DiscoverDTypeAndShape( npy_intp out_shape[NPY_MAXDIMS], coercion_cache_obj **coercion_cache, PyArray_DTypeMeta *fixed_DType, PyArray_Descr *requested_descr, - PyArray_Descr **out_descr, int copy, int *was_copied); + PyArray_Descr **out_descr, int copy, int *was_copied_by__array__); NPY_NO_EXPORT PyObject * _discover_array_parameters(PyObject *NPY_UNUSED(self), diff --git a/numpy/_core/src/multiarray/ctors.c b/numpy/_core/src/multiarray/ctors.c index 1c98ca33e2b6..163f63f8d2cf 100644 --- a/numpy/_core/src/multiarray/ctors.c +++ b/numpy/_core/src/multiarray/ctors.c @@ -1429,8 +1429,8 @@ _array_from_buffer_3118(PyObject *memoryview) * @param writeable whether the result must be writeable. * @param context Unused parameter, must be NULL (should be removed later). * @param copy Specifies the copy behavior. - * @param was_copied Set to 1 if it can be assumed that a copy was made - * by implementor. + * @param was_copied_by__array__ Set to 1 if it can be assumed that a copy + * was made by implementor. * * @returns The array object, Py_NotImplemented if op is not array-like, * or NULL with an error set. (A new reference to Py_NotImplemented @@ -1439,7 +1439,7 @@ _array_from_buffer_3118(PyObject *memoryview) NPY_NO_EXPORT PyObject * _array_from_array_like(PyObject *op, PyArray_Descr *requested_dtype, npy_bool writeable, PyObject *context, - int copy, int *was_copied) { + int copy, int *was_copied_by__array__) { PyObject* tmp; /* @@ -1487,7 +1487,8 @@ _array_from_array_like(PyObject *op, } if (tmp == Py_NotImplemented) { - tmp = PyArray_FromArrayAttr_int(op, requested_dtype, copy, was_copied); + tmp = PyArray_FromArrayAttr_int( + op, requested_dtype, copy, was_copied_by__array__); if (tmp == NULL) { return NULL; } @@ -1574,7 +1575,7 @@ PyArray_FromAny_int(PyObject *op, PyArray_Descr *in_descr, // Default is copy = None int copy = -1; - int was_copied = 0; + int was_copied_by__array__ = 0; if (flags & NPY_ARRAY_ENSURENOCOPY) { copy = 0; @@ -1583,7 +1584,8 @@ PyArray_FromAny_int(PyObject *op, PyArray_Descr *in_descr, } ndim = PyArray_DiscoverDTypeAndShape( - op, NPY_MAXDIMS, dims, &cache, in_DType, in_descr, &dtype, copy, &was_copied); + op, NPY_MAXDIMS, dims, &cache, in_DType, in_descr, &dtype, + copy, &was_copied_by__array__); if (ndim < 0) { return NULL; @@ -1620,7 +1622,7 @@ PyArray_FromAny_int(PyObject *op, PyArray_Descr *in_descr, assert(cache->converted_obj == op); arr = (PyArrayObject *)(cache->arr_or_sequence); /* we may need to cast or assert flags (e.g. copy) */ - if (was_copied == 1 && flags & NPY_ARRAY_ENSURECOPY) { + if (was_copied_by__array__ == 1 && flags & NPY_ARRAY_ENSURECOPY) { flags = flags & ~NPY_ARRAY_ENSURECOPY; flags = flags | NPY_ARRAY_ENSURENOCOPY; } @@ -2495,14 +2497,14 @@ check_or_clear_and_warn_error_if_due_to_copy_kwarg(PyObject *kwnames) * NOTE: For copy == -1 it passes `op.__array__(copy=None)`, * for copy == 0, `op.__array__(copy=False)`, and * for copy == 1, `op.__array__(copy=True). - * @param was_copied Set to 1 if it can be assumed that a copy was made - * by implementor. + * @param was_copied_by__array__ Set to 1 if it can be assumed that a copy + * was made by implementor. * @returns NotImplemented if `__array__` is not defined or a NumPy array * (or subclass). On error, return NULL. */ NPY_NO_EXPORT PyObject * -PyArray_FromArrayAttr_int( - PyObject *op, PyArray_Descr *descr, int copy, int *was_copied) +PyArray_FromArrayAttr_int(PyObject *op, PyArray_Descr *descr, int copy, + int *was_copied_by__array__) { PyObject *new; PyObject *array_meth; @@ -2589,9 +2591,10 @@ PyArray_FromArrayAttr_int( Py_DECREF(new); return NULL; } - if (was_copied != NULL && copy == 1 && must_copy_but_copy_kwarg_unimplemented == 0) { + if (was_copied_by__array__ != NULL && copy == 1 && + must_copy_but_copy_kwarg_unimplemented == 0) { /* We can assume that a copy was made */ - *was_copied = 1; + *was_copied_by__array__ = 1; } return new; diff --git a/numpy/_core/src/multiarray/ctors.h b/numpy/_core/src/multiarray/ctors.h index a629a4ee1c44..094589968b66 100644 --- a/numpy/_core/src/multiarray/ctors.h +++ b/numpy/_core/src/multiarray/ctors.h @@ -54,7 +54,7 @@ PyArray_New( NPY_NO_EXPORT PyObject * _array_from_array_like(PyObject *op, PyArray_Descr *requested_dtype, npy_bool writeable, PyObject *context, - int copy, int *was_copied); + int copy, int *was_copied_by__array__); NPY_NO_EXPORT PyObject * PyArray_FromAny_int(PyObject *op, PyArray_Descr *in_descr, @@ -84,8 +84,8 @@ NPY_NO_EXPORT PyObject * PyArray_FromInterface(PyObject *input); NPY_NO_EXPORT PyObject * -PyArray_FromArrayAttr_int( - PyObject *op, PyArray_Descr *descr, int copy, int *was_copied); +PyArray_FromArrayAttr_int(PyObject *op, PyArray_Descr *descr, int copy, + int *was_copied_by__array__); NPY_NO_EXPORT PyObject * PyArray_FromArrayAttr(PyObject *op, PyArray_Descr *typecode, diff --git a/numpy/_core/tests/test_multiarray.py b/numpy/_core/tests/test_multiarray.py index 896c7dfb23ba..7252a21ce331 100644 --- a/numpy/_core/tests/test_multiarray.py +++ b/numpy/_core/tests/test_multiarray.py @@ -8502,7 +8502,6 @@ def __array__(self, dtype=None): match=r"Unable to avoid copy(.|\n)*numpy_2_0_migration_guide.html"): np.array(a, copy=False) - @pytest.mark.skipif(IS_PYPY, reason="PyPy copies differently") def test___array__copy_once(self): size = 100 base_arr = np.zeros((size, size)) From 6c7b6f20cd791b9df8000fe92f31fb669db8b1ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Sok=C3=B3=C5=82?= Date: Thu, 11 Apr 2024 12:06:04 +0200 Subject: [PATCH 3/4] Apply review comments --- numpy/_core/src/multiarray/ctors.c | 4 ++-- numpy/_core/tests/test_multiarray.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/numpy/_core/src/multiarray/ctors.c b/numpy/_core/src/multiarray/ctors.c index 163f63f8d2cf..519e9888f613 100644 --- a/numpy/_core/src/multiarray/ctors.c +++ b/numpy/_core/src/multiarray/ctors.c @@ -1622,9 +1622,8 @@ PyArray_FromAny_int(PyObject *op, PyArray_Descr *in_descr, assert(cache->converted_obj == op); arr = (PyArrayObject *)(cache->arr_or_sequence); /* we may need to cast or assert flags (e.g. copy) */ - if (was_copied_by__array__ == 1 && flags & NPY_ARRAY_ENSURECOPY) { + if (was_copied_by__array__ == 1) { flags = flags & ~NPY_ARRAY_ENSURECOPY; - flags = flags | NPY_ARRAY_ENSURENOCOPY; } PyObject *res = PyArray_FromArray(arr, dtype, flags); npy_unlink_coercion_cache(cache); @@ -2591,6 +2590,7 @@ PyArray_FromArrayAttr_int(PyObject *op, PyArray_Descr *descr, int copy, Py_DECREF(new); return NULL; } + /* TODO: Remove was_copied_by__array__ argument */ if (was_copied_by__array__ != NULL && copy == 1 && must_copy_but_copy_kwarg_unimplemented == 0) { /* We can assume that a copy was made */ diff --git a/numpy/_core/tests/test_multiarray.py b/numpy/_core/tests/test_multiarray.py index 7252a21ce331..04084c35af2c 100644 --- a/numpy/_core/tests/test_multiarray.py +++ b/numpy/_core/tests/test_multiarray.py @@ -8532,6 +8532,11 @@ def __array__(self, dtype=None, copy=None): _ = np.array([arr_random], copy=True) assert not arr_random.true_passed + arr_random = ArrayRandom() + second_copy = np.array(arr_random, copy=True, order="F") + assert arr_random.true_passed + assert not second_copy is copy_arr + @pytest.mark.skipif(not HAS_REFCOUNT, reason="Python lacks refcounts") def test__array__reference_leak(self): class NotAnArray: From 3549902ecb6b550c89c840604082bc6b84f64d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Sok=C3=B3=C5=82?= Date: Thu, 11 Apr 2024 12:08:15 +0200 Subject: [PATCH 4/4] linting --- numpy/_core/tests/test_multiarray.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/numpy/_core/tests/test_multiarray.py b/numpy/_core/tests/test_multiarray.py index 04084c35af2c..99cb9453c6ae 100644 --- a/numpy/_core/tests/test_multiarray.py +++ b/numpy/_core/tests/test_multiarray.py @@ -8490,7 +8490,7 @@ def __array__(self, dtype=None): # As of NumPy 2.1, explicitly passing copy=True does trigger passing # it to __array__ (deprecation warning is triggered). with pytest.warns(DeprecationWarning, - match="__array__.*should implement.*'copy'"): + match="__array__.*must implement.*'copy'"): arr = np.array(a, copy=True) assert_array_equal(arr, base_arr) assert arr is not base_arr @@ -8535,7 +8535,7 @@ def __array__(self, dtype=None, copy=None): arr_random = ArrayRandom() second_copy = np.array(arr_random, copy=True, order="F") assert arr_random.true_passed - assert not second_copy is copy_arr + assert second_copy is not copy_arr @pytest.mark.skipif(not HAS_REFCOUNT, reason="Python lacks refcounts") def test__array__reference_leak(self):