Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for SmallVec in conversion traits (#3440) #3507

Merged
merged 3 commits into from Oct 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Expand Up @@ -41,6 +41,7 @@ num-bigint = { version = "0.4", optional = true }
num-complex = { version = ">= 0.2, < 0.5", optional = true }
rust_decimal = { version = "1.0.0", default-features = false, optional = true }
serde = { version = "1.0", optional = true }
smallvec = { version = "1.0", optional = true }

[dev-dependencies]
assert_approx_eq = "1.1.0"
Expand Down Expand Up @@ -104,6 +105,7 @@ full = [
"num-bigint",
"num-complex",
"hashbrown",
"smallvec",
"serde",
"indexmap",
"eyre",
Expand Down
1 change: 1 addition & 0 deletions newsfragments/3507.added.md
@@ -0,0 +1 @@
Add `smallvec` feature to add `ToPyObject`, `IntoPy` and `FromPyObject` implementations for `smallvec::SmallVec`.
1 change: 1 addition & 0 deletions src/conversions/mod.rs
Expand Up @@ -9,4 +9,5 @@ pub mod num_bigint;
pub mod num_complex;
pub mod rust_decimal;
pub mod serde;
pub mod smallvec;
mod std;
140 changes: 140 additions & 0 deletions src/conversions/smallvec.rs
@@ -0,0 +1,140 @@
#![cfg(feature = "smallvec")]

//! Conversions to and from [smallvec](https://docs.rs/smallvec/).
//!
//! # Setup
//!
//! To use this feature, add this to your **`Cargo.toml`**:
//!
//! ```toml
//! [dependencies]
//! # change * to the latest versions
//! smallvec = "*"
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"smallvec\"] }")]
//! ```
//!
//! Note that you must use compatible versions of smallvec and PyO3.
//! The required smallvec version may vary based on the version of PyO3.
use crate::exceptions::PyTypeError;
#[cfg(feature = "experimental-inspect")]
use crate::inspect::types::TypeInfo;
use crate::types::list::new_from_iter;
use crate::types::{PySequence, PyString};
use crate::{
ffi, FromPyObject, IntoPy, PyAny, PyDowncastError, PyObject, PyResult, Python, ToPyObject,
};
use smallvec::{Array, SmallVec};

impl<A> ToPyObject for SmallVec<A>
where
A: Array,
A::Item: ToPyObject,
{
fn to_object(&self, py: Python<'_>) -> PyObject {
self.as_slice().to_object(py)
}
}

impl<A> IntoPy<PyObject> for SmallVec<A>
where
A: Array,
A::Item: IntoPy<PyObject>,
{
fn into_py(self, py: Python<'_>) -> PyObject {
let mut iter = self.into_iter().map(|e| e.into_py(py));
let list = new_from_iter(py, &mut iter);
list.into()
}

#[cfg(feature = "experimental-inspect")]
fn type_output() -> TypeInfo {
TypeInfo::list_of(A::Item::type_output())
}
}

impl<'a, A> FromPyObject<'a> for SmallVec<A>
where
A: Array,
A::Item: FromPyObject<'a>,
{
fn extract(obj: &'a PyAny) -> PyResult<Self> {
if obj.is_instance_of::<PyString>() {
return Err(PyTypeError::new_err("Can't extract `str` to `SmallVec`"));
}
extract_sequence(obj)
}

#[cfg(feature = "experimental-inspect")]
fn type_input() -> TypeInfo {
TypeInfo::sequence_of(A::Item::type_input())
}
}

