Merge pull request #1114 from mtreinish/hashbrown

Add optional support for conversion from Hashbrown types
This commit is contained in:
David Hewitt 2020-08-26 12:48:21 +01:00 committed by GitHub
commit 16ef96904a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 171 additions and 3 deletions

View file

@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Implement type information for conversion failures. [#1050](https://github.com/PyO3/pyo3/pull/1050)
- Add `PyBytes::new_with` and `PyByteArray::new_with` for initialising Python-allocated bytes and bytearrays using a closure. [#1074](https://github.com/PyO3/pyo3/pull/1074)
- Add `Py::as_ref` and `Py::into_ref`. [#1098](https://github.com/PyO3/pyo3/pull/1098)
- Add optional implementations of `ToPyObject`, `IntoPy`, and `FromPyObject` for [hashbrown](https://crates.io/crates/hashbrown)'s `HashMap` and `HashSet` types. The `hashbrown` feature must be enabled for these implementations to be built. [#1114](https://github.com/PyO3/pyo3/pull/1114/)
### Changed
- Exception types have been renamed from e.g. `RuntimeError` to `PyRuntimeError`, and are now only accessible by `&T` or `Py<T>` similar to other Python-native types. The old names continue to exist but are deprecated. [#1024](https://github.com/PyO3/pyo3/pull/1024)

View file

@ -25,6 +25,7 @@ num-complex = { version = "0.3", optional = true }
paste = { version = "0.1.6", optional = true }
pyo3cls = { path = "pyo3cls", version = "=0.11.1", optional = true }
unindent = { version = "0.1.4", optional = true }
hashbrown = { version = "0.8", optional = true }
[dev-dependencies]
assert_approx_eq = "1.1.0"

View file

@ -24,10 +24,10 @@ The table below contains the Python type and the corresponding function argument
| `float` | `f32`, `f64` | `&PyFloat` |
| `complex` | `num_complex::Complex`[^1] | `&PyComplex` |
| `list[T]` | `Vec<T>` | `&PyList` |
| `dict[K, V]` | `HashMap<K, V>`, `BTreeMap<K, V>` | `&PyDict` |
| `dict[K, V]` | `HashMap<K, V>`, `BTreeMap<K, V>`, `hashbrown::HashMap<K, V>`[^2] | `&PyDict` |
| `tuple[T, U]` | `(T, U)`, `Vec<T>` | `&PyTuple` |
| `set[T]` | `HashSet<T>`, `BTreeSet<T>` | `&PySet` |
| `frozenset[T]` | `HashSet<T>`, `BTreeSet<T>` | `&PyFrozenSet` |
| `set[T]` | `HashSet<T>`, `BTreeSet<T>`, `hashbrown::HashSet<T>`[^2] | `&PySet` |
| `frozenset[T]` | `HashSet<T>`, `BTreeSet<T>`, `hashbrown::HashSet<T>`[^2] | `&PyFrozenSet` |
| `bytearray` | `Vec<u8>` | `&PyByteArray` |
| `slice` | - | `&PySlice` |
| `type` | - | `&PyType` |
@ -250,3 +250,4 @@ fn main() {
[`PyRefMut`]: https://docs.rs/pyo3/latest/pyo3/pycell/struct.PyRefMut.html
[^1]: Requires the `num-complex` optional feature.
[^2]: Requires the `hashbrown` optional feature.

View file

@ -354,6 +354,97 @@ where
}
}
#[cfg(feature = "hashbrown")]
mod hashbrown_hashmap_conversion {
use super::*;
use crate::{FromPyObject, PyErr, PyObject, ToPyObject};
impl<K, V, H> ToPyObject for hashbrown::HashMap<K, V, H>
where
K: hash::Hash + cmp::Eq + ToPyObject,
V: ToPyObject,
H: hash::BuildHasher,
{
fn to_object(&self, py: Python) -> PyObject {
IntoPyDict::into_py_dict(self, py).into()
}
}
impl<K, V, H> IntoPy<PyObject> for hashbrown::HashMap<K, V, H>
where
K: hash::Hash + cmp::Eq + IntoPy<PyObject>,
V: IntoPy<PyObject>,
H: hash::BuildHasher,
{
fn into_py(self, py: Python) -> PyObject {
let iter = self
.into_iter()
.map(|(k, v)| (k.into_py(py), v.into_py(py)));
IntoPyDict::into_py_dict(iter, py).into()
}
}
impl<'source, K, V, S> FromPyObject<'source> for hashbrown::HashMap<K, V, S>
where
K: FromPyObject<'source> + cmp::Eq + hash::Hash,
V: FromPyObject<'source>,
S: hash::BuildHasher + Default,
{
fn extract(ob: &'source PyAny) -> Result<Self, PyErr> {
let dict = <PyDict as PyTryFrom>::try_from(ob)?;
let mut ret = hashbrown::HashMap::default();
for (k, v) in dict.iter() {
ret.insert(K::extract(k)?, V::extract(v)?);
}
Ok(ret)
}
}
#[test]
fn test_hashbrown_hashmap_to_python() {
let gil = Python::acquire_gil();
let py = gil.python();
let mut map = hashbrown::HashMap::<i32, i32>::new();
map.insert(1, 1);
let m = map.to_object(py);
let py_map = <PyDict as PyTryFrom>::try_from(m.as_ref(py)).unwrap();
assert!(py_map.len() == 1);
assert!(py_map.get_item(1).unwrap().extract::<i32>().unwrap() == 1);
assert_eq!(map, py_map.extract().unwrap());
}
#[test]
fn test_hashbrown_hashmap_into_python() {
let gil = Python::acquire_gil();
let py = gil.python();
let mut map = hashbrown::HashMap::<i32, i32>::new();
map.insert(1, 1);
let m: PyObject = map.into_py(py);
let py_map = <PyDict as PyTryFrom>::try_from(m.as_ref(py)).unwrap();
assert!(py_map.len() == 1);
assert!(py_map.get_item(1).unwrap().extract::<i32>().unwrap() == 1);
}
#[test]
fn test_hashbrown_hashmap_into_dict() {
let gil = Python::acquire_gil();
let py = gil.python();
let mut map = hashbrown::HashMap::<i32, i32>::new();
map.insert(1, 1);
let py_map = map.into_py_dict(py);
assert_eq!(py_map.len(), 1);
assert_eq!(py_map.get_item(1).unwrap().extract::<i32>().unwrap(), 1);
}
}
#[cfg(test)]
mod test {
use crate::conversion::IntoPy;

View file

@ -300,6 +300,80 @@ impl<'a> std::iter::IntoIterator for &'a PyFrozenSet {
}
}
#[cfg(feature = "hashbrown")]
mod hashbrown_hashset_conversion {
use super::*;
use crate::{FromPyObject, PyObject, PyResult, ToPyObject};
impl<T> ToPyObject for hashbrown::HashSet<T>
where
T: hash::Hash + Eq + ToPyObject,
{
fn to_object(&self, py: Python) -> PyObject {
let set = PySet::new::<T>(py, &[]).expect("Failed to construct empty set");
{
for val in self {
set.add(val).expect("Failed to add to set");
}
}
set.into()
}
}
impl<K, S> IntoPy<PyObject> for hashbrown::HashSet<K, S>
where
K: IntoPy<PyObject> + Eq + hash::Hash,
S: hash::BuildHasher + Default,
{
fn into_py(self, py: Python) -> PyObject {
let set = PySet::empty(py).expect("Failed to construct empty set");
{
for val in self {
set.add(val.into_py(py)).expect("Failed to add to set");
}
}
set.into()
}
}
impl<'source, K, S> FromPyObject<'source> for hashbrown::HashSet<K, S>
where
K: FromPyObject<'source> + cmp::Eq + hash::Hash,
S: hash::BuildHasher + Default,
{
fn extract(ob: &'source PyAny) -> PyResult<Self> {
let set: &PySet = ob.downcast()?;
set.iter().map(K::extract).collect()
}
}
#[test]
fn test_extract_hashbrown_hashset() {
use std::iter::FromIterator;
let gil = Python::acquire_gil();
let py = gil.python();
let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap();
let hash_set: hashbrown::HashSet<usize> = set.extract().unwrap();
assert_eq!(
hash_set,
hashbrown::HashSet::from_iter([1, 2, 3, 4, 5].iter().copied())
);
}
#[test]
fn test_hashbrown_hashset_into_py() {
let gil = Python::acquire_gil();
let py = gil.python();
let hs: hashbrown::HashSet<u64> = [1, 2, 3, 4, 5].iter().cloned().collect();
let hso: PyObject = hs.clone().into_py(py);
assert_eq!(hs, hso.extract(py).unwrap());
}
}
#[cfg(test)]
mod test {
use super::{PyFrozenSet, PySet};