Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ pub use crate::pyclass::{PyClass, PyClassGuard, PyClassGuardMut};
pub use crate::pyclass_init::PyClassInitializer;
pub use crate::type_object::{PyTypeCheck, PyTypeInfo};
pub use crate::types::PyAny;
pub use crate::unpack::Unpackable;
pub use crate::version::PythonVersionInfo;

pub(crate) mod ffi_ptr_ext;
Expand Down Expand Up @@ -424,6 +425,7 @@ mod instance;
mod interpreter_lifecycle;
pub mod marker;
pub mod marshal;
mod unpack;
#[macro_use]
pub mod sync;
pub(crate) mod byteswriter;
Expand Down
66 changes: 66 additions & 0 deletions src/unpack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use crate::{
exceptions::PyValueError,
types::{PyAnyMethods, PyIterator},
Borrowed, Bound, FromPyObject, PyAny, PyResult,
};

/// TODO
pub trait Unpackable<'py>: Sized {
/// TODO
fn unpack(obj: Borrowed<'_, 'py, PyAny>) -> PyResult<Self>;
}

fn get_value<'py, T>(iter: &mut Bound<'py, PyIterator>, expected: usize) -> PyResult<T>
where
T: for<'a> FromPyObject<'a, 'py>,
{
let Some(item) = iter.next() else {
return Err(PyValueError::new_err(format!(
"not enough values to unpack (expected {expected})",
)));
};
match item?.extract::<T>() {
Ok(v) => Ok(v),
Err(e) => return Err(e.into()),
}
}

fn one<T>() -> usize {
1
}

macro_rules! tuple_impls {
($T:ident $num:literal) => {
tuple_impls!(@impl $T $num);
};
($T:ident $num:literal $( $U:ident $unum:literal )+) => {
tuple_impls!($( $U $unum )+);
tuple_impls!(@impl $T $num $( $U $unum )+);
};
(@impl $( $T:ident $num:literal )+) => {
impl<'py, $($T,)+> Unpackable<'py> for ($($T,)+)
where
$($T: for<'a> FromPyObject<'a, 'py>),+
{
fn unpack(obj: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
let total = $(one::<$T>() +)+ 0;
let mut iter = obj.try_iter()?;
let out = ($(
get_value::<$T>(&mut iter, total)?,
)+);

if iter.next().is_some() {
return Err(PyValueError::new_err(format!(
"too many values to unpack (expected {total})"
)));
}

Ok(out)
}
}
};
}

tuple_impls! {
T11 11 T10 10 T9 9 T8 8 T7 7 T6 6 T5 5 T4 4 T3 3 T2 2 T1 1 T0 0
}
48 changes: 48 additions & 0 deletions tests/test_unpack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#![cfg(feature = "macros")]

use pyo3::prelude::*;
use pyo3::Unpackable;

mod test_utils;

#[test]
fn test_unpack_3() {
Python::attach(|py| {
let tuple = (0, 1, 2);
let py_tuple = tuple.into_pyobject(py).unwrap();
let unpacked: (i32, i32, i32) =
Unpackable::unpack(py_tuple.as_any().as_borrowed()).unwrap();

assert_eq!(tuple, unpacked);
});
}

#[test]
fn test_unpack_not_enough() {
Python::attach(|py| {
let tuple = (0, 1);
let py_tuple = tuple.into_pyobject(py).unwrap();
let try_unpack =
<(i32, i32, i32) as Unpackable>::unpack(py_tuple.as_any().as_borrowed()).unwrap_err();

assert_eq!(
try_unpack.value(py).to_string(),
"not enough values to unpack (expected 3)"
);
});
}

#[test]
fn test_unpack_too_many() {
Python::attach(|py| {
let tuple = (0, 1, 2);
let py_tuple = tuple.into_pyobject(py).unwrap();
let try_unpack =
<(i32, i32) as Unpackable>::unpack(py_tuple.as_any().as_borrowed()).unwrap_err();

assert_eq!(
try_unpack.value(py).to_string(),
"too many values to unpack (expected 2)"
);
});
}
Loading