Skip to main content

pyo3/conversions/std/
array.rs

1use crate::conversion::{FromPyObjectOwned, FromPyObjectSequence, IntoPyObject};
2#[cfg(feature = "experimental-inspect")]
3use crate::inspect::{PyStaticExpr, type_hint_identifier, type_hint_subscript};
4use crate::types::PySequence;
5use crate::types::any::PyAnyMethods;
6use crate::{Borrowed, Bound, PyErr, exceptions};
7use crate::{FromPyObject, PyAny, PyResult, PyTypeInfo, Python, err::CastError, ffi};
8
9use core::mem::MaybeUninit;
10
11impl<'py, T, const N: usize> IntoPyObject<'py> for [T; N]
12where
13    T: IntoPyObject<'py>,
14{
15    type Target = PyAny;
16    type Output = Bound<'py, Self::Target>;
17    type Error = PyErr;
18
19    #[cfg(feature = "experimental-inspect")]
20    const OUTPUT_TYPE: PyStaticExpr = T::SEQUENCE_OUTPUT_TYPE;
21
22    /// Turns [`[u8; N]`](core::array) into [`PyBytes`], all other `T`s will be turned into a [`PyList`]
23    ///
24    /// [`PyBytes`]: crate::types::PyBytes
25    /// [`PyList`]: crate::types::PyList
26    #[inline]
27    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
28        T::owned_sequence_into_pyobject(self, py, crate::conversion::private::Token)
29    }
30}
31
32impl<'a, 'py, T, const N: usize> IntoPyObject<'py> for &'a [T; N]
33where
34    &'a T: IntoPyObject<'py>,
35{
36    type Target = PyAny;
37    type Output = Bound<'py, Self::Target>;
38    type Error = PyErr;
39
40    #[cfg(feature = "experimental-inspect")]
41    const OUTPUT_TYPE: PyStaticExpr = <&[T]>::OUTPUT_TYPE;
42
43    #[inline]
44    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
45        self.as_slice().into_pyobject(py)
46    }
47}
48
49impl<'py, T, const N: usize> FromPyObject<'_, 'py> for [T; N]
50where
51    T: FromPyObjectOwned<'py>,
52{
53    type Error = PyErr;
54
55    #[cfg(feature = "experimental-inspect")]
56    const INPUT_TYPE: PyStaticExpr = type_hint_subscript!(
57        type_hint_identifier!("_typeshed", "SupportsLenAndGetItem"),
58        T::INPUT_TYPE
59    );
60
61    fn extract(obj: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
62        if let Some(extractor) = T::sequence_extractor(obj, crate::conversion::private::Token) {
63            return extractor.to_array();
64        }
65
66        create_array_from_obj(obj)
67    }
68}
69
70fn create_array_from_obj<'py, T, const N: usize>(obj: Borrowed<'_, 'py, PyAny>) -> PyResult<[T; N]>
71where
72    T: FromPyObjectOwned<'py>,
73{
74    // Types that pass `PySequence_Check` usually implement enough of the sequence protocol
75    // to support this function and if not, we will only fail extraction safely.
76    // SAFETY: passing valid pointer to python API
77    if unsafe { ffi::PySequence_Check(obj.as_ptr()) } == 0 {
78        return Err(CastError::new(obj, PySequence::type_object(obj.py()).into_any()).into());
79    }
80
81    let seq_len = obj.len()?;
82    if seq_len != N {
83        return Err(invalid_sequence_length(N, seq_len));
84    }
85    array_try_from_fn(|idx| {
86        obj.get_item(idx)
87            .and_then(|any| any.extract().map_err(Into::into))
88    })
89}
90
91// TODO: use `core::array::try_from_fn`, if that stabilises:
92// https://github.com/rust-lang/rust/issues/89379
93fn array_try_from_fn<E, F, T, const N: usize>(mut cb: F) -> Result<[T; N], E>
94where
95    F: FnMut(usize) -> Result<T, E>,
96{
97    let mut array: [MaybeUninit<T>; N] = [const { MaybeUninit::uninit() }; N];
98
99    array_try_from_fn_erased(&mut array, &mut cb)?;
100
101    // TODO: use `MaybeUninit::array_assume_init` if that stabilises in future?
102    //
103    // SAFETY: the loop above has fully initialized all `N` elements of `array`,
104    // since we only exit the loop once `guard.initialized == N`.
105    Ok(array.map(|elem| unsafe { elem.assume_init() }))
106}
107
108fn array_try_from_fn_erased<E, T>(
109    buffer: &mut [MaybeUninit<T>],
110    cb: &mut impl FnMut(usize) -> Result<T, E>,
111) -> Result<(), E> {
112    // Panic guard for incremental initialization of arrays.
113    //
114    // Disarm the guard with `mem::forget` once the array has been initialized.
115    struct Guard<'a, T> {
116        // The array to be initialized.
117        array_mut: &'a mut [MaybeUninit<T>],
118        // The number of items that have been initialized so far.
119        initialized: usize,
120    }
121
122    impl<T> Guard<'_, T> {
123        // Adds an item to the array and updates the initialized item counter.
124        //
125        // # Safety
126        //
127        // No more than N elements must be initialized.
128        #[inline]
129        unsafe fn push_unchecked(&mut self, item: T) {
130            // SAFETY: If `initialized` was correct before and the caller does not
131            // invoke this method more than N times, then writes will be in-bounds
132            // and slots will not be initialized more than once.
133            unsafe {
134                self.array_mut
135                    .get_unchecked_mut(self.initialized)
136                    .write(item);
137
138                self.initialized = self.initialized.unchecked_add(1)
139            }
140        }
141    }
142
143    impl<T> Drop for Guard<'_, T> {
144        #[inline]
145        fn drop(&mut self) {
146            debug_assert!(self.initialized <= self.array_mut.len());
147            // SAFETY: this slice will contain only initialized objects.
148            unsafe {
149                core::ptr::drop_in_place(
150                    &mut self.array_mut[..self.initialized] as *mut [_] as *mut [T],
151                );
152            }
153        }
154    }
155
156    let mut guard = Guard {
157        array_mut: buffer,
158        initialized: 0,
159    };
160
161    while guard.initialized < guard.array_mut.len() {
162        let item = cb(guard.initialized)?;
163        // SAFETY: loop condition ensures there's space to push the item.
164        unsafe { guard.push_unchecked(item) };
165    }
166
167    core::mem::forget(guard);
168
169    Ok(())
170}
171
172pub(crate) fn invalid_sequence_length(expected: usize, actual: usize) -> PyErr {
173    exceptions::PyValueError::new_err(format!(
174        "expected a sequence of length {expected} (got {actual})"
175    ))
176}
177
178#[cfg(test)]
179mod tests {
180    use crate::platform::prelude::*;
181    #[cfg(panic = "unwind")]
182    use core::any::Any;
183    #[cfg(panic = "unwind")]
184    use core::sync::atomic::{AtomicUsize, Ordering};
185
186    // allow use of panic mod without leaking anything else from std
187    #[cfg(panic = "unwind")]
188    mod panic {
189        extern crate std;
190        pub use std::panic::*;
191    }
192
193    use crate::{PyResult, Python, types::PyList};
194    use crate::{
195        conversion::IntoPyObject,
196        types::{PyBytes, PyBytesMethods, any::PyAnyMethods},
197    };
198
199    #[test]
200    #[cfg(panic = "unwind")]
201    fn array_try_from_fn() {
202        static DROP_COUNTER: AtomicUsize = AtomicUsize::new(0);
203        struct CountDrop;
204        impl Drop for CountDrop {
205            fn drop(&mut self) {
206                DROP_COUNTER.fetch_add(1, Ordering::SeqCst);
207            }
208        }
209        let _ = catch_unwind_silent(move || {
210            let _: Result<[CountDrop; 4], ()> = super::array_try_from_fn(|idx| {
211                #[expect(clippy::manual_assert, reason = "testing panic during array creation")]
212                if idx == 2 {
213                    panic!("peek a boo");
214                }
215                Ok(CountDrop)
216            });
217        });
218        assert_eq!(DROP_COUNTER.load(Ordering::SeqCst), 2);
219    }
220
221    #[test]
222    fn test_extract_bytes_to_array() {
223        Python::attach(|py| {
224            let v: [u8; 33] = py
225                .eval(c"b'abcabcabcabcabcabcabcabcabcabcabc'", None, None)
226                .unwrap()
227                .extract()
228                .unwrap();
229            assert_eq!(&v, b"abcabcabcabcabcabcabcabcabcabcabc");
230        })
231    }
232
233    #[test]
234    fn test_extract_bytes_wrong_length() {
235        Python::attach(|py| {
236            let v: PyResult<[u8; 3]> = py.eval(c"b'abcdefg'", None, None).unwrap().extract();
237            assert_eq!(
238                v.unwrap_err().to_string(),
239                "ValueError: expected a sequence of length 3 (got 7)"
240            );
241        })
242    }
243
244    #[test]
245    fn test_extract_bytearray_to_array() {
246        Python::attach(|py| {
247            let v: [u8; 33] = py
248                .eval(
249                    c"bytearray(b'abcabcabcabcabcabcabcabcabcabcabc')",
250                    None,
251                    None,
252                )
253                .unwrap()
254                .extract()
255                .unwrap();
256            assert_eq!(&v, b"abcabcabcabcabcabcabcabcabcabcabc");
257        })
258    }
259
260    #[test]
261    fn test_extract_small_bytearray_to_array() {
262        Python::attach(|py| {
263            let v: [u8; 3] = py
264                .eval(c"bytearray(b'abc')", None, None)
265                .unwrap()
266                .extract()
267                .unwrap();
268            assert_eq!(&v, b"abc");
269        });
270    }
271    #[test]
272    fn test_into_pyobject_array_conversion() {
273        Python::attach(|py| {
274            let array: [f32; 4] = [0.0, -16.0, 16.0, 42.0];
275            let pyobject = array.into_pyobject(py).unwrap();
276            let pylist = pyobject.cast::<PyList>().unwrap();
277            assert_eq!(pylist.get_item(0).unwrap().extract::<f32>().unwrap(), 0.0);
278            assert_eq!(pylist.get_item(1).unwrap().extract::<f32>().unwrap(), -16.0);
279            assert_eq!(pylist.get_item(2).unwrap().extract::<f32>().unwrap(), 16.0);
280            assert_eq!(pylist.get_item(3).unwrap().extract::<f32>().unwrap(), 42.0);
281        });
282    }
283
284    #[test]
285    fn test_extract_invalid_sequence_length() {
286        Python::attach(|py| {
287            let v: PyResult<[u8; 3]> = py
288                .eval(c"bytearray(b'abcdefg')", None, None)
289                .unwrap()
290                .extract();
291            assert_eq!(
292                v.unwrap_err().to_string(),
293                "ValueError: expected a sequence of length 3 (got 7)"
294            );
295        })
296    }
297
298    #[test]
299    fn test_intopyobject_array_conversion() {
300        Python::attach(|py| {
301            let array: [f32; 4] = [0.0, -16.0, 16.0, 42.0];
302            let pylist = array
303                .into_pyobject(py)
304                .unwrap()
305                .cast_into::<PyList>()
306                .unwrap();
307
308            assert_eq!(pylist.get_item(0).unwrap().extract::<f32>().unwrap(), 0.0);
309            assert_eq!(pylist.get_item(1).unwrap().extract::<f32>().unwrap(), -16.0);
310            assert_eq!(pylist.get_item(2).unwrap().extract::<f32>().unwrap(), 16.0);
311            assert_eq!(pylist.get_item(3).unwrap().extract::<f32>().unwrap(), 42.0);
312        });
313    }
314
315    #[test]
316    fn test_array_intopyobject_impl() {
317        Python::attach(|py| {
318            let bytes: [u8; 6] = *b"foobar";
319            let obj = bytes.into_pyobject(py).unwrap();
320            assert!(obj.is_instance_of::<PyBytes>());
321            let obj = obj.cast_into::<PyBytes>().unwrap();
322            assert_eq!(obj.as_bytes(), &bytes);
323
324            let nums: [u16; 4] = [0, 1, 2, 3];
325            let obj = nums.into_pyobject(py).unwrap();
326            assert!(obj.is_instance_of::<PyList>());
327        });
328    }
329
330    #[test]
331    fn test_extract_non_iterable_to_array() {
332        Python::attach(|py| {
333            let v = py.eval(c"42", None, None).unwrap();
334            v.extract::<i32>().unwrap();
335            v.extract::<[i32; 1]>().unwrap_err();
336        });
337    }
338
339    #[cfg(feature = "macros")]
340    #[test]
341    fn test_pyclass_intopy_array_conversion() {
342        #[crate::pyclass(crate = "crate")]
343        struct Foo;
344
345        Python::attach(|py| {
346            let array: [Foo; 8] = [Foo, Foo, Foo, Foo, Foo, Foo, Foo, Foo];
347            let list = array
348                .into_pyobject(py)
349                .unwrap()
350                .cast_into::<PyList>()
351                .unwrap();
352            let _bound = list.get_item(4).unwrap().cast::<Foo>().unwrap();
353        });
354    }
355
356    // https://stackoverflow.com/a/59211505
357    #[cfg(panic = "unwind")]
358    fn catch_unwind_silent<F, R>(f: F) -> Result<R, Box<dyn Any + Send + 'static>>
359    where
360        F: FnOnce() -> R + panic::UnwindSafe,
361    {
362        let prev_hook = panic::take_hook();
363        panic::set_hook(Box::new(|_| {}));
364        let result = panic::catch_unwind(f);
365        panic::set_hook(prev_hook);
366        result
367    }
368}