From 609e22acceaa94950a88a17aea4ecbdfb3b8e95c Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 5 Mar 2024 21:22:49 +0100 Subject: [PATCH] convert `PyArray` to `Bound` API --- src/array.rs | 1086 +++++++++++++++++++++++++++++++++++++-------- src/array_like.rs | 9 +- src/borrow/mod.rs | 50 ++- tests/array.rs | 4 +- tests/borrow.rs | 11 +- 5 files changed, 933 insertions(+), 227 deletions(-) diff --git a/src/array.rs b/src/array.rs index 11a119371..f63455896 100644 --- a/src/array.rs +++ b/src/array.rs @@ -225,6 +225,7 @@ impl PyArray { /// assert_eq!(array.as_ref(py).readonly().as_slice().unwrap(), [0.0; 5]); /// }); /// ``` + #[deprecated(since = "0.21.0", note = "use Bound::unbind() instead")] pub fn to_owned(&self) -> Py { unsafe { Py::from_borrowed_ptr(self.py(), self.as_ptr()) } } @@ -264,13 +265,11 @@ impl PyArray { if npyffi::PyArray_Check(ob.py(), ob.as_ptr()) == 0 { return Err(DowncastError::new(ob, ::NAME).into()); } - ob.downcast_unchecked() + ob.downcast_unchecked::() }; - let arr_gil_ref: &PyArray = array.as_gil_ref(); - // Check if the dimensionality matches `D`. - let src_ndim = arr_gil_ref.ndim(); + let src_ndim = array.ndim(); if let Some(dst_ndim) = D::NDIM { if src_ndim != dst_ndim { return Err(DimensionalityError::new(src_ndim, dst_ndim).into()); @@ -596,7 +595,7 @@ impl PyArray { /// ``` #[inline(always)] pub unsafe fn get(&self, index: impl NpyIndex) -> Option<&T> { - let ptr = self.get_raw(index)?; + let ptr = get_raw(&self.as_borrowed(), index)?; Some(&*ptr) } @@ -628,19 +627,10 @@ impl PyArray { /// ``` #[inline(always)] pub unsafe fn get_mut(&self, index: impl NpyIndex) -> Option<&mut T> { - let ptr = self.get_raw(index)?; + let ptr = get_raw(&self.as_borrowed(), index)?; Some(&mut *ptr) } - #[inline(always)] - fn get_raw(&self, index: Idx) -> Option<*mut T> - where - Idx: NpyIndex, - { - let offset = index.get_checked::(self.shape(), self.strides())?; - Some(unsafe { self.data().offset(offset) }) - } - /// Get an immutable reference of the specified element, /// without checking the given index. /// @@ -702,8 +692,7 @@ impl PyArray { where Idx: NpyIndex, { - let offset = index.get_unchecked::(self.strides()); - self.data().offset(offset) as *mut _ + self.as_borrowed().uget_raw(index) } /// Get a copy of the specified element in the array. @@ -725,12 +714,12 @@ impl PyArray { where Idx: NpyIndex, { - unsafe { self.get(index) }.cloned() + self.as_borrowed().get_owned(index) } /// Turn an array with fixed dimensionality into one with dynamic dimensionality. pub fn to_dyn(&self) -> &PyArray { - unsafe { PyArray::from_borrowed_ptr(self.py(), self.as_ptr()) } + self.as_borrowed().to_dyn().clone().into_gil_ref() } /// Returns a copy of the internal data of the array as a [`Vec`]. @@ -754,7 +743,7 @@ impl PyArray { /// }); /// ``` pub fn to_vec(&self) -> Result, NotContiguousError> { - unsafe { self.as_slice() }.map(ToOwned::to_owned) + self.as_borrowed().to_vec() } /// Construct a NumPy array from a [`ndarray::ArrayBase`]. @@ -784,7 +773,10 @@ impl PyArray { /// Get an immutable borrow of the NumPy array pub fn try_readonly(&self) -> Result, BorrowError> { - PyReadonlyArray::try_new(self) + // TODO: replace with `Borrowed::to_owned` once + // pyo3#3963 makes it into a release + let bound = &*self.as_borrowed(); + PyReadonlyArray::try_new(bound.clone()) } /// Get an immutable borrow of the NumPy array @@ -800,7 +792,10 @@ impl PyArray { /// Get a mutable borrow of the NumPy array pub fn try_readwrite(&self) -> Result, BorrowError> { - PyReadwriteArray::try_new(self) + // TODO: replace with `Borrowed::to_owned` once + // pyo3#3963 makes it into a release + let bound = &*self.as_borrowed(); + PyReadwriteArray::try_new(bound.clone()) } /// Get a mutable borrow of the NumPy array @@ -817,60 +812,6 @@ impl PyArray { self.try_readwrite().unwrap() } - fn as_view(&self, from_shape_ptr: F) -> ArrayBase - where - F: FnOnce(StrideShape, *mut T) -> ArrayBase, - { - fn inner( - shape: &[usize], - strides: &[isize], - itemsize: usize, - mut data_ptr: *mut u8, - ) -> (StrideShape, u32, *mut u8) { - let shape = D::from_dimension(&Dim(shape)).expect(DIMENSIONALITY_MISMATCH_ERR); - - assert!(strides.len() <= 32, "{}", MAX_DIMENSIONALITY_ERR); - - let mut new_strides = D::zeros(strides.len()); - let mut inverted_axes = 0_u32; - - for i in 0..strides.len() { - // FIXME(kngwyu): Replace this hacky negative strides support with - // a proper constructor, when it's implemented. - // See https://github.com/rust-ndarray/ndarray/issues/842 for more. - if strides[i] >= 0 { - new_strides[i] = strides[i] as usize / itemsize; - } else { - // Move the pointer to the start position. - data_ptr = unsafe { data_ptr.offset(strides[i] * (shape[i] as isize - 1)) }; - - new_strides[i] = (-strides[i]) as usize / itemsize; - inverted_axes |= 1 << i; - } - } - - (shape.strides(new_strides), inverted_axes, data_ptr) - } - - let (shape, mut inverted_axes, data_ptr) = inner( - self.shape(), - self.strides(), - mem::size_of::(), - self.data() as _, - ); - - let mut array = from_shape_ptr(shape, data_ptr as _); - - while inverted_axes != 0 { - let axis = inverted_axes.trailing_zeros() as usize; - inverted_axes &= !(1 << axis); - - array.invert_axis(Axis(axis)); - } - - array - } - /// Returns an [`ArrayView`] of the internal array. /// /// See also [`PyReadonlyArray::as_array`]. @@ -879,7 +820,9 @@ impl PyArray { /// /// Calling this method invalidates all exclusive references to the internal data, e.g. `&mut [T]` or `ArrayViewMut`. pub unsafe fn as_array(&self) -> ArrayView<'_, T, D> { - self.as_view(|shape, ptr| ArrayView::from_shape_ptr(shape, ptr)) + as_view(&self.as_borrowed(), |shape, ptr| { + ArrayView::from_shape_ptr(shape, ptr) + }) } /// Returns an [`ArrayViewMut`] of the internal array. @@ -890,17 +833,19 @@ impl PyArray { /// /// Calling this method invalidates all other references to the internal data, e.g. `ArrayView` or `ArrayViewMut`. pub unsafe fn as_array_mut(&self) -> ArrayViewMut<'_, T, D> { - self.as_view(|shape, ptr| ArrayViewMut::from_shape_ptr(shape, ptr)) + as_view(&self.as_borrowed(), |shape, ptr| { + ArrayViewMut::from_shape_ptr(shape, ptr) + }) } /// Returns the internal array as [`RawArrayView`] enabling element access via raw pointers pub fn as_raw_array(&self) -> RawArrayView { - self.as_view(|shape, ptr| unsafe { RawArrayView::from_shape_ptr(shape, ptr) }) + self.as_borrowed().as_raw_array() } /// Returns the internal array as [`RawArrayViewMut`] enabling element access via raw pointers pub fn as_raw_array_mut(&self) -> RawArrayViewMut { - self.as_view(|shape, ptr| unsafe { RawArrayViewMut::from_shape_ptr(shape, ptr) }) + self.as_borrowed().as_raw_array_mut() } /// Get a copy of the array as an [`ndarray::Array`]. @@ -922,7 +867,7 @@ impl PyArray { /// }); /// ``` pub fn to_owned_array(&self) -> Array { - unsafe { self.as_array() }.to_owned() + self.as_borrowed().to_owned_array() } } @@ -932,61 +877,6 @@ where N: nalgebra::Scalar + Element, D: Dimension, { - fn try_as_matrix_shape_strides( - &self, - ) -> Option<((R, C), (RStride, CStride))> - where - R: nalgebra::Dim, - C: nalgebra::Dim, - RStride: nalgebra::Dim, - CStride: nalgebra::Dim, - { - let ndim = self.ndim(); - let shape = self.shape(); - let strides = self.strides(); - - if ndim != 1 && ndim != 2 { - return None; - } - - if strides.iter().any(|strides| *strides < 0) { - return None; - } - - let rows = shape[0]; - let cols = *shape.get(1).unwrap_or(&1); - - if R::try_to_usize().map(|expected| rows == expected) == Some(false) { - return None; - } - - if C::try_to_usize().map(|expected| cols == expected) == Some(false) { - return None; - } - - let row_stride = strides[0] as usize / mem::size_of::(); - let col_stride = strides - .get(1) - .map_or(rows, |stride| *stride as usize / mem::size_of::()); - - if RStride::try_to_usize().map(|expected| row_stride == expected) == Some(false) { - return None; - } - - if CStride::try_to_usize().map(|expected| col_stride == expected) == Some(false) { - return None; - } - - let shape = (R::from_usize(rows), C::from_usize(cols)); - - let strides = ( - RStride::from_usize(row_stride), - CStride::from_usize(col_stride), - ); - - Some((shape, strides)) - } - /// Try to convert this array into a [`nalgebra::MatrixView`] using the given shape and strides. /// /// # Safety @@ -1002,7 +892,7 @@ where RStride: nalgebra::Dim, CStride: nalgebra::Dim, { - let (shape, strides) = self.try_as_matrix_shape_strides()?; + let (shape, strides) = try_as_matrix_shape_strides(&self.as_borrowed())?; let storage = nalgebra::ViewStorage::from_raw_parts(self.data(), shape, strides); @@ -1024,7 +914,7 @@ where RStride: nalgebra::Dim, CStride: nalgebra::Dim, { - let (shape, strides) = self.try_as_matrix_shape_strides()?; + let (shape, strides) = try_as_matrix_shape_strides(&self.as_borrowed())?; let storage = nalgebra::ViewStorageMut::from_raw_parts(self.data(), shape, strides); @@ -1087,7 +977,7 @@ impl PyArray { /// /// See [`inner`][crate::inner] for an example. pub fn item(&self) -> T { - unsafe { *self.data() } + self.as_borrowed().item() } } @@ -1280,14 +1170,7 @@ impl PyArray { /// /// [PyArray_CopyInto]: https://numpy.org/doc/stable/reference/c-api/array.html#c.PyArray_CopyInto pub fn copy_to(&self, other: &PyArray) -> PyResult<()> { - let self_ptr = self.as_array_ptr(); - let other_ptr = other.as_array_ptr(); - let result = unsafe { PY_ARRAY_API.PyArray_CopyInto(self.py(), other_ptr, self_ptr) }; - if result != -1 { - Ok(()) - } else { - Err(PyErr::fetch(self.py())) - } + self.as_borrowed().copy_to(&other.as_borrowed()) } /// Cast the `PyArray` to `PyArray`, by allocating a new array. @@ -1311,19 +1194,7 @@ impl PyArray { /// /// [PyArray_CastToType]: https://numpy.org/doc/stable/reference/c-api/array.html#c.PyArray_CastToType pub fn cast<'py, U: Element>(&'py self, is_fortran: bool) -> PyResult<&'py PyArray> { - let ptr = unsafe { - PY_ARRAY_API.PyArray_CastToType( - self.py(), - self.as_array_ptr(), - U::get_dtype_bound(self.py()).into_dtype_ptr(), - if is_fortran { -1 } else { 0 }, - ) - }; - if !ptr.is_null() { - Ok(unsafe { PyArray::::from_owned_ptr(self.py(), ptr) }) - } else { - Err(PyErr::fetch(self.py())) - } + self.as_borrowed().cast(is_fortran).map(Bound::into_gil_ref) } /// Construct a new array which has same values as self, @@ -1357,21 +1228,9 @@ impl PyArray { dims: ID, order: NPY_ORDER, ) -> PyResult<&'py PyArray> { - let mut dims = dims.into_dimension(); - let mut dims = dims.to_npy_dims(); - let ptr = unsafe { - PY_ARRAY_API.PyArray_Newshape( - self.py(), - self.as_array_ptr(), - &mut dims as *mut npyffi::PyArray_Dims, - order, - ) - }; - if !ptr.is_null() { - Ok(unsafe { PyArray::::from_owned_ptr(self.py(), ptr) }) - } else { - Err(PyErr::fetch(self.py())) - } + self.as_borrowed() + .reshape_with_order(dims, order) + .map(Bound::into_gil_ref) } /// Special case of [`reshape_with_order`][Self::reshape_with_order] which keeps the memory order the same. @@ -1380,7 +1239,7 @@ impl PyArray { &'py self, dims: ID, ) -> PyResult<&'py PyArray> { - self.reshape_with_order(dims, NPY_ORDER::NPY_ANYORDER) + self.as_borrowed().reshape(dims).map(Bound::into_gil_ref) } /// Extends or truncates the dimensions of an array. @@ -1415,20 +1274,7 @@ impl PyArray { /// [ndarray-resize]: https://numpy.org/doc/stable/reference/generated/numpy.ndarray.resize.html /// [PyArray_Resize]: https://numpy.org/doc/stable/reference/c-api/array.html#c.PyArray_Resize pub unsafe fn resize(&self, dims: ID) -> PyResult<()> { - let mut dims = dims.into_dimension(); - let mut dims = dims.to_npy_dims(); - let res = PY_ARRAY_API.PyArray_Resize( - self.py(), - self.as_array_ptr(), - &mut dims as *mut npyffi::PyArray_Dims, - 1, - NPY_ORDER::NPY_ANYORDER, - ); - if !res.is_null() { - Ok(()) - } else { - Err(PyErr::fetch(self.py())) - } + self.as_borrowed().resize(dims) } } @@ -1485,14 +1331,864 @@ unsafe fn clone_elements(elems: &[T], data_ptr: &mut *mut T) { pub trait PyArrayMethods<'py, T, D>: PyUntypedArrayMethods<'py> { /// Access an untyped representation of this array. fn as_untyped(&self) -> &Bound<'py, PyUntypedArray>; -} -impl<'py, T, D> PyArrayMethods<'py, T, D> for Bound<'py, PyArray> { + /// Returns a pointer to the first element of the array. + fn data(&self) -> *mut T; + + /// Same as [`shape`][PyUntypedArray::shape], but returns `D` instead of `&[usize]`. #[inline(always)] - fn as_untyped(&self) -> &Bound<'py, PyUntypedArray> { - unsafe { self.downcast_unchecked() } + fn dims(&self) -> D + where + D: Dimension, + { + D::from_dimension(&Dim(self.shape())).expect(DIMENSIONALITY_MISMATCH_ERR) } -} + + /// Returns an immutable view of the internal data as a slice. + /// + /// # Safety + /// + /// Calling this method is undefined behaviour if the underlying array + /// is aliased mutably by other instances of `PyArray` + /// or concurrently modified by Python or other native code. + /// + /// Please consider the safe alternative [`PyReadonlyArray::as_slice`]. + unsafe fn as_slice(&self) -> Result<&[T], NotContiguousError> + where + T: Element, + D: Dimension, + { + if self.is_contiguous() { + Ok(slice::from_raw_parts(self.data(), self.len())) + } else { + Err(NotContiguousError) + } + } + + /// Returns a mutable view of the internal data as a slice. + /// + /// # Safety + /// + /// Calling this method is undefined behaviour if the underlying array + /// is aliased immutably or mutably by other instances of [`PyArray`] + /// or concurrently modified by Python or other native code. + /// + /// Please consider the safe alternative [`PyReadwriteArray::as_slice_mut`]. + unsafe fn as_slice_mut(&self) -> Result<&mut [T], NotContiguousError> + where + T: Element, + D: Dimension, + { + if self.is_contiguous() { + Ok(slice::from_raw_parts_mut(self.data(), self.len())) + } else { + Err(NotContiguousError) + } + } + + /// Get a reference of the specified element if the given index is valid. + /// + /// # Safety + /// + /// Calling this method is undefined behaviour if the underlying array + /// is aliased mutably by other instances of `PyArray` + /// or concurrently modified by Python or other native code. + /// + /// Consider using safe alternatives like [`PyReadonlyArray::get`]. + /// + /// # Example + /// + /// ``` + /// use numpy::PyArray; + /// use pyo3::Python; + /// + /// Python::with_gil(|py| { + /// let pyarray = PyArray::arange(py, 0, 16, 1).reshape([2, 2, 4]).unwrap(); + /// + /// assert_eq!(unsafe { *pyarray.get([1, 0, 3]).unwrap() }, 11); + /// }); + /// ``` + unsafe fn get(&self, index: impl NpyIndex) -> Option<&T> + where + T: Element, + D: Dimension; + + /// Same as [`get`][Self::get], but returns `Option<&mut T>`. + /// + /// # Safety + /// + /// Calling this method is undefined behaviour if the underlying array + /// is aliased immutably or mutably by other instances of [`PyArray`] + /// or concurrently modified by Python or other native code. + /// + /// Consider using safe alternatives like [`PyReadwriteArray::get_mut`]. + /// + /// # Example + /// + /// ``` + /// use numpy::PyArray; + /// use pyo3::Python; + /// + /// Python::with_gil(|py| { + /// let pyarray = PyArray::arange(py, 0, 16, 1).reshape([2, 2, 4]).unwrap(); + /// + /// unsafe { + /// *pyarray.get_mut([1, 0, 3]).unwrap() = 42; + /// } + /// + /// assert_eq!(unsafe { *pyarray.get([1, 0, 3]).unwrap() }, 42); + /// }); + /// ``` + unsafe fn get_mut(&self, index: impl NpyIndex) -> Option<&mut T> + where + T: Element, + D: Dimension; + + /// Get an immutable reference of the specified element, + /// without checking the given index. + /// + /// See [`NpyIndex`] for what types can be used as the index. + /// + /// # Safety + /// + /// Passing an invalid index is undefined behavior. + /// The element must also have been initialized and + /// all other references to it is must also be shared. + /// + /// See [`PyReadonlyArray::get`] for a safe alternative. + /// + /// # Example + /// + /// ``` + /// use numpy::PyArray; + /// use pyo3::Python; + /// + /// Python::with_gil(|py| { + /// let pyarray = PyArray::arange(py, 0, 16, 1).reshape([2, 2, 4]).unwrap(); + /// + /// assert_eq!(unsafe { *pyarray.uget([1, 0, 3]) }, 11); + /// }); + /// ``` + #[inline(always)] + unsafe fn uget(&self, index: Idx) -> &T + where + T: Element, + D: Dimension, + Idx: NpyIndex, + { + &*self.uget_raw(index) + } + + /// Same as [`uget`](Self::uget), but returns `&mut T`. + /// + /// # Safety + /// + /// Passing an invalid index is undefined behavior. + /// The element must also have been initialized and + /// other references to it must not exist. + /// + /// See [`PyReadwriteArray::get_mut`] for a safe alternative. + #[inline(always)] + #[allow(clippy::mut_from_ref)] + unsafe fn uget_mut(&self, index: Idx) -> &mut T + where + T: Element, + D: Dimension, + Idx: NpyIndex, + { + &mut *self.uget_raw(index) + } + + /// Same as [`uget`][Self::uget], but returns `*mut T`. + /// + /// # Safety + /// + /// Passing an invalid index is undefined behavior. + #[inline(always)] + unsafe fn uget_raw(&self, index: Idx) -> *mut T + where + T: Element, + D: Dimension, + Idx: NpyIndex, + { + let offset = index.get_unchecked::(self.strides()); + self.data().offset(offset) as *mut _ + } + + /// Get a copy of the specified element in the array. + /// + /// See [`NpyIndex`] for what types can be used as the index. + /// + /// # Example + /// ``` + /// use numpy::PyArray; + /// use pyo3::Python; + /// + /// Python::with_gil(|py| { + /// let pyarray = PyArray::arange(py, 0, 16, 1).reshape([2, 2, 4]).unwrap(); + /// + /// assert_eq!(pyarray.get_owned([1, 0, 3]), Some(11)); + /// }); + /// ``` + fn get_owned(&self, index: Idx) -> Option + where + T: Element, + D: Dimension, + Idx: NpyIndex, + { + unsafe { self.get(index) }.cloned() + } + + /// Turn an array with fixed dimensionality into one with dynamic dimensionality. + fn to_dyn(&self) -> &Bound<'py, PyArray> + where + T: Element, + D: Dimension; + + /// Returns a copy of the internal data of the array as a [`Vec`]. + /// + /// Fails if the internal array is not contiguous. See also [`as_slice`][Self::as_slice]. + /// + /// # Example + /// + /// ``` + /// use numpy::PyArray2; + /// use pyo3::Python; + /// + /// Python::with_gil(|py| { + /// let pyarray= py + /// .eval("__import__('numpy').array([[0, 1], [2, 3]], dtype='int64')", None, None) + /// .unwrap() + /// .downcast::>() + /// .unwrap(); + /// + /// assert_eq!(pyarray.to_vec().unwrap(), vec![0, 1, 2, 3]); + /// }); + /// ``` + fn to_vec(&self) -> Result, NotContiguousError> + where + T: Element, + D: Dimension, + { + unsafe { self.as_slice() }.map(ToOwned::to_owned) + } + + /// Get an immutable borrow of the NumPy array + fn try_readonly(&self) -> Result, BorrowError> + where + T: Element, + D: Dimension; + + /// Get an immutable borrow of the NumPy array + /// + /// # Panics + /// + /// Panics if the allocation backing the array is currently mutably borrowed. + /// + /// For a non-panicking variant, use [`try_readonly`][Self::try_readonly]. + fn readonly(&self) -> PyReadonlyArray<'py, T, D> + where + T: Element, + D: Dimension, + { + self.try_readonly().unwrap() + } + + /// Get a mutable borrow of the NumPy array + fn try_readwrite(&self) -> Result, BorrowError> + where + T: Element, + D: Dimension; + + /// Get a mutable borrow of the NumPy array + /// + /// # Panics + /// + /// Panics if the allocation backing the array is currently borrowed or + /// if the array is [flagged as][flags] not writeable. + /// + /// For a non-panicking variant, use [`try_readwrite`][Self::try_readwrite]. + /// + /// [flags]: https://numpy.org/doc/stable/reference/generated/numpy.ndarray.flags.html + fn readwrite(&self) -> PyReadwriteArray<'py, T, D> + where + T: Element, + D: Dimension, + { + self.try_readwrite().unwrap() + } + + /// Returns an [`ArrayView`] of the internal array. + /// + /// See also [`PyReadonlyArray::as_array`]. + /// + /// # Safety + /// + /// Calling this method invalidates all exclusive references to the internal data, e.g. `&mut [T]` or `ArrayViewMut`. + unsafe fn as_array(&self) -> ArrayView<'_, T, D> + where + T: Element, + D: Dimension; + + /// Returns an [`ArrayViewMut`] of the internal array. + /// + /// See also [`PyReadwriteArray::as_array_mut`]. + /// + /// # Safety + /// + /// Calling this method invalidates all other references to the internal data, e.g. `ArrayView` or `ArrayViewMut`. + unsafe fn as_array_mut(&self) -> ArrayViewMut<'_, T, D> + where + T: Element, + D: Dimension; + + /// Returns the internal array as [`RawArrayView`] enabling element access via raw pointers + fn as_raw_array(&self) -> RawArrayView + where + T: Element, + D: Dimension; + + /// Returns the internal array as [`RawArrayViewMut`] enabling element access via raw pointers + fn as_raw_array_mut(&self) -> RawArrayViewMut + where + T: Element, + D: Dimension; + + /// Get a copy of the array as an [`ndarray::Array`]. + /// + /// # Example + /// + /// ``` + /// use numpy::PyArray; + /// use ndarray::array; + /// use pyo3::Python; + /// + /// Python::with_gil(|py| { + /// let pyarray = PyArray::arange(py, 0, 4, 1).reshape([2, 2]).unwrap(); + /// + /// assert_eq!( + /// pyarray.to_owned_array(), + /// array![[0, 1], [2, 3]] + /// ) + /// }); + /// ``` + fn to_owned_array(&self) -> Array + where + T: Element, + D: Dimension, + { + unsafe { self.as_array() }.to_owned() + } + + /// Copies `self` into `other`, performing a data type conversion if necessary. + /// + /// See also [`PyArray_CopyInto`][PyArray_CopyInto]. + /// + /// # Example + /// + /// ``` + /// use numpy::PyArray; + /// use pyo3::Python; + /// + /// Python::with_gil(|py| { + /// let pyarray_f = PyArray::arange(py, 2.0, 5.0, 1.0); + /// let pyarray_i = unsafe { PyArray::::new(py, [3], false) }; + /// + /// assert!(pyarray_f.copy_to(pyarray_i).is_ok()); + /// + /// assert_eq!(pyarray_i.readonly().as_slice().unwrap(), &[2, 3, 4]); + /// }); + /// ``` + /// + /// [PyArray_CopyInto]: https://numpy.org/doc/stable/reference/c-api/array.html#c.PyArray_CopyInto + fn copy_to(&self, other: &Bound<'py, PyArray>) -> PyResult<()> + where + T: Element; + + /// Cast the `PyArray` to `PyArray`, by allocating a new array. + /// + /// See also [`PyArray_CastToType`][PyArray_CastToType]. + /// + /// # Example + /// + /// ``` + /// use numpy::PyArray; + /// use pyo3::Python; + /// + /// Python::with_gil(|py| { + /// let pyarray_f = PyArray::arange(py, 2.0, 5.0, 1.0); + /// + /// let pyarray_i = pyarray_f.cast::(false).unwrap(); + /// + /// assert_eq!(pyarray_i.readonly().as_slice().unwrap(), &[2, 3, 4]); + /// }); + /// ``` + /// + /// [PyArray_CastToType]: https://numpy.org/doc/stable/reference/c-api/array.html#c.PyArray_CastToType + fn cast(&self, is_fortran: bool) -> PyResult>> + where + T: Element; + + /// Construct a new array which has same values as self, + /// but has different dimensions specified by `dims` + /// and a possibly different memory order specified by `order`. + /// + /// See also [`numpy.reshape`][numpy-reshape] and [`PyArray_Newshape`][PyArray_Newshape]. + /// + /// # Example + /// + /// ``` + /// use numpy::{npyffi::NPY_ORDER, PyArray}; + /// use pyo3::Python; + /// use ndarray::array; + /// + /// Python::with_gil(|py| { + /// let array = + /// PyArray::from_iter(py, 0..9).reshape_with_order([3, 3], NPY_ORDER::NPY_FORTRANORDER).unwrap(); + /// + /// assert_eq!(array.readonly().as_array(), array![[0, 3, 6], [1, 4, 7], [2, 5, 8]]); + /// assert!(array.is_fortran_contiguous()); + /// + /// assert!(array.reshape([5]).is_err()); + /// }); + /// ``` + /// + /// [numpy-reshape]: https://numpy.org/doc/stable/reference/generated/numpy.reshape.html + /// [PyArray_Newshape]: https://numpy.org/doc/stable/reference/c-api/array.html#c.PyArray_Newshape + fn reshape_with_order( + &self, + dims: ID, + order: NPY_ORDER, + ) -> PyResult>> + where + T: Element; + + /// Special case of [`reshape_with_order`][Self::reshape_with_order] which keeps the memory order the same. + #[inline(always)] + fn reshape(&self, dims: ID) -> PyResult>> + where + T: Element, + { + self.reshape_with_order(dims, NPY_ORDER::NPY_ANYORDER) + } + + /// Extends or truncates the dimensions of an array. + /// + /// This method works only on [contiguous][PyUntypedArray::is_contiguous] arrays. + /// Missing elements will be initialized as if calling [`zeros`][Self::zeros]. + /// + /// See also [`ndarray.resize`][ndarray-resize] and [`PyArray_Resize`][PyArray_Resize]. + /// + /// # Safety + /// + /// There should be no outstanding references (shared or exclusive) into the array + /// as this method might re-allocate it and thereby invalidate all pointers into it. + /// + /// # Example + /// + /// ``` + /// use numpy::PyArray; + /// use pyo3::Python; + /// + /// Python::with_gil(|py| { + /// let pyarray = PyArray::::zeros(py, (10, 10), false); + /// assert_eq!(pyarray.shape(), [10, 10]); + /// + /// unsafe { + /// pyarray.resize((100, 100)).unwrap(); + /// } + /// assert_eq!(pyarray.shape(), [100, 100]); + /// }); + /// ``` + /// + /// [ndarray-resize]: https://numpy.org/doc/stable/reference/generated/numpy.ndarray.resize.html + /// [PyArray_Resize]: https://numpy.org/doc/stable/reference/c-api/array.html#c.PyArray_Resize + unsafe fn resize(&self, dims: ID) -> PyResult<()> + where + T: Element; + + /// Try to convert this array into a [`nalgebra::MatrixView`] using the given shape and strides. + /// + /// # Safety + /// + /// Calling this method invalidates all exclusive references to the internal data, e.g. `ArrayViewMut` or `MatrixSliceMut`. + #[doc(alias = "nalgebra")] + #[cfg(feature = "nalgebra")] + unsafe fn try_as_matrix( + &self, + ) -> Option> + where + T: nalgebra::Scalar + Element, + D: Dimension, + R: nalgebra::Dim, + C: nalgebra::Dim, + RStride: nalgebra::Dim, + CStride: nalgebra::Dim; + + /// Try to convert this array into a [`nalgebra::MatrixViewMut`] using the given shape and strides. + /// + /// # Safety + /// + /// Calling this method invalidates all other references to the internal data, e.g. `ArrayView`, `MatrixSlice`, `ArrayViewMut` or `MatrixSliceMut`. + #[doc(alias = "nalgebra")] + #[cfg(feature = "nalgebra")] + unsafe fn try_as_matrix_mut( + &self, + ) -> Option> + where + T: nalgebra::Scalar + Element, + D: Dimension, + R: nalgebra::Dim, + C: nalgebra::Dim, + RStride: nalgebra::Dim, + CStride: nalgebra::Dim; +} + +/// Implementation of functionality for [`PyArray0`]. +#[doc(alias = "PyArray", alias = "PyArray0")] +pub trait PyArray0Methods<'py, T>: PyArrayMethods<'py, T, Ix0> { + /// Get the single element of a zero-dimensional array. + /// + /// See [`inner`][crate::inner] for an example. + fn item(&self) -> T + where + T: Element + Copy, + { + unsafe { *self.data() } + } +} + +#[inline(always)] +fn get_raw(slf: &Bound<'_, PyArray>, index: Idx) -> Option<*mut T> +where + T: Element, + D: Dimension, + Idx: NpyIndex, +{ + let offset = index.get_checked::(slf.shape(), slf.strides())?; + Some(unsafe { slf.data().offset(offset) }) +} + +fn as_view(slf: &Bound<'_, PyArray>, from_shape_ptr: F) -> ArrayBase +where + T: Element, + D: Dimension, + S: RawData, + F: FnOnce(StrideShape, *mut T) -> ArrayBase, +{ + fn inner( + shape: &[usize], + strides: &[isize], + itemsize: usize, + mut data_ptr: *mut u8, + ) -> (StrideShape, u32, *mut u8) { + let shape = D::from_dimension(&Dim(shape)).expect(DIMENSIONALITY_MISMATCH_ERR); + + assert!(strides.len() <= 32, "{}", MAX_DIMENSIONALITY_ERR); + + let mut new_strides = D::zeros(strides.len()); + let mut inverted_axes = 0_u32; + + for i in 0..strides.len() { + // FIXME(kngwyu): Replace this hacky negative strides support with + // a proper constructor, when it's implemented. + // See https://github.com/rust-ndarray/ndarray/issues/842 for more. + if strides[i] >= 0 { + new_strides[i] = strides[i] as usize / itemsize; + } else { + // Move the pointer to the start position. + data_ptr = unsafe { data_ptr.offset(strides[i] * (shape[i] as isize - 1)) }; + + new_strides[i] = (-strides[i]) as usize / itemsize; + inverted_axes |= 1 << i; + } + } + + (shape.strides(new_strides), inverted_axes, data_ptr) + } + + let (shape, mut inverted_axes, data_ptr) = inner( + slf.shape(), + slf.strides(), + mem::size_of::(), + slf.data() as _, + ); + + let mut array = from_shape_ptr(shape, data_ptr as _); + + while inverted_axes != 0 { + let axis = inverted_axes.trailing_zeros() as usize; + inverted_axes &= !(1 << axis); + + array.invert_axis(Axis(axis)); + } + + array +} + +#[cfg(feature = "nalgebra")] +fn try_as_matrix_shape_strides( + slf: &Bound<'_, PyArray>, +) -> Option<((R, C), (RStride, CStride))> +where + N: nalgebra::Scalar + Element, + D: Dimension, + R: nalgebra::Dim, + C: nalgebra::Dim, + RStride: nalgebra::Dim, + CStride: nalgebra::Dim, +{ + let ndim = slf.ndim(); + let shape = slf.shape(); + let strides = slf.strides(); + + if ndim != 1 && ndim != 2 { + return None; + } + + if strides.iter().any(|strides| *strides < 0) { + return None; + } + + let rows = shape[0]; + let cols = *shape.get(1).unwrap_or(&1); + + if R::try_to_usize().map(|expected| rows == expected) == Some(false) { + return None; + } + + if C::try_to_usize().map(|expected| cols == expected) == Some(false) { + return None; + } + + let row_stride = strides[0] as usize / mem::size_of::(); + let col_stride = strides + .get(1) + .map_or(rows, |stride| *stride as usize / mem::size_of::()); + + if RStride::try_to_usize().map(|expected| row_stride == expected) == Some(false) { + return None; + } + + if CStride::try_to_usize().map(|expected| col_stride == expected) == Some(false) { + return None; + } + + let shape = (R::from_usize(rows), C::from_usize(cols)); + + let strides = ( + RStride::from_usize(row_stride), + CStride::from_usize(col_stride), + ); + + Some((shape, strides)) +} + +impl<'py, T, D> PyArrayMethods<'py, T, D> for Bound<'py, PyArray> { + #[inline(always)] + fn as_untyped(&self) -> &Bound<'py, PyUntypedArray> { + unsafe { self.downcast_unchecked() } + } + + #[inline(always)] + fn data(&self) -> *mut T { + unsafe { (*self.as_array_ptr()).data.cast() } + } + + #[inline(always)] + unsafe fn get(&self, index: impl NpyIndex) -> Option<&T> + where + T: Element, + D: Dimension, + { + let ptr = get_raw(self, index)?; + Some(&*ptr) + } + + #[inline(always)] + unsafe fn get_mut(&self, index: impl NpyIndex) -> Option<&mut T> + where + T: Element, + D: Dimension, + { + let ptr = get_raw(self, index)?; + Some(&mut *ptr) + } + + fn to_dyn(&self) -> &Bound<'py, PyArray> { + unsafe { self.downcast_unchecked() } + } + + fn try_readonly(&self) -> Result, BorrowError> + where + T: Element, + D: Dimension, + { + PyReadonlyArray::try_new(self.clone()) + } + + fn try_readwrite(&self) -> Result, BorrowError> + where + T: Element, + D: Dimension, + { + PyReadwriteArray::try_new(self.clone()) + } + + unsafe fn as_array(&self) -> ArrayView<'_, T, D> + where + T: Element, + D: Dimension, + { + as_view(self, |shape, ptr| ArrayView::from_shape_ptr(shape, ptr)) + } + + unsafe fn as_array_mut(&self) -> ArrayViewMut<'_, T, D> + where + T: Element, + D: Dimension, + { + as_view(self, |shape, ptr| ArrayViewMut::from_shape_ptr(shape, ptr)) + } + + fn as_raw_array(&self) -> RawArrayView + where + T: Element, + D: Dimension, + { + as_view(self, |shape, ptr| unsafe { + RawArrayView::from_shape_ptr(shape, ptr) + }) + } + + fn as_raw_array_mut(&self) -> RawArrayViewMut + where + T: Element, + D: Dimension, + { + as_view(self, |shape, ptr| unsafe { + RawArrayViewMut::from_shape_ptr(shape, ptr) + }) + } + + fn copy_to(&self, other: &Bound<'py, PyArray>) -> PyResult<()> + where + T: Element, + { + let self_ptr = self.as_array_ptr(); + let other_ptr = other.as_array_ptr(); + let result = unsafe { PY_ARRAY_API.PyArray_CopyInto(self.py(), other_ptr, self_ptr) }; + if result != -1 { + Ok(()) + } else { + Err(PyErr::fetch(self.py())) + } + } + + fn cast(&self, is_fortran: bool) -> PyResult>> + where + T: Element, + { + let ptr = unsafe { + PY_ARRAY_API.PyArray_CastToType( + self.py(), + self.as_array_ptr(), + U::get_dtype_bound(self.py()).into_dtype_ptr(), + if is_fortran { -1 } else { 0 }, + ) + }; + if !ptr.is_null() { + Ok(unsafe { Bound::from_owned_ptr(self.py(), ptr).downcast_into_unchecked() }) + } else { + Err(PyErr::fetch(self.py())) + } + } + + fn reshape_with_order( + &self, + dims: ID, + order: NPY_ORDER, + ) -> PyResult>> + where + T: Element, + { + let mut dims = dims.into_dimension(); + let mut dims = dims.to_npy_dims(); + let ptr = unsafe { + PY_ARRAY_API.PyArray_Newshape( + self.py(), + self.as_array_ptr(), + &mut dims as *mut npyffi::PyArray_Dims, + order, + ) + }; + if !ptr.is_null() { + Ok(unsafe { Bound::from_owned_ptr(self.py(), ptr).downcast_into_unchecked() }) + } else { + Err(PyErr::fetch(self.py())) + } + } + + unsafe fn resize(&self, dims: ID) -> PyResult<()> + where + T: Element, + { + let mut dims = dims.into_dimension(); + let mut dims = dims.to_npy_dims(); + let res = PY_ARRAY_API.PyArray_Resize( + self.py(), + self.as_array_ptr(), + &mut dims as *mut npyffi::PyArray_Dims, + 1, + NPY_ORDER::NPY_ANYORDER, + ); + if !res.is_null() { + Ok(()) + } else { + Err(PyErr::fetch(self.py())) + } + } + + #[cfg(feature = "nalgebra")] + unsafe fn try_as_matrix( + &self, + ) -> Option> + where + T: nalgebra::Scalar + Element, + D: Dimension, + R: nalgebra::Dim, + C: nalgebra::Dim, + RStride: nalgebra::Dim, + CStride: nalgebra::Dim, + { + let (shape, strides) = try_as_matrix_shape_strides(self)?; + + let storage = nalgebra::ViewStorage::from_raw_parts(self.data(), shape, strides); + + Some(nalgebra::Matrix::from_data(storage)) + } + + #[cfg(feature = "nalgebra")] + unsafe fn try_as_matrix_mut( + &self, + ) -> Option> + where + T: nalgebra::Scalar + Element, + D: Dimension, + R: nalgebra::Dim, + C: nalgebra::Dim, + RStride: nalgebra::Dim, + CStride: nalgebra::Dim, + { + let (shape, strides) = try_as_matrix_shape_strides(self)?; + + let storage = nalgebra::ViewStorageMut::from_raw_parts(self.data(), shape, strides); + + Some(nalgebra::Matrix::from_data(storage)) + } +} + +impl<'py, T> PyArray0Methods<'py, T> for Bound<'py, PyArray0> {} #[cfg(test)] mod tests { diff --git a/src/array_like.rs b/src/array_like.rs index ef070ceb1..86747d13f 100644 --- a/src/array_like.rs +++ b/src/array_like.rs @@ -6,9 +6,10 @@ use pyo3::{ intern, sync::GILOnceCell, types::{PyAnyMethods, PyDict}, - FromPyObject, Py, PyAny, PyResult, + Bound, FromPyObject, Py, PyAny, PyResult, }; +use crate::array::PyArrayMethods; use crate::sealed::Sealed; use crate::{get_array_module, Element, IntoPyArray, PyArray, PyReadonlyArray}; @@ -131,12 +132,12 @@ where impl<'py, T, D, C> FromPyObject<'py> for PyArrayLike<'py, T, D, C> where - T: Element, - D: Dimension, + T: Element + 'py, + D: Dimension + 'py, C: Coerce, Vec: FromPyObject<'py>, { - fn extract(ob: &'py PyAny) -> PyResult { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { if let Ok(array) = ob.downcast::>() { return Ok(Self(array.readonly(), PhantomData)); } diff --git a/src/borrow/mod.rs b/src/borrow/mod.rs index fe2c685d1..26b189936 100644 --- a/src/borrow/mod.rs +++ b/src/borrow/mod.rs @@ -169,12 +169,13 @@ use std::ops::Deref; use ndarray::{ ArrayView, ArrayViewMut, Dimension, IntoDimension, Ix0, Ix1, Ix2, Ix3, Ix4, Ix5, Ix6, IxDyn, }; -use pyo3::{FromPyObject, PyAny, PyResult}; +use pyo3::{types::PyAnyMethods, Bound, FromPyObject, PyAny, PyResult}; -use crate::array::PyArray; +use crate::array::{PyArray, PyArrayMethods}; use crate::convert::NpyIndex; use crate::dtype::Element; use crate::error::{BorrowError, NotContiguousError}; +use crate::untyped_array::PyUntypedArrayMethods; use shared::{acquire, acquire_mut, release, release_mut}; @@ -190,7 +191,7 @@ where T: Element, D: Dimension, { - array: &'py PyArray, + array: Bound<'py, PyArray>, } /// Read-only borrow of a zero-dimensional array. @@ -222,16 +223,16 @@ where T: Element, D: Dimension, { - type Target = PyArray; + type Target = Bound<'py, PyArray>; fn deref(&self) -> &Self::Target { - self.array + &self.array } } impl<'py, T: Element, D: Dimension> FromPyObject<'py> for PyReadonlyArray<'py, T, D> { - fn extract(obj: &'py PyAny) -> PyResult { - let array: &'py PyArray = obj.extract()?; + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + let array = obj.downcast::>()?; Ok(array.readonly()) } } @@ -241,7 +242,7 @@ where T: Element, D: Dimension, { - pub(crate) fn try_new(array: &'py PyArray) -> Result { + pub(crate) fn try_new(array: Bound<'py, PyArray>) -> Result { acquire(array.py(), array.as_array_ptr())?; Ok(Self { array }) @@ -324,7 +325,7 @@ where } } -impl<'a, T, D> Clone for PyReadonlyArray<'a, T, D> +impl<'py, T, D> Clone for PyReadonlyArray<'py, T, D> where T: Element, D: Dimension, @@ -332,11 +333,13 @@ where fn clone(&self) -> Self { acquire(self.array.py(), self.array.as_array_ptr()).unwrap(); - Self { array: self.array } + Self { + array: self.array.clone(), + } } } -impl<'a, T, D> Drop for PyReadonlyArray<'a, T, D> +impl<'py, T, D> Drop for PyReadonlyArray<'py, T, D> where T: Element, D: Dimension, @@ -374,7 +377,7 @@ where T: Element, D: Dimension, { - array: &'py PyArray, + array: Bound<'py, PyArray>, } /// Read-write borrow of a zero-dimensional array. @@ -415,8 +418,8 @@ where } impl<'py, T: Element, D: Dimension> FromPyObject<'py> for PyReadwriteArray<'py, T, D> { - fn extract(obj: &'py PyAny) -> PyResult { - let array: &'py PyArray = obj.extract()?; + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + let array = obj.downcast::>()?; Ok(array.readwrite()) } } @@ -426,7 +429,7 @@ where T: Element, D: Dimension, { - pub(crate) fn try_new(array: &'py PyArray) -> Result { + pub(crate) fn try_new(array: Bound<'py, PyArray>) -> Result { acquire_mut(array.py(), array.as_array_ptr())?; Ok(Self { array }) @@ -520,7 +523,7 @@ where /// # Example /// /// ``` - /// use numpy::PyArray; + /// use numpy::{PyArray, PyUntypedArrayMethods}; /// use pyo3::Python; /// /// Python::with_gil(|py| { @@ -533,20 +536,23 @@ where /// }); /// ``` pub fn resize(self, dims: ID) -> PyResult { - let array = self.array; - // SAFETY: Ownership of `self` proves exclusive access to the interior of the array. unsafe { - array.resize(dims)?; + self.array.resize(dims)?; } - drop(self); + let py = self.array.py(); + let ptr = self.array.as_array_ptr(); + + // Update the borrow metadata to match the shape change. + release_mut(py, ptr); + acquire_mut(py, ptr).unwrap(); - Ok(Self::try_new(array).unwrap()) + Ok(self) } } -impl<'a, T, D> Drop for PyReadwriteArray<'a, T, D> +impl<'py, T, D> Drop for PyReadwriteArray<'py, T, D> where T: Element, D: Dimension, diff --git a/tests/array.rs b/tests/array.rs index 41d7f05db..ac55a3572 100644 --- a/tests/array.rs +++ b/tests/array.rs @@ -5,7 +5,8 @@ use half::{bf16, f16}; use ndarray::{array, s, Array1, Dim}; use numpy::{ dtype_bound, get_array_module, npyffi::NPY_ORDER, pyarray, PyArray, PyArray1, PyArray2, - PyArrayDescr, PyArrayDescrMethods, PyArrayDyn, PyFixedString, PyFixedUnicode, ToPyArray, + PyArrayDescr, PyArrayDescrMethods, PyArrayDyn, PyFixedString, PyFixedUnicode, + PyUntypedArrayMethods, ToPyArray, }; use pyo3::{ py_run, pyclass, pymethods, @@ -471,6 +472,7 @@ fn to_owned_works() { let arr: Py> = Python::with_gil(|py| { let arr = PyArray::from_slice(py, &[1_i32, 2, 3]); + #[allow(deprecated)] arr.to_owned() }); diff --git a/tests/borrow.rs b/tests/borrow.rs index 584940798..576eb9fc1 100644 --- a/tests/borrow.rs +++ b/tests/borrow.rs @@ -1,9 +1,10 @@ use std::thread::spawn; use numpy::{ - npyffi::NPY_ARRAY_WRITEABLE, PyArray, PyArray1, PyArray2, PyReadonlyArray3, PyReadwriteArray3, + array::PyArrayMethods, npyffi::NPY_ARRAY_WRITEABLE, PyArray, PyArray1, PyArray2, + PyReadonlyArray3, PyReadwriteArray3, PyUntypedArrayMethods, }; -use pyo3::{py_run, pyclass, pymethods, types::IntoPyDict, Py, PyAny, Python}; +use pyo3::{py_run, pyclass, pymethods, types::IntoPyDict, Py, PyAny, PyNativeType, Python}; #[test] fn distinct_borrows() { @@ -107,16 +108,16 @@ fn borrows_span_frames() { #[test] fn borrows_span_threads() { Python::with_gil(|py| { - let array = PyArray::::zeros(py, (1, 2, 3), false); + let array = (*PyArray::::zeros(py, (1, 2, 3), false).as_borrowed()).clone(); let _exclusive = array.readwrite(); - let array = array.to_owned(); + let array = array.unbind(); py.allow_threads(move || { let thread = spawn(move || { Python::with_gil(|py| { - let array = array.as_ref(py); + let array = array.bind(py); let _exclusive = array.readwrite(); });