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 optional support for conversion from indexmap::IndexMap #1728

Merged
merged 23 commits into from
Jul 22, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ jobs:
id: settings
shell: bash
run: |
echo "::set-output name=all_additive_features::macros num-bigint num-complex hashbrown serde multiple-pymethods"
echo "::set-output name=all_additive_features::macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods"

- if: matrix.msrv == 'MSRV'
name: Prepare minimal package versions (MSRV only)
Expand Down Expand Up @@ -229,7 +229,7 @@ jobs:
profile: minimal
components: llvm-tools-preview
- run: cargo test --no-default-features --no-fail-fast
- run: cargo test --no-default-features --no-fail-fast --features "macros num-bigint num-complex hashbrown serde multiple-pymethods"
- run: cargo test --no-default-features --no-fail-fast --features "macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods"
- run: cargo test --manifest-path=pyo3-macros-backend/Cargo.toml
- run: cargo test --manifest-path=pyo3-build-config/Cargo.toml
# can't yet use actions-rs/grcov with source-based coverage: https://github.com/actions-rs/grcov/issues/105
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/guide.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
# This adds the docs to gh-pages-build/doc
- name: Build the doc
run: |
cargo +nightly rustdoc --lib --no-default-features --features="macros num-bigint num-complex hashbrown serde multiple-pymethods" -- --cfg docsrs
cargo +nightly rustdoc --lib --no-default-features --features="macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods" -- --cfg docsrs
cp -r target/doc gh-pages-build/doc
echo "<meta http-equiv=refresh content=0;url=pyo3/index.html>" > gh-pages-build/doc/index.html

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- Add `indexmap` feature to convert `indexmap::IndexMap` into `&PyDict`, an alternative that preserves the insertion order of the elements.
IvanIsCoding marked this conversation as resolved.
Show resolved Hide resolved

### Fixed

- Fix regression in 0.14.0 rejecting usage of `#[doc(hidden)]` on structs and functions annotated with PyO3 macros. [#1722](https://github.com/PyO3/pyo3/pull/1722)
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ paste = { version = "0.1.18", optional = true }
pyo3-macros = { path = "pyo3-macros", version = "=0.14.1", optional = true }
unindent = { version = "0.1.4", optional = true }
hashbrown = { version = ">= 0.9, < 0.12", optional = true }
indexmap = { version = "1.6.2", optional = true }
IvanIsCoding marked this conversation as resolved.
Show resolved Hide resolved
serde = {version = "1.0", optional = true}

[dev-dependencies]
Expand Down Expand Up @@ -117,5 +118,5 @@ members = [

[package.metadata.docs.rs]
no-default-features = true
features = ["macros", "num-bigint", "num-complex", "hashbrown", "serde", "multiple-pymethods"]
features = ["macros", "num-bigint", "num-complex", "hashbrown", "serde", "multiple-pymethods", "indexmap"]
rustdoc-args = ["--cfg", "docsrs"]
4 changes: 3 additions & 1 deletion guide/src/conversions/tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ 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>`, `hashbrown::HashMap<K, V>`[^2] | `&PyDict` |
| `dict[K, V]` | `HashMap<K, V>`, `BTreeMap<K, V>`, `hashbrown::HashMap<K, V>`[^2], `indexmap::IndexMap<K, V>`[^3] | `&PyDict` |
| `tuple[T, U]` | `(T, U)`, `Vec<T>` | `&PyTuple` |
| `set[T]` | `HashSet<T>`, `BTreeSet<T>`, `hashbrown::HashSet<T>`[^2] | `&PySet` |
| `frozenset[T]` | `HashSet<T>`, `BTreeSet<T>`, `hashbrown::HashSet<T>`[^2] | `&PyFrozenSet` |
Expand Down Expand Up @@ -94,3 +94,5 @@ Finally, the following Rust types are also able to convert to Python as return v
[^1]: Requires the `num-complex` optional feature.

[^2]: Requires the `hashbrown` optional feature.

[^3]: Requires the `indexmap` optional feature.
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@
//! [`HashMap`](https://docs.rs/hashbrown/latest/hashbrown/struct.HashMap.html) and
//! [`HashSet`](https://docs.rs/hashbrown/latest/hashbrown/struct.HashSet.html) types.
//
//! - `indexmap`: Enables conversions between Python dictionary and
//! [indexmap](https://docs.rs/indexmap)'s
//! [`IndexMap`](https://docs.rs/indexmap/latest/indexmap/map/struct.IndexMap.html).
//
//! - `multiple-pymethods`: Enables the use of multiple
//! [`#[pymethods]`](crate::proc_macro::pymethods) blocks per
//! [`#[pyclass]`](crate::proc_macro::pyclass). This adds a dependency on the
Expand Down
93 changes: 93 additions & 0 deletions src/types/dict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,99 @@ mod hashbrown_hashmap_conversion {
}
}

#[cfg(feature = "indexmap")]
#[cfg_attr(docsrs, doc(cfg(feature = "indexmap")))]
mod indexmap_indexmap_conversion {
IvanIsCoding marked this conversation as resolved.
Show resolved Hide resolved
use super::*;
use crate::{FromPyObject, PyErr, PyObject, ToPyObject};

impl<K, V, H> ToPyObject for indexmap::IndexMap<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 indexmap::IndexMap<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 indexmap::IndexMap<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 = indexmap::IndexMap::with_capacity_and_hasher(dict.len(), S::default());
for (k, v) in dict.iter() {
ret.insert(K::extract(k)?, V::extract(v)?);
}
Ok(ret)
}
}

#[test]
fn test_indexmap_indexmap_to_python() {
IvanIsCoding marked this conversation as resolved.
Show resolved Hide resolved
Python::with_gil(|py| {
let mut map = indexmap::IndexMap::<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::<indexmap::IndexMap::<i32, i32>>().unwrap()
);
});
}

#[test]
fn test_indexmap_indexmap_into_python() {
Python::with_gil(|py| {
let mut map = indexmap::IndexMap::<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_indexmap_indexmap_into_dict() {
Python::with_gil(|py| {
let mut map = indexmap::IndexMap::<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;
Expand Down