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 iteraing 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 [`PyArray::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, CastError, FromPyObject, PyAny, PyResult};
179
180use crate::array::{PyArray, PyArrayMethods};
181use crate::convert::NpyIndex;
182use crate::dtype::Element;
183use crate::error::{BorrowError, NotContiguousError};
184use crate::npyffi::flags;
185use crate::untyped_array::PyUntypedArrayMethods;
186
187use shared::{acquire, acquire_mut, release, release_mut};
188
189/// Read-only borrow of an array.
190///
191/// An instance of this type ensures that there are no instances of [`PyReadwriteArray`],
192/// i.e. that only shared references into the interior of the array can be created safely.
193///
194/// See the [module-level documentation](self) for more.
195#[repr(transparent)]
196pub struct PyReadonlyArray<'py, T, D>
197where
198 T: Element,
199 D: Dimension,
200{
201 array: Bound<'py, PyArray<T, D>>,
202}
203
204/// Read-only borrow of a zero-dimensional array.
205pub type PyReadonlyArray0<'py, T> = PyReadonlyArray<'py, T, Ix0>;
206
207/// Read-only borrow of a one-dimensional array.
208pub type PyReadonlyArray1<'py, T> = PyReadonlyArray<'py, T, Ix1>;
209
210/// Read-only borrow of a two-dimensional array.
211pub type PyReadonlyArray2<'py, T> = PyReadonlyArray<'py, T, Ix2>;
212
213/// Read-only borrow of a three-dimensional array.
214pub type PyReadonlyArray3<'py, T> = PyReadonlyArray<'py, T, Ix3>;
215
216/// Read-only borrow of a four-dimensional array.
217pub type PyReadonlyArray4<'py, T> = PyReadonlyArray<'py, T, Ix4>;
218
219/// Read-only borrow of a five-dimensional array.
220pub type PyReadonlyArray5<'py, T> = PyReadonlyArray<'py, T, Ix5>;
221
222/// Read-only borrow of a six-dimensional array.
223pub type PyReadonlyArray6<'py, T> = PyReadonlyArray<'py, T, Ix6>;
224
225/// Read-only borrow of an array whose dimensionality is determined at runtime.
226pub type PyReadonlyArrayDyn<'py, T> = PyReadonlyArray<'py, T, IxDyn>;
227
228impl<'py, T, D> Deref for PyReadonlyArray<'py, T, D>
229where
230 T: Element,
231 D: Dimension,
232{
233 type Target = Bound<'py, PyArray<T, D>>;
234
235 fn deref(&self) -> &Self::Target {
236 &self.array
237 }
238}
239
240impl<'a, 'py, T: Element + 'a, D: Dimension + 'a> FromPyObject<'a, 'py>
241 for PyReadonlyArray<'py, T, D>
242{
243 type Error = CastError<'a, 'py>;
244
245 fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
246 let array = obj.cast::<PyArray<T, D>>()?;
247 Ok(array.readonly())
248 }
249}
250
251impl<'py, T, D> PyReadonlyArray<'py, T, D>
252where
253 T: Element,
254 D: Dimension,
255{
256 pub(crate) fn try_new(array: Bound<'py, PyArray<T, D>>) -> Result<Self, BorrowError> {
257 acquire(array.py(), array.as_array_ptr())?;
258
259 Ok(Self { array })
260 }
261
262 /// Provides an immutable array view of the interior of the NumPy array.
263 #[inline(always)]
264 pub fn as_array(&self) -> ArrayView<'_, T, D> {
265 // SAFETY: Global borrow flags ensure aliasing discipline.
266 unsafe { self.array.as_array() }
267 }
268
269 /// Provide an immutable slice view of the interior of the NumPy array if it is contiguous.
270 #[inline(always)]
271 pub fn as_slice(&self) -> Result<&[T], NotContiguousError> {
272 // SAFETY: Global borrow flags ensure aliasing discipline.
273 unsafe { self.array.as_slice() }
274 }
275
276 /// Provide an immutable reference to an element of the NumPy array if the index is within bounds.
277 #[inline(always)]
278 pub fn get<I>(&self, index: I) -> Option<&T>
279 where
280 I: NpyIndex<Dim = D>,
281 {
282 unsafe { self.array.get(index) }
283 }
284}
285
286#[cfg(feature = "nalgebra")]
287impl<'py, N, D> PyReadonlyArray<'py, N, D>
288where
289 N: nalgebra::Scalar + Element,
290 D: Dimension,
291{
292 /// Try to convert this array into a [`nalgebra::MatrixView`] using the given shape and strides.
293 ///
294 /// Note that nalgebra's types default to Fortan/column-major standard strides whereas NumPy creates C/row-major strides by default.
295 /// Furthermore, array views created by slicing into existing arrays will often have non-standard strides.
296 ///
297 /// If you do not fully control the memory layout of a given array, e.g. at your API entry points,
298 /// it can be useful to opt into nalgebra's support for [dynamic strides][nalgebra::Dyn], for example
299 ///
300 /// ```rust
301 /// # use pyo3::prelude::*;
302 /// use pyo3::{py_run, ffi::c_str};
303 /// use numpy::{get_array_module, PyReadonlyArray2};
304 /// use nalgebra::{MatrixView, Const, Dyn};
305 ///
306 /// #[pyfunction]
307 /// fn sum_standard_layout<'py>(py: Python<'py>, array: PyReadonlyArray2<'py, f64>) -> Option<f64> {
308 /// let matrix: Option<MatrixView<f64, Const<2>, Const<2>>> = array.try_as_matrix();
309 /// matrix.map(|matrix| matrix.sum())
310 /// }
311 ///
312 /// #[pyfunction]
313 /// fn sum_dynamic_strides<'py>(py: Python<'py>, array: PyReadonlyArray2<'py, f64>) -> Option<f64> {
314 /// let matrix: Option<MatrixView<f64, Const<2>, Const<2>, Dyn, Dyn>> = array.try_as_matrix();
315 /// matrix.map(|matrix| matrix.sum())
316 /// }
317 ///
318 /// # fn main() -> pyo3::PyResult<()> {
319 /// Python::attach(|py| {
320 /// let np = py.eval(c_str!("__import__('numpy')"), None, None)?;
321 /// let sum_standard_layout = wrap_pyfunction!(sum_standard_layout)(py)?;
322 /// let sum_dynamic_strides = wrap_pyfunction!(sum_dynamic_strides)(py)?;
323 ///
324 /// py_run!(py, np sum_standard_layout, r"assert sum_standard_layout(np.ones((2, 2), order='F')) == 4.");
325 /// py_run!(py, np sum_standard_layout, r"assert sum_standard_layout(np.ones((2, 2, 2))[:,:,0]) is None");
326 ///
327 /// py_run!(py, np sum_dynamic_strides, r"assert sum_dynamic_strides(np.ones((2, 2), order='F')) == 4.");
328 /// py_run!(py, np sum_dynamic_strides, r"assert sum_dynamic_strides(np.ones((2, 2, 2))[:,:,0]) == 4.");
329 /// # Ok(())
330 /// })
331 /// # }
332 /// ```
333 #[doc(alias = "nalgebra")]
334 pub fn try_as_matrix<R, C, RStride, CStride>(
335 &self,
336 ) -> Option<nalgebra::MatrixView<'_, N, R, C, RStride, CStride>>
337 where
338 R: nalgebra::Dim,
339 C: nalgebra::Dim,
340 RStride: nalgebra::Dim,
341 CStride: nalgebra::Dim,
342 {
343 unsafe { self.array.try_as_matrix() }
344 }
345}
346
347#[cfg(feature = "nalgebra")]
348impl<'py, N> PyReadonlyArray<'py, N, Ix1>
349where
350 N: nalgebra::Scalar + Element,
351{
352 /// Convert this one-dimensional array into a [`nalgebra::DMatrixView`] using dynamic strides.
353 ///
354 /// # Panics
355 ///
356 /// Panics if the array has negative strides.
357 #[doc(alias = "nalgebra")]
358 pub fn as_matrix(&self) -> nalgebra::DMatrixView<'_, N, nalgebra::Dyn, nalgebra::Dyn> {
359 self.try_as_matrix().unwrap()
360 }
361}
362
363#[cfg(feature = "nalgebra")]
364impl<'py, N> PyReadonlyArray<'py, N, Ix2>
365where
366 N: nalgebra::Scalar + Element,
367{
368 /// Convert this two-dimensional array into a [`nalgebra::DMatrixView`] using dynamic strides.
369 ///
370 /// # Panics
371 ///
372 /// Panics if the array has negative strides.
373 #[doc(alias = "nalgebra")]
374 pub fn as_matrix(&self) -> nalgebra::DMatrixView<'_, N, nalgebra::Dyn, nalgebra::Dyn> {
375 self.try_as_matrix().unwrap()
376 }
377}
378
379impl<'py, T, D> Clone for PyReadonlyArray<'py, T, D>
380where
381 T: Element,
382 D: Dimension,
383{
384 fn clone(&self) -> Self {
385 acquire(self.array.py(), self.array.as_array_ptr()).unwrap();
386
387 Self {
388 array: self.array.clone(),
389 }
390 }
391}
392
393impl<'py, T, D> Drop for PyReadonlyArray<'py, T, D>
394where
395 T: Element,
396 D: Dimension,
397{
398 fn drop(&mut self) {
399 release(self.array.py(), self.array.as_array_ptr());
400 }
401}
402
403impl<'py, T, D> fmt::Debug for PyReadonlyArray<'py, T, D>
404where
405 T: Element,
406 D: Dimension,
407{
408 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
409 let name = format!(
410 "PyReadonlyArray<{}, {}>",
411 type_name::<T>(),
412 type_name::<D>()
413 );
414
415 f.debug_struct(&name).finish()
416 }
417}
418
419/// Read-write borrow of an array.
420///
421/// An instance of this type ensures that there are no instances of [`PyReadonlyArray`] and no other instances of [`PyReadwriteArray`],
422/// i.e. that only a single exclusive reference into the interior of the array can be created safely.
423///
424/// See the [module-level documentation](self) for more.
425#[repr(transparent)]
426pub struct PyReadwriteArray<'py, T, D>
427where
428 T: Element,
429 D: Dimension,
430{
431 array: Bound<'py, PyArray<T, D>>,
432}
433
434/// Read-write borrow of a zero-dimensional array.
435pub type PyReadwriteArray0<'py, T> = PyReadwriteArray<'py, T, Ix0>;
436
437/// Read-write borrow of a one-dimensional array.
438pub type PyReadwriteArray1<'py, T> = PyReadwriteArray<'py, T, Ix1>;
439
440/// Read-write borrow of a two-dimensional array.
441pub type PyReadwriteArray2<'py, T> = PyReadwriteArray<'py, T, Ix2>;
442
443/// Read-write borrow of a three-dimensional array.
444pub type PyReadwriteArray3<'py, T> = PyReadwriteArray<'py, T, Ix3>;
445
446/// Read-write borrow of a four-dimensional array.
447pub type PyReadwriteArray4<'py, T> = PyReadwriteArray<'py, T, Ix4>;
448
449/// Read-write borrow of a five-dimensional array.
450pub type PyReadwriteArray5<'py, T> = PyReadwriteArray<'py, T, Ix5>;
451
452/// Read-write borrow of a six-dimensional array.
453pub type PyReadwriteArray6<'py, T> = PyReadwriteArray<'py, T, Ix6>;
454
455/// Read-write borrow of an array whose dimensionality is determined at runtime.
456pub type PyReadwriteArrayDyn<'py, T> = PyReadwriteArray<'py, T, IxDyn>;
457
458impl<'py, T, D> Deref for PyReadwriteArray<'py, T, D>
459where
460 T: Element,
461 D: Dimension,
462{
463 type Target = PyReadonlyArray<'py, T, D>;
464
465 fn deref(&self) -> &Self::Target {
466 // SAFETY: Exclusive references decay implictly into shared references.
467 unsafe { &*(self as *const Self as *const Self::Target) }
468 }
469}
470impl<'py, T, D> From<PyReadwriteArray<'py, T, D>> for PyReadonlyArray<'py, T, D>
471where
472 T: Element,
473 D: Dimension,
474{
475 fn from(value: PyReadwriteArray<'py, T, D>) -> Self {
476 let array = value.array.clone();
477 ::std::mem::drop(value);
478 Self::try_new(array)
479 .expect("releasing an exclusive reference should immediately permit a shared reference")
480 }
481}
482
483impl<'a, 'py, T: Element + 'a, D: Dimension + 'a> FromPyObject<'a, 'py>
484 for PyReadwriteArray<'py, T, D>
485{
486 type Error = CastError<'a, 'py>;
487
488 fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
489 let array = obj.cast::<PyArray<T, D>>()?;
490 Ok(array.readwrite())
491 }
492}
493
494impl<'py, T, D> PyReadwriteArray<'py, T, D>
495where
496 T: Element,
497 D: Dimension,
498{
499 pub(crate) fn try_new(array: Bound<'py, PyArray<T, D>>) -> Result<Self, BorrowError> {
500 acquire_mut(array.py(), array.as_array_ptr())?;
501
502 Ok(Self { array })
503 }
504
505 /// Provides a mutable array view of the interior of the NumPy array.
506 #[inline(always)]
507 pub fn as_array_mut(&mut self) -> ArrayViewMut<'_, T, D> {
508 // SAFETY: Global borrow flags ensure aliasing discipline.
509 unsafe { self.array.as_array_mut() }
510 }
511
512 /// Provide a mutable slice view of the interior of the NumPy array if it is contiguous.
513 #[inline(always)]
514 pub fn as_slice_mut(&mut self) -> Result<&mut [T], NotContiguousError> {
515 // SAFETY: Global borrow flags ensure aliasing discipline.
516 unsafe { self.array.as_slice_mut() }
517 }
518
519 /// Provide a mutable reference to an element of the NumPy array if the index is within bounds.
520 #[inline(always)]
521 pub fn get_mut<I>(&mut self, index: I) -> Option<&mut T>
522 where
523 I: NpyIndex<Dim = D>,
524 {
525 unsafe { self.array.get_mut(index) }
526 }
527
528 /// Clear the [`WRITEABLE` flag][writeable] from the underlying NumPy array.
529 ///
530 /// Calling this will prevent any further [PyReadwriteArray]s from being taken out. Python
531 /// space can reset this flag, unless the additional flag [`OWNDATA`][owndata] is unset. Such
532 /// an array can be created from Rust space by using [PyArray::borrow_from_array_bound].
533 ///
534 /// [writeable]: https://numpy.org/doc/stable/reference/c-api/array.html#c.NPY_ARRAY_WRITEABLE
535 /// [owndata]: https://numpy.org/doc/stable/reference/c-api/array.html#c.NPY_ARRAY_OWNDATA
536 pub fn make_nonwriteable(self) -> PyReadonlyArray<'py, T, D> {
537 // SAFETY: consuming the only extant mutable reference guarantees we cannot invalidate an
538 // existing reference, nor allow the caller to keep hold of one.
539 unsafe {
540 (*self.as_array_ptr()).flags &= !flags::NPY_ARRAY_WRITEABLE;
541 }
542 self.into()
543 }
544}
545
546#[cfg(feature = "nalgebra")]
547impl<'py, N, D> PyReadwriteArray<'py, N, D>
548where
549 N: nalgebra::Scalar + Element,
550 D: Dimension,
551{
552 /// Try to convert this array into a [`nalgebra::MatrixViewMut`] using the given shape and strides.
553 ///
554 /// See [`PyReadonlyArray::try_as_matrix`] for a discussion of the memory layout requirements.
555 #[doc(alias = "nalgebra")]
556 pub fn try_as_matrix_mut<R, C, RStride, CStride>(
557 &self,
558 ) -> Option<nalgebra::MatrixViewMut<'_, N, R, C, RStride, CStride>>
559 where
560 R: nalgebra::Dim,
561 C: nalgebra::Dim,
562 RStride: nalgebra::Dim,
563 CStride: nalgebra::Dim,
564 {
565 unsafe { self.array.try_as_matrix_mut() }
566 }
567}
568
569#[cfg(feature = "nalgebra")]
570impl<'py, N> PyReadwriteArray<'py, N, Ix1>
571where
572 N: nalgebra::Scalar + Element,
573{
574 /// Convert this one-dimensional array into a [`nalgebra::DMatrixViewMut`] using dynamic strides.
575 ///
576 /// # Panics
577 ///
578 /// Panics if the array has negative strides.
579 #[doc(alias = "nalgebra")]
580 pub fn as_matrix_mut(&self) -> nalgebra::DMatrixViewMut<'_, N, nalgebra::Dyn, nalgebra::Dyn> {
581 self.try_as_matrix_mut().unwrap()
582 }
583}
584
585#[cfg(feature = "nalgebra")]
586impl<'py, N> PyReadwriteArray<'py, N, Ix2>
587where
588 N: nalgebra::Scalar + Element,
589{
590 /// Convert this two-dimensional array into a [`nalgebra::DMatrixViewMut`] using dynamic strides.
591 ///
592 /// # Panics
593 ///
594 /// Panics if the array has negative strides.
595 #[doc(alias = "nalgebra")]
596 pub fn as_matrix_mut(&self) -> nalgebra::DMatrixViewMut<'_, N, nalgebra::Dyn, nalgebra::Dyn> {
597 self.try_as_matrix_mut().unwrap()
598 }
599}
600
601impl<'py, T> PyReadwriteArray<'py, T, Ix1>
602where
603 T: Element,
604{
605 /// Extends or truncates the dimensions of an array.
606 ///
607 /// Safe wrapper for [`PyArray::resize`].
608 ///
609 /// # Example
610 ///
611 /// ```
612 /// use numpy::{PyArray, PyArrayMethods, PyUntypedArrayMethods};
613 /// use pyo3::Python;
614 ///
615 /// Python::attach(|py| {
616 /// let pyarray = PyArray::arange(py, 0, 10, 1);
617 /// assert_eq!(pyarray.len(), 10);
618 ///
619 /// let pyarray = pyarray.readwrite();
620 /// let pyarray = pyarray.resize(100).unwrap();
621 /// assert_eq!(pyarray.len(), 100);
622 /// });
623 /// ```
624 pub fn resize<ID: IntoDimension>(self, dims: ID) -> PyResult<Self> {
625 // SAFETY: Ownership of `self` proves exclusive access to the interior of the array.
626 unsafe {
627 self.array.resize(dims)?;
628 }
629
630 let py = self.array.py();
631 let ptr = self.array.as_array_ptr();
632
633 // Update the borrow metadata to match the shape change.
634 release_mut(py, ptr);
635 acquire_mut(py, ptr).unwrap();
636
637 Ok(self)
638 }
639}
640
641impl<'py, T, D> Drop for PyReadwriteArray<'py, T, D>
642where
643 T: Element,
644 D: Dimension,
645{
646 fn drop(&mut self) {
647 release_mut(self.array.py(), self.array.as_array_ptr());
648 }
649}
650
651impl<'py, T, D> fmt::Debug for PyReadwriteArray<'py, T, D>
652where
653 T: Element,
654 D: Dimension,
655{
656 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
657 let name = format!(
658 "PyReadwriteArray<{}, {}>",
659 type_name::<T>(),
660 type_name::<D>()
661 );
662
663 f.debug_struct(&name).finish()
664 }
665}
666
667#[cfg(test)]
668mod tests {
669 use super::*;
670
671 use pyo3::{types::IntoPyDict, Python};
672
673 use crate::array::PyArray1;
674 use pyo3::ffi::c_str;
675
676 #[test]
677 fn test_debug_formatting() {
678 Python::attach(|py| {
679 let array = PyArray::<f64, _>::zeros(py, (1, 2, 3), false);
680
681 {
682 let shared = array.readonly();
683
684 assert_eq!(
685 format!("{shared:?}"),
686 "PyReadonlyArray<f64, ndarray::dimension::dim::Dim<[usize; 3]>>"
687 );
688 }
689
690 {
691 let exclusive = array.readwrite();
692
693 assert_eq!(
694 format!("{exclusive:?}"),
695 "PyReadwriteArray<f64, ndarray::dimension::dim::Dim<[usize; 3]>>"
696 );
697 }
698 });
699 }
700
701 #[test]
702 #[should_panic(expected = "AlreadyBorrowed")]
703 fn cannot_clone_exclusive_borrow_via_deref() {
704 Python::attach(|py| {
705 let array = PyArray::<f64, _>::zeros(py, (3, 2, 1), false);
706
707 let exclusive = array.readwrite();
708 let _shared = exclusive.clone();
709 });
710 }
711
712 #[test]
713 fn failed_resize_does_not_double_release() {
714 Python::attach(|py| {
715 let array = PyArray::<f64, _>::zeros(py, 10, false);
716
717 // The view will make the internal reference check of `PyArray_Resize` fail.
718 let locals = [("array", &array)].into_py_dict(py).unwrap();
719 let _view = py
720 .eval(c_str!("array[:]"), None, Some(&locals))
721 .unwrap()
722 .cast_into::<PyArray1<f64>>()
723 .unwrap();
724
725 let exclusive = array.readwrite();
726 assert!(exclusive.resize(100).is_err());
727 });
728 }
729
730 #[test]
731 fn ineffective_resize_does_not_conflict() {
732 Python::attach(|py| {
733 let array = PyArray::<f64, _>::zeros(py, 10, false);
734
735 let exclusive = array.readwrite();
736 assert!(exclusive.resize(10).is_ok());
737 });
738 }
739}