Skip to main content

numpy/borrow/
mod.rs

1//! Types to safely create references into NumPy arrays
2//!
3//! It is assumed that unchecked code - which includes unsafe Rust and Python - is validated by its author
4//! which together with the dynamic borrow checking performed by this crate ensures that
5//! safe Rust code cannot cause undefined behaviour by creating references into NumPy arrays.
6//!
7//! With these borrows established, [references to individual elements][PyReadonlyArray::get] or [reference-based views of whole array][PyReadonlyArray::as_array]
8//! can be created safely. These are then the starting point for algorithms iterating over and operating on the elements of the array.
9//!
10//! # Examples
11//!
12//! The first example shows that dynamic borrow checking works to constrain
13//! both what safe Rust code can invoke and how it is invoked.
14//!
15//! ```rust
16//! # use std::panic::{catch_unwind, AssertUnwindSafe};
17//! #
18//! use numpy::{PyArray1, PyArrayMethods, npyffi::flags};
19//! use ndarray::Zip;
20//! use pyo3::{Python, Bound};
21//!
22//! fn add(x: &Bound<'_, PyArray1<f64>>, y: &Bound<'_, PyArray1<f64>>, z: &Bound<'_, PyArray1<f64>>) {
23//!     let x1 = x.readonly();
24//!     let y1 = y.readonly();
25//!     let mut z1 = z.readwrite();
26//!
27//!     let x2 = x1.as_array();
28//!     let y2 = y1.as_array();
29//!     let z2 = z1.as_array_mut();
30//!
31//!     Zip::from(x2)
32//!         .and(y2)
33//!         .and(z2)
34//!         .for_each(|x3, y3, z3| *z3 = x3 + y3);
35//!
36//!     // Will fail at runtime due to conflict with `x1`.
37//!     let res = catch_unwind(AssertUnwindSafe(|| {
38//!         let _x4 = x.readwrite();
39//!     }));
40//!     assert!(res.is_err());
41//! }
42//!
43//! Python::attach(|py| {
44//!     let x = PyArray1::<f64>::zeros(py, 42, false);
45//!     let y = PyArray1::<f64>::zeros(py, 42, false);
46//!     let z = PyArray1::<f64>::zeros(py, 42, false);
47//!
48//!     // Will work as the three arrays are distinct.
49//!     add(&x, &y, &z);
50//!
51//!     // Will work as `x1` and `y1` are compatible borrows.
52//!     add(&x, &x, &z);
53//!
54//!     // Will fail at runtime due to conflict between `y1` and `z1`.
55//!     let res = catch_unwind(AssertUnwindSafe(|| {
56//!         add(&x, &y, &y);
57//!     }));
58//!     assert!(res.is_err());
59//! });
60//! ```
61//!
62//! The second example shows that non-overlapping and interleaved views are also supported.
63//!
64//! ```rust
65//! use numpy::{PyArray1, PyArrayMethods};
66//! use pyo3::{types::{IntoPyDict, PyAnyMethods}, Python, ffi::c_str};
67//!
68//! # fn main() -> pyo3::PyResult<()> {
69//! Python::attach(|py| {
70//!     let array = PyArray1::arange(py, 0.0, 10.0, 1.0);
71//!     let locals = [("array", array)].into_py_dict(py)?;
72//!
73//!     let view1 = py.eval(c_str!("array[:5]"), None, Some(&locals))?.cast_into::<PyArray1<f64>>()?;
74//!     let view2 = py.eval(c_str!("array[5:]"), None, Some(&locals))?.cast_into::<PyArray1<f64>>()?;
75//!     let view3 = py.eval(c_str!("array[::2]"), None, Some(&locals))?.cast_into::<PyArray1<f64>>()?;
76//!     let view4 = py.eval(c_str!("array[1::2]"), None, Some(&locals))?.cast_into::<PyArray1<f64>>()?;
77//!
78//!     {
79//!         let _view1 = view1.readwrite();
80//!         let _view2 = view2.readwrite();
81//!     }
82//!
83//!     {
84//!         let _view3 = view3.readwrite();
85//!         let _view4 = view4.readwrite();
86//!     }
87//! #   Ok(())
88//! })
89//! # }
90//! ```
91//!
92//! The third example shows that some views are incorrectly rejected since the borrows are over-approximated.
93//!
94//! ```rust
95//! # use std::panic::{catch_unwind, AssertUnwindSafe};
96//! #
97//! use numpy::{PyArray2, PyArrayMethods};
98//! use pyo3::{types::{IntoPyDict, PyAnyMethods}, Python, ffi::c_str};
99//!
100//! # fn main() -> pyo3::PyResult<()> {
101//! Python::attach(|py| {
102//!     let array = PyArray2::<f64>::zeros(py, (10, 10), false);
103//!     let locals = [("array", array)].into_py_dict(py)?;
104//!
105//!     let view1 = py.eval(c_str!("array[:, ::3]"), None, Some(&locals))?.cast_into::<PyArray2<f64>>()?;
106//!     let view2 = py.eval(c_str!("array[:, 1::3]"), None, Some(&locals))?.cast_into::<PyArray2<f64>>()?;
107//!
108//!     // A false conflict as the views do not actually share any elements.
109//!     let res = catch_unwind(AssertUnwindSafe(|| {
110//!         let _view1 = view1.readwrite();
111//!         let _view2 = view2.readwrite();
112//!     }));
113//!     assert!(res.is_err());
114//! #   Ok(())
115//! })
116//! # }
117//! ```
118//!
119//! # Rationale
120//!
121//! Rust references require aliasing discipline to be maintained, i.e. there must always
122//! exist only a single mutable (aka exclusive) reference or multiple immutable (aka shared) references
123//! for each object, otherwise the program contains undefined behaviour.
124//!
125//! The aim of this module is to ensure that safe Rust code is unable to violate these requirements on its own.
126//! We cannot prevent unchecked code - this includes unsafe Rust, Python or other native code like C or Fortran -
127//! from violating them. Therefore the responsibility to avoid this lies with the author of that code instead of the compiler.
128//! However, assuming that the unchecked code is correct, we can ensure that safe Rust is unable to introduce mistakes
129//! into an otherwise correct program by dynamically checking which arrays are currently borrowed and in what manner.
130//!
131//! This means that we follow the [base object chain][base] of each array to the original allocation backing it and
132//! track which parts of that allocation are covered by the array and thereby ensure that only a single read-write array
133//! or multiple read-only arrays overlapping with that region are borrowed at any time.
134//!
135//! In contrast to Rust references, the mere existence of Python references or raw pointers is not an issue
136//! because these values are not assumed to follow aliasing discipline by the Rust compiler.
137//!
138//! This cannot prevent unchecked code from concurrently modifying an array via callbacks or using multiple threads,
139//! but that would lead to incorrect results even if the code that is interfered with is implemented in another language
140//! which does not require aliasing discipline.
141//!
142//! Concerning multi-threading in particular: While the GIL needs to be acquired to create borrows, they are not bound to the GIL
143//! and will stay active after the GIL is released, for example by calling [`detach`][pyo3::Python::detach].
144//! Borrows also do not provide synchronization, i.e. multiple threads borrowing the same array will lead to runtime panics,
145//! it will not block those threads until already active borrows are released.
146//!
147//! In summary, this crate takes the position that all unchecked code - unsafe Rust, Python, C, Fortran, etc. - must be checked for correctness by its author.
148//! Safe Rust code can then rely on this correctness, but should not be able to introduce memory safety issues on its own. Additionally, dynamic borrow checking
149//! can catch _some_ mistakes introduced by unchecked code, e.g. Python calling a function with the same array as an input and as an output argument.
150//!
151//! # Limitations
152//!
153//! Note that the current implementation of this is an over-approximation: It will consider borrows
154//! potentially conflicting if the initial arrays have the same object at the end of their [base object chain][base].
155//! Then, multiple conditions which are sufficient but not necessary to show the absence of conflicts are checked.
156//!
157//! While this is sufficient to handle common situations like slicing an array with a non-unit step size which divides
158//! the dimension along that axis, there are also cases which it does not handle. For example, if the step size does
159//! not divide the dimension along the sliced axis. Under such conditions, borrows are rejected even though the arrays
160//! do not actually share any elements.
161//!
162//! This does limit the set of programs that can be written using safe Rust in way similar to rustc itself
163//! which ensures that all accepted programs are memory safe but does not necessarily accept all memory safe programs.
164//! However, the unsafe method [`PyArrayMethods::as_array_mut`] can be used as an escape hatch.
165//! More involved cases like the example from above may be supported in the future.
166//!
167//! [base]: https://numpy.org/doc/stable/reference/c-api/types-and-structures.html#c.NPY_AO.base
168
169mod shared;
170
171use std::any::type_name;
172use std::fmt;
173use std::ops::Deref;
174
175use ndarray::{
176    ArrayView, ArrayViewMut, Dimension, IntoDimension, Ix0, Ix1, Ix2, Ix3, Ix4, Ix5, Ix6, IxDyn,
177};
178use pyo3::{Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult};
179
180use crate::array::{PyArray, PyArrayMethods};
181use crate::convert::NpyIndex;
182use crate::dtype::Element;
183use crate::error::{AsSliceError, BorrowError};
184use crate::npyffi;
185use crate::npyffi::flags;
186use crate::untyped_array::PyUntypedArrayMethods;
187
188use shared::{acquire, acquire_mut, release, release_mut};
189
190/// Read-only borrow of an array.
191///
192/// An instance of this type ensures that there are no instances of [`PyReadwriteArray`],
193/// i.e. that only shared references into the interior of the array can be created safely.
194///
195/// See the [module-level documentation](self) for more.
196#[repr(transparent)]
197pub struct PyReadonlyArray<'py, T, D>
198where
199    T: Element,
200    D: Dimension,
201{
202    array: Bound<'py, PyArray<T, D>>,
203}
204
205/// Read-only borrow of a zero-dimensional array.
206pub type PyReadonlyArray0<'py, T> = PyReadonlyArray<'py, T, Ix0>;
207
208/// Read-only borrow of a one-dimensional array.
209pub type PyReadonlyArray1<'py, T> = PyReadonlyArray<'py, T, Ix1>;
210
211/// Read-only borrow of a two-dimensional array.
212pub type PyReadonlyArray2<'py, T> = PyReadonlyArray<'py, T, Ix2>;
213
214/// Read-only borrow of a three-dimensional array.
215pub type PyReadonlyArray3<'py, T> = PyReadonlyArray<'py, T, Ix3>;
216
217/// Read-only borrow of a four-dimensional array.
218pub type PyReadonlyArray4<'py, T> = PyReadonlyArray<'py, T, Ix4>;
219
220/// Read-only borrow of a five-dimensional array.
221pub type PyReadonlyArray5<'py, T> = PyReadonlyArray<'py, T, Ix5>;
222
223/// Read-only borrow of a six-dimensional array.
224pub type PyReadonlyArray6<'py, T> = PyReadonlyArray<'py, T, Ix6>;
225
226/// Read-only borrow of an array whose dimensionality is determined at runtime.
227pub type PyReadonlyArrayDyn<'py, T> = PyReadonlyArray<'py, T, IxDyn>;
228
229impl<'py, T, D> Deref for PyReadonlyArray<'py, T, D>
230where
231    T: Element,
232    D: Dimension,
233{
234    type Target = Bound<'py, PyArray<T, D>>;
235
236    fn deref(&self) -> &Self::Target {
237        &self.array
238    }
239}
240
241impl<'a, 'py, T: Element + 'a, D: Dimension + 'a> FromPyObject<'a, 'py>
242    for PyReadonlyArray<'py, T, D>
243{
244    type Error = PyErr;
245
246    fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
247        let array = PyArray::<T, D>::extract::<PyErr>(obj, npyffi::PyArray_Check)?;
248        Ok(array.try_readonly()?)
249    }
250}
251
252impl<'py, T, D> PyReadonlyArray<'py, T, D>
253where
254    T: Element,
255    D: Dimension,
256{
257    pub(crate) fn try_new(array: Bound<'py, PyArray<T, D>>) -> Result<Self, BorrowError> {
258        acquire(array.py(), array.as_array_ptr())?;
259
260        Ok(Self { array })
261    }
262
263    /// Provides an immutable array view of the interior of the NumPy array.
264    #[inline(always)]
265    pub fn as_array(&self) -> ArrayView<'_, T, D> {
266        // SAFETY: Global borrow flags ensure aliasing discipline.
267        unsafe { self.array.as_array() }
268    }
269
270    /// Provide an immutable slice view of the interior of the NumPy array if it is contiguous.
271    #[inline(always)]
272    pub fn as_slice(&self) -> Result<&[T], AsSliceError> {
273        // SAFETY: Global borrow flags ensure aliasing discipline.
274        unsafe { self.array.as_slice() }
275    }
276
277    /// Provide an immutable reference to an element of the NumPy array if the index is within bounds.
278    #[inline(always)]
279    pub fn get<I>(&self, index: I) -> Option<&T>
280    where
281        I: NpyIndex<Dim = D>,
282    {
283        unsafe { self.array.get(index) }
284    }
285}
286
287#[cfg(feature = "nalgebra")]
288impl<'py, N, D> PyReadonlyArray<'py, N, D>
289where
290    N: nalgebra::Scalar + Element,
291    D: Dimension,
292{
293    /// Try to convert this array into a [`nalgebra::MatrixView`] using the given shape and strides.
294    ///
295    /// Note that nalgebra's types default to Fortan/column-major standard strides whereas NumPy creates C/row-major strides by default.
296    /// Furthermore, array views created by slicing into existing arrays will often have non-standard strides.
297    ///
298    /// If you do not fully control the memory layout of a given array, e.g. at your API entry points,
299    /// it can be useful to opt into nalgebra's support for [dynamic strides][nalgebra::Dyn], for example
300    ///
301    /// ```rust
302    /// # use pyo3::prelude::*;
303    /// use pyo3::{py_run, ffi::c_str};
304    /// use numpy::{get_array_module, PyReadonlyArray2};
305    /// use nalgebra::{MatrixView, Const, Dyn};
306    ///
307    /// #[pyfunction]
308    /// fn sum_standard_layout<'py>(py: Python<'py>, array: PyReadonlyArray2<'py, f64>) -> Option<f64> {
309    ///     let matrix: Option<MatrixView<f64, Const<2>, Const<2>>> = array.try_as_matrix();
310    ///     matrix.map(|matrix| matrix.sum())
311    /// }
312    ///
313    /// #[pyfunction]
314    /// fn sum_dynamic_strides<'py>(py: Python<'py>, array: PyReadonlyArray2<'py, f64>) -> Option<f64> {
315    ///     let matrix: Option<MatrixView<f64, Const<2>, Const<2>, Dyn, Dyn>> = array.try_as_matrix();
316    ///     matrix.map(|matrix| matrix.sum())
317    /// }
318    ///
319    /// # fn main() -> pyo3::PyResult<()> {
320    /// Python::attach(|py| {
321    ///     let np = py.eval(c_str!("__import__('numpy')"), None, None)?;
322    ///     let sum_standard_layout = wrap_pyfunction!(sum_standard_layout)(py)?;
323    ///     let sum_dynamic_strides = wrap_pyfunction!(sum_dynamic_strides)(py)?;
324    ///
325    ///     py_run!(py, np sum_standard_layout, r"assert sum_standard_layout(np.ones((2, 2), order='F')) == 4.");
326    ///     py_run!(py, np sum_standard_layout, r"assert sum_standard_layout(np.ones((2, 2, 2))[:,:,0]) is None");
327    ///
328    ///     py_run!(py, np sum_dynamic_strides, r"assert sum_dynamic_strides(np.ones((2, 2), order='F')) == 4.");
329    ///     py_run!(py, np sum_dynamic_strides, r"assert sum_dynamic_strides(np.ones((2, 2, 2))[:,:,0]) == 4.");
330    /// #   Ok(())
331    /// })
332    /// # }
333    /// ```
334    #[doc(alias = "nalgebra")]
335    pub fn try_as_matrix<R, C, RStride, CStride>(
336        &self,
337    ) -> Option<nalgebra::MatrixView<'_, N, R, C, RStride, CStride>>
338    where
339        R: nalgebra::Dim,
340        C: nalgebra::Dim,
341        RStride: nalgebra::Dim,
342        CStride: nalgebra::Dim,
343    {
344        unsafe { self.array.try_as_matrix() }
345    }
346}
347
348#[cfg(feature = "nalgebra")]
349impl<'py, N> PyReadonlyArray<'py, N, Ix1>
350where
351    N: nalgebra::Scalar + Element,
352{
353    /// Convert this one-dimensional array into a [`nalgebra::DMatrixView`] using dynamic strides.
354    ///
355    /// # Panics
356    ///
357    /// Panics if the array has negative strides.
358    #[doc(alias = "nalgebra")]
359    pub fn as_matrix(&self) -> nalgebra::DMatrixView<'_, N, nalgebra::Dyn, nalgebra::Dyn> {
360        self.try_as_matrix().unwrap()
361    }
362}
363
364#[cfg(feature = "nalgebra")]
365impl<'py, N> PyReadonlyArray<'py, N, Ix2>
366where
367    N: nalgebra::Scalar + Element,
368{
369    /// Convert this two-dimensional array into a [`nalgebra::DMatrixView`] using dynamic strides.
370    ///
371    /// # Panics
372    ///
373    /// Panics if the array has negative strides.
374    #[doc(alias = "nalgebra")]
375    pub fn as_matrix(&self) -> nalgebra::DMatrixView<'_, N, nalgebra::Dyn, nalgebra::Dyn> {
376        self.try_as_matrix().unwrap()
377    }
378}
379
380impl<'py, T, D> Clone for PyReadonlyArray<'py, T, D>
381where
382    T: Element,
383    D: Dimension,
384{
385    fn clone(&self) -> Self {
386        acquire(self.array.py(), self.array.as_array_ptr()).unwrap();
387
388        Self {
389            array: self.array.clone(),
390        }
391    }
392}
393
394impl<'py, T, D> Drop for PyReadonlyArray<'py, T, D>
395where
396    T: Element,
397    D: Dimension,
398{
399    fn drop(&mut self) {
400        release(self.array.py(), self.array.as_array_ptr());
401    }
402}
403
404impl<'py, T, D> fmt::Debug for PyReadonlyArray<'py, T, D>
405where
406    T: Element,
407    D: Dimension,
408{
409    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410        let name = format!(
411            "PyReadonlyArray<{}, {}>",
412            type_name::<T>(),
413            type_name::<D>()
414        );
415
416        f.debug_struct(&name).finish()
417    }
418}
419
420/// Read-write borrow of an array.
421///
422/// An instance of this type ensures that there are no instances of [`PyReadonlyArray`] and no other instances of [`PyReadwriteArray`],
423/// i.e. that only a single exclusive reference into the interior of the array can be created safely.
424///
425/// See the [module-level documentation](self) for more.
426#[repr(transparent)]
427pub struct PyReadwriteArray<'py, T, D>
428where
429    T: Element,
430    D: Dimension,
431{
432    array: Bound<'py, PyArray<T, D>>,
433}
434
435/// Read-write borrow of a zero-dimensional array.
436pub type PyReadwriteArray0<'py, T> = PyReadwriteArray<'py, T, Ix0>;
437
438/// Read-write borrow of a one-dimensional array.
439pub type PyReadwriteArray1<'py, T> = PyReadwriteArray<'py, T, Ix1>;
440
441/// Read-write borrow of a two-dimensional array.
442pub type PyReadwriteArray2<'py, T> = PyReadwriteArray<'py, T, Ix2>;
443
444/// Read-write borrow of a three-dimensional array.
445pub type PyReadwriteArray3<'py, T> = PyReadwriteArray<'py, T, Ix3>;
446
447/// Read-write borrow of a four-dimensional array.
448pub type PyReadwriteArray4<'py, T> = PyReadwriteArray<'py, T, Ix4>;
449
450/// Read-write borrow of a five-dimensional array.
451pub type PyReadwriteArray5<'py, T> = PyReadwriteArray<'py, T, Ix5>;
452
453/// Read-write borrow of a six-dimensional array.
454pub type PyReadwriteArray6<'py, T> = PyReadwriteArray<'py, T, Ix6>;
455
456/// Read-write borrow of an array whose dimensionality is determined at runtime.
457pub type PyReadwriteArrayDyn<'py, T> = PyReadwriteArray<'py, T, IxDyn>;
458
459impl<'py, T, D> Deref for PyReadwriteArray<'py, T, D>
460where
461    T: Element,
462    D: Dimension,
463{
464    type Target = PyReadonlyArray<'py, T, D>;
465
466    fn deref(&self) -> &Self::Target {
467        // SAFETY: Exclusive references decay implicitly into shared references.
468        unsafe { &*(self as *const Self as *const Self::Target) }
469    }
470}
471impl<'py, T, D> From<PyReadwriteArray<'py, T, D>> for PyReadonlyArray<'py, T, D>
472where
473    T: Element,
474    D: Dimension,
475{
476    fn from(value: PyReadwriteArray<'py, T, D>) -> Self {
477        let array = value.array.clone();
478        ::std::mem::drop(value);
479        Self::try_new(array)
480            .expect("releasing an exclusive reference should immediately permit a shared reference")
481    }
482}
483
484impl<'a, 'py, T: Element + 'a, D: Dimension + 'a> FromPyObject<'a, 'py>
485    for PyReadwriteArray<'py, T, D>
486{
487    type Error = PyErr;
488
489    fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
490        let array = PyArray::<T, D>::extract::<PyErr>(obj, npyffi::PyArray_Check)?;
491        Ok(array.try_readwrite()?)
492    }
493}
494
495impl<'py, T, D> PyReadwriteArray<'py, T, D>
496where
497    T: Element,
498    D: Dimension,
499{
500    pub(crate) fn try_new(array: Bound<'py, PyArray<T, D>>) -> Result<Self, BorrowError> {
501        acquire_mut(array.py(), array.as_array_ptr())?;
502
503        Ok(Self { array })
504    }
505
506    /// Provides a mutable array view of the interior of the NumPy array.
507    #[inline(always)]
508    pub fn as_array_mut(&mut self) -> ArrayViewMut<'_, T, D> {
509        // SAFETY: Global borrow flags ensure aliasing discipline.
510        unsafe { self.array.as_array_mut() }
511    }
512
513    /// Provide a mutable slice view of the interior of the NumPy array if it is contiguous.
514    #[inline(always)]
515    pub fn as_slice_mut(&mut self) -> Result<&mut [T], AsSliceError> {
516        // SAFETY: Global borrow flags ensure aliasing discipline.
517        unsafe { self.array.as_slice_mut() }
518    }
519
520    /// Provide a mutable reference to an element of the NumPy array if the index is within bounds.
521    #[inline(always)]
522    pub fn get_mut<I>(&mut self, index: I) -> Option<&mut T>
523    where
524        I: NpyIndex<Dim = D>,
525    {
526        unsafe { self.array.get_mut(index) }
527    }
528
529    /// Clear the [`WRITEABLE` flag][writeable] from the underlying NumPy array.
530    ///
531    /// Calling this will prevent any further [PyReadwriteArray]s from being taken out.  Python
532    /// space can reset this flag, unless the additional flag [`OWNDATA`][owndata] is unset.  Such
533    /// an array can be created from Rust space by using [PyArray::borrow_from_array].
534    ///
535    /// [writeable]: https://numpy.org/doc/stable/reference/c-api/array.html#c.NPY_ARRAY_WRITEABLE
536    /// [owndata]: https://numpy.org/doc/stable/reference/c-api/array.html#c.NPY_ARRAY_OWNDATA
537    pub fn make_nonwriteable(self) -> PyReadonlyArray<'py, T, D> {
538        // SAFETY: consuming the only extant mutable reference guarantees we cannot invalidate an
539        // existing reference, nor allow the caller to keep hold of one.
540        unsafe {
541            (*self.as_array_ptr()).flags &= !flags::NPY_ARRAY_WRITEABLE;
542        }
543        self.into()
544    }
545}
546
547#[cfg(feature = "nalgebra")]
548impl<'py, N, D> PyReadwriteArray<'py, N, D>
549where
550    N: nalgebra::Scalar + Element,
551    D: Dimension,
552{
553    /// Try to convert this array into a [`nalgebra::MatrixViewMut`] using the given shape and strides.
554    ///
555    /// See [`PyReadonlyArray::try_as_matrix`] for a discussion of the memory layout requirements.
556    #[doc(alias = "nalgebra")]
557    pub fn try_as_matrix_mut<R, C, RStride, CStride>(
558        &self,
559    ) -> Option<nalgebra::MatrixViewMut<'_, N, R, C, RStride, CStride>>
560    where
561        R: nalgebra::Dim,
562        C: nalgebra::Dim,
563        RStride: nalgebra::Dim,
564        CStride: nalgebra::Dim,
565    {
566        unsafe { self.array.try_as_matrix_mut() }
567    }
568}
569
570#[cfg(feature = "nalgebra")]
571impl<'py, N> PyReadwriteArray<'py, N, Ix1>
572where
573    N: nalgebra::Scalar + Element,
574{
575    /// Convert this one-dimensional array into a [`nalgebra::DMatrixViewMut`] using dynamic strides.
576    ///
577    /// # Panics
578    ///
579    /// Panics if the array has negative strides.
580    #[doc(alias = "nalgebra")]
581    pub fn as_matrix_mut(&self) -> nalgebra::DMatrixViewMut<'_, N, nalgebra::Dyn, nalgebra::Dyn> {
582        self.try_as_matrix_mut().unwrap()
583    }
584}
585
586#[cfg(feature = "nalgebra")]
587impl<'py, N> PyReadwriteArray<'py, N, Ix2>
588where
589    N: nalgebra::Scalar + Element,
590{
591    /// Convert this two-dimensional array into a [`nalgebra::DMatrixViewMut`] using dynamic strides.
592    ///
593    /// # Panics
594    ///
595    /// Panics if the array has negative strides.
596    #[doc(alias = "nalgebra")]
597    pub fn as_matrix_mut(&self) -> nalgebra::DMatrixViewMut<'_, N, nalgebra::Dyn, nalgebra::Dyn> {
598        self.try_as_matrix_mut().unwrap()
599    }
600}
601
602impl<'py, T> PyReadwriteArray<'py, T, Ix1>
603where
604    T: Element,
605{
606    /// Extends or truncates the dimensions of an array.
607    ///
608    /// Safe wrapper for [`PyArrayMethods::resize`].
609    ///
610    /// Note that as this mutates a pointed-to object, the [`PyReadwriteArray`] must be the only
611    /// Python reference to the object.  There cannot be `PyArray` pointers or even `Bound<PyAny>`
612    /// pointing to the same object; this means for example that an object received from a PyO3
613    /// `pyfunction` cannot call this method, since the PyO3 wrapper maintains a reference itself.
614    /// Attempting to call this method when there are other Python references is still safe; NumPy
615    /// will raise a Python-space exception.
616    ///
617    /// # Example
618    ///
619    /// ```
620    /// use numpy::{PyArray, PyArrayMethods, PyUntypedArrayMethods};
621    /// use pyo3::Python;
622    ///
623    /// Python::attach(|py| {
624    ///     let pyarray = PyArray::arange(py, 0, 10, 1);
625    ///     assert_eq!(pyarray.len(), 10);
626    ///
627    ///     let pyarray = pyarray.into_readwrite();
628    ///     let pyarray = pyarray.resize(100).unwrap();
629    ///     assert_eq!(pyarray.len(), 100);
630    /// });
631    /// ```
632    pub fn resize<ID: IntoDimension>(self, dims: ID) -> PyResult<Self> {
633        // SAFETY: Ownership of `self` proves exclusive access to the interior of the array.
634        unsafe {
635            self.array.resize(dims)?;
636        }
637
638        let py = self.array.py();
639        let ptr = self.array.as_array_ptr();
640
641        // Update the borrow metadata to match the shape change.
642        release_mut(py, ptr);
643        acquire_mut(py, ptr).unwrap();
644
645        Ok(self)
646    }
647}
648
649impl<'py, T, D> Drop for PyReadwriteArray<'py, T, D>
650where
651    T: Element,
652    D: Dimension,
653{
654    fn drop(&mut self) {
655        release_mut(self.array.py(), self.array.as_array_ptr());
656    }
657}
658
659impl<'py, T, D> fmt::Debug for PyReadwriteArray<'py, T, D>
660where
661    T: Element,
662    D: Dimension,
663{
664    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
665        let name = format!(
666            "PyReadwriteArray<{}, {}>",
667            type_name::<T>(),
668            type_name::<D>()
669        );
670
671        f.debug_struct(&name).finish()
672    }
673}
674
675#[cfg(test)]
676mod tests {
677    use super::*;
678
679    use pyo3::{
680        types::{IntoPyDict, PyAnyMethods},
681        Python,
682    };
683
684    use crate::array::PyArray1;
685    use pyo3::ffi::c_str;
686
687    #[test]
688    fn test_debug_formatting() {
689        Python::attach(|py| {
690            let array = PyArray::<f64, _>::zeros(py, (1, 2, 3), false);
691
692            {
693                let shared = array.readonly();
694
695                assert_eq!(
696                    format!("{shared:?}"),
697                    "PyReadonlyArray<f64, ndarray::dimension::dim::Dim<[usize; 3]>>"
698                );
699            }
700
701            {
702                let exclusive = array.readwrite();
703
704                assert_eq!(
705                    format!("{exclusive:?}"),
706                    "PyReadwriteArray<f64, ndarray::dimension::dim::Dim<[usize; 3]>>"
707                );
708            }
709        });
710    }
711
712    #[test]
713    #[should_panic(expected = "AlreadyBorrowed")]
714    fn cannot_clone_exclusive_borrow_via_deref() {
715        Python::attach(|py| {
716            let array = PyArray::<f64, _>::zeros(py, (3, 2, 1), false);
717
718            let exclusive = array.readwrite();
719            let _shared = exclusive.clone();
720        });
721    }
722
723    #[test]
724    fn failed_resize_does_not_double_release() {
725        Python::attach(|py| {
726            let array = PyArray::<f64, _>::zeros(py, 10, false);
727
728            // The view will make the internal reference check of `PyArray_Resize` fail.
729            let locals = [("array", &array)].into_py_dict(py).unwrap();
730            let _view = py
731                .eval(c_str!("array[:]"), None, Some(&locals))
732                .unwrap()
733                .cast_into::<PyArray1<f64>>()
734                .unwrap();
735
736            let exclusive = array.into_readwrite();
737            assert!(exclusive.resize(100).is_err());
738        });
739    }
740
741    #[test]
742    fn ineffective_resize_does_not_conflict() {
743        Python::attach(|py| {
744            let array = PyArray::<f64, _>::zeros(py, 10, false);
745
746            let exclusive = array.into_readwrite();
747            assert!(exclusive.resize(10).is_ok());
748        });
749    }
750
751    #[test]
752    fn extraction_reports_dtype_mismatch() {
753        Python::attach(|py| {
754            let array = PyArray::<f64, _>::zeros(py, (2, 2), false);
755            let any = array.as_any();
756
757            let err = any.extract::<PyReadonlyArray2<'_, f32>>().unwrap_err();
758            let msg = err.to_string();
759            assert_eq!(msg, "TypeError: type mismatch:\n from=float64, to=float32");
760
761            let err = any.extract::<PyReadwriteArray2<'_, f32>>().unwrap_err();
762            let msg = err.to_string();
763            assert_eq!(msg, "TypeError: type mismatch:\n from=float64, to=float32");
764        });
765    }
766
767    #[test]
768    fn extraction_reports_dimensionality_mismatch() {
769        Python::attach(|py| {
770            let array = PyArray::<f64, _>::zeros(py, (2, 2), false);
771            let any = array.as_any();
772
773            let err = any.extract::<PyReadonlyArray3<'_, f64>>().unwrap_err();
774            let msg = err.to_string();
775            assert_eq!(msg, "TypeError: dimensionality mismatch:\n from=2, to=3");
776
777            let err = any.extract::<PyReadwriteArray3<'_, f64>>().unwrap_err();
778            let msg = err.to_string();
779            assert_eq!(msg, "TypeError: dimensionality mismatch:\n from=2, to=3");
780        });
781    }
782}