fn extract_sequence<'s, A>(obj: &'s PyAny) -> PyResult<SmallVec<A>>
where
A: Array,
A::Item: FromPyObject<'s>,
{
// Types that pass `PySequence_Check` usually implement enough of the sequence protocol
// to support this function and if not, we will only fail extraction safely.
let seq: &PySequence = unsafe {
if ffi::PySequence_Check(obj.as_ptr()) != 0 {
obj.downcast_unchecked()
} else {
return Err(PyDowncastError::new(obj, "Sequence").into());
}
};

let mut sv = SmallVec::with_capacity(seq.len().unwrap_or(0));
for item in seq.iter()? {
sv.push(item?.extract::<A::Item>()?);
}
Ok(sv)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::types::{PyDict, PyList};

#[test]
fn test_smallvec_into_py() {
Python::with_gil(|py| {
let sv: SmallVec<[u64; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect();
let hso: PyObject = sv.clone().into_py(py);
let l = PyList::new(py, [1, 2, 3, 4, 5]);
assert!(l.eq(hso).unwrap());
});
}

#[test]
fn test_smallvec_from_py_object() {
Python::with_gil(|py| {
let l = PyList::new(py, [1, 2, 3, 4, 5]);
let sv: SmallVec<[u64; 8]> = l.extract().unwrap();
assert_eq!(sv.as_slice(), [1, 2, 3, 4, 5]);
});
}

#[test]
fn test_smallvec_from_py_object_fails() {
Python::with_gil(|py| {
let dict = PyDict::new(py);
let sv: PyResult<SmallVec<[u64; 8]>> = dict.extract();
assert_eq!(
sv.unwrap_err().to_string(),
"TypeError: 'dict' object cannot be converted to 'Sequence'"
);
});
}

#[test]
fn test_smallvec_to_object() {
Python::with_gil(|py| {
let sv: SmallVec<[u64; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect();
let hso: PyObject = sv.to_object(py);
let l = PyList::new(py, [1, 2, 3, 4, 5]);
assert!(l.eq(hso).unwrap());
});
}
}
3 changes: 3 additions & 0 deletions src/lib.rs
Expand Up @@ -94,6 +94,7 @@
//! - [`eyre`]: Enables a conversion from [eyre]’s [`Report`] type to [`PyErr`].
//! - [`hashbrown`]: Enables conversions between Python objects and [hashbrown]'s [`HashMap`] and
//! [`HashSet`] types.
//! - [`smallvec`][smallvec]: Enables conversions between Python list and [smallvec]'s [`SmallVec`].
//! - [`indexmap`][indexmap_feature]: Enables conversions between Python dictionary and [indexmap]'s [`IndexMap`].
//! - [`num-bigint`]: Enables conversions between Python objects and [num-bigint]'s [`BigInt`] and
//! [`BigUint`] types.
Expand Down Expand Up @@ -256,6 +257,7 @@
//! [inventory]: https://docs.rs/inventory
//! [`HashMap`]: https://docs.rs/hashbrown/latest/hashbrown/struct.HashMap.html
//! [`HashSet`]: https://docs.rs/hashbrown/latest/hashbrown/struct.HashSet.html
//! [`SmallVec`]: https://docs.rs/smallvec/latest/smallvec/struct.SmallVec.html
//! [`IndexMap`]: https://docs.rs/indexmap/latest/indexmap/map/struct.IndexMap.html
//! [`BigInt`]: https://docs.rs/num-bigint/latest/num_bigint/struct.BigInt.html
//! [`BigUint`]: https://docs.rs/num-bigint/latest/num_bigint/struct.BigUint.html
Expand All @@ -282,6 +284,7 @@
//! [feature flags]: https://doc.rust-lang.org/cargo/reference/features.html "Features - The Cargo Book"
//! [global interpreter lock]: https://docs.python.org/3/glossary.html#term-global-interpreter-lock
//! [hashbrown]: https://docs.rs/hashbrown
//! [smallvec]: https://docs.rs/smallvec
//! [indexmap]: https://docs.rs/indexmap
//! [manual_builds]: https://pyo3.rs/latest/building_and_distribution.html#manual-builds "Manual builds - Building and Distribution - PyO3 user guide"
//! [num-bigint]: https://docs.rs/num-bigint
Expand Down