From 71ddd5038c0f3536b6c19764d209420b25378810 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Thu, 15 Jul 2021 00:06:46 -0700 Subject: [PATCH 01/23] Add support to IndexMap --- Cargo.toml | 3 +- src/types/dict.rs | 182 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 54df6cc6b2c..2f2904813c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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, < 1.7", optional = true } serde = {version = "1.0", optional = true} [dev-dependencies] @@ -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"] diff --git a/src/types/dict.rs b/src/types/dict.rs index 40cb19177ff..a69cac07aab 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -455,6 +455,188 @@ mod hashbrown_hashmap_conversion { } } +#[cfg(feature = "hashbrown")] +mod hashbrown_hashmap_conversion { + use super::*; + use crate::{FromPyObject, PyErr, PyObject, ToPyObject}; + + impl ToPyObject for hashbrown::HashMap + 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 IntoPy for hashbrown::HashMap + where + K: hash::Hash + cmp::Eq + IntoPy, + V: IntoPy, + 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 + where + K: FromPyObject<'source> + cmp::Eq + hash::Hash, + V: FromPyObject<'source>, + S: hash::BuildHasher + Default, + { + fn extract(ob: &'source PyAny) -> Result { + let dict = ::try_from(ob)?; + let mut ret = hashbrown::HashMap::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_hashbrown_hashmap_to_python() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let mut map = hashbrown::HashMap::::new(); + map.insert(1, 1); + + let m = map.to_object(py); + let py_map = ::try_from(m.as_ref(py)).unwrap(); + + assert!(py_map.len() == 1); + assert!(py_map.get_item(1).unwrap().extract::().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::::new(); + map.insert(1, 1); + + let m: PyObject = map.into_py(py); + let py_map = ::try_from(m.as_ref(py)).unwrap(); + + assert!(py_map.len() == 1); + assert!(py_map.get_item(1).unwrap().extract::().unwrap() == 1); + } + + #[test] + fn test_hashbrown_hashmap_into_dict() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let mut map = hashbrown::HashMap::::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::().unwrap(), 1); + } +} + +#[cfg(feature = "indexmap")] +mod indexmap_indexmap_conversion { + use super::*; + use crate::{FromPyObject, PyErr, PyObject, ToPyObject}; + + impl ToPyObject for indexmap::IndexMap + 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 IntoPy for indexmap::IndexMap + where + K: hash::Hash + cmp::Eq + IntoPy, + V: IntoPy, + 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 + where + K: FromPyObject<'source> + cmp::Eq + hash::Hash, + V: FromPyObject<'source>, + S: hash::BuildHasher + Default, + { + fn extract(ob: &'source PyAny) -> Result { + let dict = ::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() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let mut map = indexmap::IndexMap::::new(); + map.insert(1, 1); + + let m = map.to_object(py); + let py_map = ::try_from(m.as_ref(py)).unwrap(); + + assert!(py_map.len() == 1); + assert!(py_map.get_item(1).unwrap().extract::().unwrap() == 1); + assert_eq!(map, py_map.extract().unwrap()); + } + #[test] + fn test_indexmap_indexmap_into_python() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let mut map = indexmap::IndexMap::::new(); + map.insert(1, 1); + + let m: PyObject = map.into_py(py); + let py_map = ::try_from(m.as_ref(py)).unwrap(); + + assert!(py_map.len() == 1); + assert!(py_map.get_item(1).unwrap().extract::().unwrap() == 1); + } + + #[test] + fn test_indexmap_indexmap_into_dict() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let mut map = indexmap::IndexMap::::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::().unwrap(), 1); + } +} + #[cfg(test)] mod test { use crate::conversion::IntoPy; From 2056c945cdb3579ff4af55d9827431e0063eb5d3 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Thu, 15 Jul 2021 00:18:32 -0700 Subject: [PATCH 02/23] Fix indexmap version to 1.6.2 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2f2904813c2..7ee1d08674e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +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, < 1.7", optional = true } +indexmap = { version = "1.6.2", optional = true } serde = {version = "1.0", optional = true} [dev-dependencies] From 42454bb739cc97efb742c07fcd14822aa0bcc530 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Thu, 15 Jul 2021 00:21:44 -0700 Subject: [PATCH 03/23] Remove code duplication by mistake --- src/types/dict.rs | 91 ----------------------------------------------- 1 file changed, 91 deletions(-) diff --git a/src/types/dict.rs b/src/types/dict.rs index a69cac07aab..980bae23d0f 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -455,97 +455,6 @@ mod hashbrown_hashmap_conversion { } } -#[cfg(feature = "hashbrown")] -mod hashbrown_hashmap_conversion { - use super::*; - use crate::{FromPyObject, PyErr, PyObject, ToPyObject}; - - impl ToPyObject for hashbrown::HashMap - 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 IntoPy for hashbrown::HashMap - where - K: hash::Hash + cmp::Eq + IntoPy, - V: IntoPy, - 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 - where - K: FromPyObject<'source> + cmp::Eq + hash::Hash, - V: FromPyObject<'source>, - S: hash::BuildHasher + Default, - { - fn extract(ob: &'source PyAny) -> Result { - let dict = ::try_from(ob)?; - let mut ret = hashbrown::HashMap::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_hashbrown_hashmap_to_python() { - let gil = Python::acquire_gil(); - let py = gil.python(); - - let mut map = hashbrown::HashMap::::new(); - map.insert(1, 1); - - let m = map.to_object(py); - let py_map = ::try_from(m.as_ref(py)).unwrap(); - - assert!(py_map.len() == 1); - assert!(py_map.get_item(1).unwrap().extract::().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::::new(); - map.insert(1, 1); - - let m: PyObject = map.into_py(py); - let py_map = ::try_from(m.as_ref(py)).unwrap(); - - assert!(py_map.len() == 1); - assert!(py_map.get_item(1).unwrap().extract::().unwrap() == 1); - } - - #[test] - fn test_hashbrown_hashmap_into_dict() { - let gil = Python::acquire_gil(); - let py = gil.python(); - - let mut map = hashbrown::HashMap::::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::().unwrap(), 1); - } -} - #[cfg(feature = "indexmap")] mod indexmap_indexmap_conversion { use super::*; From 0e767be216ead4d00c75cfe2f74dada35dded295 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Thu, 15 Jul 2021 23:13:10 -0700 Subject: [PATCH 04/23] Fix ambiguity in test --- src/types/dict.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/types/dict.rs b/src/types/dict.rs index 980bae23d0f..56331e1e2bc 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -514,7 +514,10 @@ mod indexmap_indexmap_conversion { assert!(py_map.len() == 1); assert!(py_map.get_item(1).unwrap().extract::().unwrap() == 1); - assert_eq!(map, py_map.extract().unwrap()); + assert_eq!( + map, + py_map.extract::>().unwrap() + ); } #[test] fn test_indexmap_indexmap_into_python() { From 0bcca6f9fdb1e757b76a4cbaced37f47e3e67899 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Thu, 15 Jul 2021 23:17:52 -0700 Subject: [PATCH 05/23] Minor change for doc.rs --- src/types/dict.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/dict.rs b/src/types/dict.rs index 56331e1e2bc..44f2423422d 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -456,6 +456,7 @@ mod hashbrown_hashmap_conversion { } #[cfg(feature = "indexmap")] +#[cfg_attr(docsrs, doc(cfg(feature = "indexmap")))] mod indexmap_indexmap_conversion { use super::*; use crate::{FromPyObject, PyErr, PyObject, ToPyObject}; From 5dc9da87e388234a1d10e6f1f89fc6d54f3cca2a Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Thu, 15 Jul 2021 23:20:40 -0700 Subject: [PATCH 06/23] Add to lib.rs docstring --- src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index f101f7fee02..bfb587e8076 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 From 6698fa55adef3a633a093516a731a9c625b8ee52 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Thu, 15 Jul 2021 23:22:50 -0700 Subject: [PATCH 07/23] Add indexmap to conversion table --- guide/src/conversions/tables.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index a7fada477a9..7a8eaccfa97 100644 --- a/guide/src/conversions/tables.md +++ b/guide/src/conversions/tables.md @@ -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` | `&PyList` | -| `dict[K, V]` | `HashMap`, `BTreeMap`, `hashbrown::HashMap`[^2] | `&PyDict` | +| `dict[K, V]` | `HashMap`, `BTreeMap`, `hashbrown::HashMap`[^2], `indexmap::IndexMap`[^3] | `&PyDict` | | `tuple[T, U]` | `(T, U)`, `Vec` | `&PyTuple` | | `set[T]` | `HashSet`, `BTreeSet`, `hashbrown::HashSet`[^2] | `&PySet` | | `frozenset[T]` | `HashSet`, `BTreeSet`, `hashbrown::HashSet`[^2] | `&PyFrozenSet` | @@ -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. From f5b9dbd853ae2b3a0ed8c92a31737577403e3583 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Thu, 15 Jul 2021 23:43:03 -0700 Subject: [PATCH 08/23] Add indexmap flag in docs.rs action --- .github/workflows/guide.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/guide.yml b/.github/workflows/guide.yml index c94d8c630da..e610c612116 100644 --- a/.github/workflows/guide.yml +++ b/.github/workflows/guide.yml @@ -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 "" > gh-pages-build/doc/index.html From e44f3dee5720cb343b0e27c525a46af663a74517 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Thu, 15 Jul 2021 23:44:20 -0700 Subject: [PATCH 09/23] Add indexmap feature to CI --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98a1ad1d9db..59f40915a91 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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) @@ -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 From d53d253e3c2ab20261662096a0f154776a936298 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Thu, 15 Jul 2021 23:58:40 -0700 Subject: [PATCH 10/23] Add note in changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0c0abbcbbe..396cf69ae33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. + ### 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) From 8b24caf1fd47f353a6320d566912f7041b185526 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Fri, 16 Jul 2021 16:25:23 -0700 Subject: [PATCH 11/23] Use with_gil in tests --- src/types/dict.rs | 58 +++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/src/types/dict.rs b/src/types/dict.rs index 44f2423422d..c308e8c58f9 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -504,49 +504,47 @@ mod indexmap_indexmap_conversion { #[test] fn test_indexmap_indexmap_to_python() { - let gil = Python::acquire_gil(); - let py = gil.python(); + Python::with_gil(|py| { + let mut map = indexmap::IndexMap::::new(); + map.insert(1, 1); - let mut map = indexmap::IndexMap::::new(); - map.insert(1, 1); + let m = map.to_object(py); + let py_map = ::try_from(m.as_ref(py)).unwrap(); - let m = map.to_object(py); - let py_map = ::try_from(m.as_ref(py)).unwrap(); - - assert!(py_map.len() == 1); - assert!(py_map.get_item(1).unwrap().extract::().unwrap() == 1); - assert_eq!( - map, - py_map.extract::>().unwrap() - ); + assert!(py_map.len() == 1); + assert!(py_map.get_item(1).unwrap().extract::().unwrap() == 1); + assert_eq!( + map, + py_map.extract::>().unwrap() + ); + }); } + #[test] fn test_indexmap_indexmap_into_python() { - let gil = Python::acquire_gil(); - let py = gil.python(); + Python::with_gil(|py| { + let mut map = indexmap::IndexMap::::new(); + map.insert(1, 1); - let mut map = indexmap::IndexMap::::new(); - map.insert(1, 1); - - let m: PyObject = map.into_py(py); - let py_map = ::try_from(m.as_ref(py)).unwrap(); + let m: PyObject = map.into_py(py); + let py_map = ::try_from(m.as_ref(py)).unwrap(); - assert!(py_map.len() == 1); - assert!(py_map.get_item(1).unwrap().extract::().unwrap() == 1); + assert!(py_map.len() == 1); + assert!(py_map.get_item(1).unwrap().extract::().unwrap() == 1); + }); } #[test] fn test_indexmap_indexmap_into_dict() { - let gil = Python::acquire_gil(); - let py = gil.python(); - - let mut map = indexmap::IndexMap::::new(); - map.insert(1, 1); + Python::with_gil(|py| { + let mut map = indexmap::IndexMap::::new(); + map.insert(1, 1); - let py_map = map.into_py_dict(py); + let py_map = map.into_py_dict(py); - assert_eq!(py_map.len(), 1); - assert_eq!(py_map.get_item(1).unwrap().extract::().unwrap(), 1); + assert_eq!(py_map.len(), 1); + assert_eq!(py_map.get_item(1).unwrap().extract::().unwrap(), 1); + }); } } From 4bc1fcfc7b7f6950223b041ea35e64c21b46c653 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sat, 17 Jul 2021 11:40:24 -0700 Subject: [PATCH 12/23] Move code to src/conversions/indexmap.rs --- src/conversions/indexmap.rs | 102 ++++++++++++++++++++++++++++++++++++ src/conversions/mod.rs | 1 + src/types/dict.rs | 93 -------------------------------- 3 files changed, 103 insertions(+), 93 deletions(-) create mode 100644 src/conversions/indexmap.rs diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs new file mode 100644 index 00000000000..ee983b08957 --- /dev/null +++ b/src/conversions/indexmap.rs @@ -0,0 +1,102 @@ +#[cfg(feature = "indexmap")] +#[cfg_attr(docsrs, doc(cfg(feature = "indexmap")))] +mod indexmap_indexmap_conversion { + + use crate::types::*; + use crate::{FromPyObject, IntoPy, PyErr, PyObject, PyTryFrom, Python, ToPyObject}; + use std::{cmp, hash}; + + impl ToPyObject for indexmap::IndexMap + 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 IntoPy for indexmap::IndexMap + where + K: hash::Hash + cmp::Eq + IntoPy, + V: IntoPy, + 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 + where + K: FromPyObject<'source> + cmp::Eq + hash::Hash, + V: FromPyObject<'source>, + S: hash::BuildHasher + Default, + { + fn extract(ob: &'source PyAny) -> Result { + let dict = ::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) + } + } +} + +#[cfg(feature = "indexmap")] +#[cfg(test)] +mod test_indexmap { + + use crate::types::*; + use crate::{IntoPy, PyObject, PyTryFrom, Python, ToPyObject}; + + #[test] + fn test_indexmap_indexmap_to_python() { + Python::with_gil(|py| { + let mut map = indexmap::IndexMap::::new(); + map.insert(1, 1); + + let m = map.to_object(py); + let py_map = ::try_from(m.as_ref(py)).unwrap(); + + assert!(py_map.len() == 1); + assert!(py_map.get_item(1).unwrap().extract::().unwrap() == 1); + assert_eq!( + map, + py_map.extract::>().unwrap() + ); + }); + } + + #[test] + fn test_indexmap_indexmap_into_python() { + Python::with_gil(|py| { + let mut map = indexmap::IndexMap::::new(); + map.insert(1, 1); + + let m: PyObject = map.into_py(py); + let py_map = ::try_from(m.as_ref(py)).unwrap(); + + assert!(py_map.len() == 1); + assert!(py_map.get_item(1).unwrap().extract::().unwrap() == 1); + }); + } + + #[test] + fn test_indexmap_indexmap_into_dict() { + Python::with_gil(|py| { + let mut map = indexmap::IndexMap::::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::().unwrap(), 1); + }); + } +} diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs index 60c57fc96a0..17c7a204b26 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -1,5 +1,6 @@ //! This module contains conversions between various Rust object and their representation in Python. mod array; +mod indexmap; mod osstr; mod path; diff --git a/src/types/dict.rs b/src/types/dict.rs index c308e8c58f9..40cb19177ff 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -455,99 +455,6 @@ mod hashbrown_hashmap_conversion { } } -#[cfg(feature = "indexmap")] -#[cfg_attr(docsrs, doc(cfg(feature = "indexmap")))] -mod indexmap_indexmap_conversion { - use super::*; - use crate::{FromPyObject, PyErr, PyObject, ToPyObject}; - - impl ToPyObject for indexmap::IndexMap - 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 IntoPy for indexmap::IndexMap - where - K: hash::Hash + cmp::Eq + IntoPy, - V: IntoPy, - 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 - where - K: FromPyObject<'source> + cmp::Eq + hash::Hash, - V: FromPyObject<'source>, - S: hash::BuildHasher + Default, - { - fn extract(ob: &'source PyAny) -> Result { - let dict = ::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() { - Python::with_gil(|py| { - let mut map = indexmap::IndexMap::::new(); - map.insert(1, 1); - - let m = map.to_object(py); - let py_map = ::try_from(m.as_ref(py)).unwrap(); - - assert!(py_map.len() == 1); - assert!(py_map.get_item(1).unwrap().extract::().unwrap() == 1); - assert_eq!( - map, - py_map.extract::>().unwrap() - ); - }); - } - - #[test] - fn test_indexmap_indexmap_into_python() { - Python::with_gil(|py| { - let mut map = indexmap::IndexMap::::new(); - map.insert(1, 1); - - let m: PyObject = map.into_py(py); - let py_map = ::try_from(m.as_ref(py)).unwrap(); - - assert!(py_map.len() == 1); - assert!(py_map.get_item(1).unwrap().extract::().unwrap() == 1); - }); - } - - #[test] - fn test_indexmap_indexmap_into_dict() { - Python::with_gil(|py| { - let mut map = indexmap::IndexMap::::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::().unwrap(), 1); - }); - } -} - #[cfg(test)] mod test { use crate::conversion::IntoPy; From 6a4fb72cd8ce2c6b3aaeb3ae9794e2e24191ba79 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sat, 17 Jul 2021 11:41:08 -0700 Subject: [PATCH 13/23] Add PR number to CHANGELOG Co-authored-by: David Hewitt <1939362+davidhewitt@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 396cf69ae33..de723f09571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added -- Add `indexmap` feature to convert `indexmap::IndexMap` into `&PyDict`, an alternative that preserves the insertion order of the elements. +- Add `indexmap` feature to add `ToPyObject`, `IntoPy` and `FromPyObject` implementations for `indexmap::IndexMap`. [#1728](https://github.com/PyO3/pyo3/pull/1728) ### Fixed From c92e77fd3bc744b8418f8f6bd9c16900e7d28ad7 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sat, 17 Jul 2021 13:00:05 -0700 Subject: [PATCH 14/23] Add round trip test --- src/conversions/indexmap.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index ee983b08957..1fa16101830 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -99,4 +99,34 @@ mod test_indexmap { assert_eq!(py_map.get_item(1).unwrap().extract::().unwrap(), 1); }); } + + #[test] + fn test_indexmap_indexmap_insertion_order_round_trip() { + Python::with_gil(|py| { + let n = 20; + let mut map = indexmap::IndexMap::::new(); + + for i in 1..=n { + if i % 2 == 1 { + map.insert(i, i); + } else { + map.insert(n - i, i); + } + } + + let py_map = map.clone().into_py_dict(py); + + let trip_map = py_map.extract::>().unwrap(); + + for (((k1, v1), (k2, v2)), (k3, v3)) in + map.iter().zip(py_map.iter()).zip(trip_map.iter()) + { + let k2 = k2.extract::().unwrap(); + let v2 = v2.extract::().unwrap(); + assert_eq!((k1, v1), (&k2, &v2)); + assert_eq!((k1, v1), (k3, v3)); + assert_eq!((&k2, &v2), (k3, v3)); + } + }); + } } From 2b2ce3c63e14c63f1b60838aaae65155af528989 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sat, 17 Jul 2021 13:14:10 -0700 Subject: [PATCH 15/23] Fix issue in MSRV Ubuntu build --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59f40915a91..8448a48838a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,6 +135,7 @@ jobs: - if: matrix.msrv == 'MSRV' name: Prepare minimal package versions (MSRV only) run: cargo update -p hashbrown --precise 0.9.1 + run: cargo update -p indexmap --precise 1.6.2 - name: Build docs run: cargo doc --no-deps --no-default-features --features "${{ steps.settings.outputs.all_additive_features }}" From 834fe488815cba422f844d826c1b8b8bcd49ec4b Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sun, 18 Jul 2021 22:33:22 -0700 Subject: [PATCH 16/23] Fix Github workflow syntax --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8448a48838a..f645b8c6cb4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,8 +134,9 @@ jobs: - if: matrix.msrv == 'MSRV' name: Prepare minimal package versions (MSRV only) - run: cargo update -p hashbrown --precise 0.9.1 - run: cargo update -p indexmap --precise 1.6.2 + run: | + cargo update -p hashbrown --precise 0.9.1 + cargo update -p indexmap --precise 1.6.2 - name: Build docs run: cargo doc --no-deps --no-default-features --features "${{ steps.settings.outputs.all_additive_features }}" From 6ec680d734763c57317b66dd294aeff85af07fb6 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sun, 18 Jul 2021 22:45:07 -0700 Subject: [PATCH 17/23] Yet Another Attempt to Fix MSRV Ubuntu build --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f645b8c6cb4..f5654596ae1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,8 +135,8 @@ jobs: - if: matrix.msrv == 'MSRV' name: Prepare minimal package versions (MSRV only) run: | - cargo update -p hashbrown --precise 0.9.1 cargo update -p indexmap --precise 1.6.2 + cargo update -p hashbrown --precise 0.9.1 - name: Build docs run: cargo doc --no-deps --no-default-features --features "${{ steps.settings.outputs.all_additive_features }}" From 685a3a6930db4bae23a7c62849672f2b0592cf2c Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Sun, 18 Jul 2021 23:06:52 -0700 Subject: [PATCH 18/23] Specify hashbrown to avoid ambiguity in CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5654596ae1..1e77a171b4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,7 +136,7 @@ jobs: name: Prepare minimal package versions (MSRV only) run: | cargo update -p indexmap --precise 1.6.2 - cargo update -p hashbrown --precise 0.9.1 + cargo update -p hashbrown:0.11.2 --precise 0.9.1 - name: Build docs run: cargo doc --no-deps --no-default-features --features "${{ steps.settings.outputs.all_additive_features }}" From 474a5e8ca7588937750098eddbc8dc24319ab573 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Mon, 19 Jul 2021 08:46:54 -0700 Subject: [PATCH 19/23] Add suggestions --- src/conversions/indexmap.rs | 3 --- src/conversions/mod.rs | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index 1fa16101830..8e48dbc55d6 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -1,5 +1,3 @@ -#[cfg(feature = "indexmap")] -#[cfg_attr(docsrs, doc(cfg(feature = "indexmap")))] mod indexmap_indexmap_conversion { use crate::types::*; @@ -48,7 +46,6 @@ mod indexmap_indexmap_conversion { } } -#[cfg(feature = "indexmap")] #[cfg(test)] mod test_indexmap { diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs index 17c7a204b26..5bf6d4c8cb6 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -1,6 +1,8 @@ //! This module contains conversions between various Rust object and their representation in Python. mod array; +#[cfg(feature = "indexmap")] +#[cfg_attr(docsrs, doc(cfg(feature = "indexmap")))] mod indexmap; mod osstr; mod path; From d4b526491d5183d67bb562989aabae057cb66ca2 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Mon, 19 Jul 2021 08:50:06 -0700 Subject: [PATCH 20/23] More flexible version for indexmap --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7ee1d08674e..7c4420eeb8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +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 } +indexmap = { version = ">= 1.6, < 1.8", optional = true } serde = {version = "1.0", optional = true} [dev-dependencies] From cdfd3caf5e060de655ce6ab65d4cb9647279c1d9 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Tue, 20 Jul 2021 00:50:51 -0700 Subject: [PATCH 21/23] Add documentation --- src/conversions/indexmap.rs | 94 +++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index 8e48dbc55d6..39dd828592b 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -1,3 +1,97 @@ +//! Conversions to and from [indexmap](https://docs.rs/indexmap/)’s +//! `IndexMap`. +//! +//! [`indexmap::IndexMap`] is a hash table that is closely compatible with the standard [`std::collections::HashMap`], +//! with the difference that it preserves the insertion order when iterating over keys. It was inspired +//! by Python's 3.6+ dict implementation. +//! +//! Dictionary order is guaranteed to be insertion order since Python 3.7, and the conversions with IndexMap will reflect +//! that guarantee. In practice, the guarantee will also be mainted in Python 3.6 because of the implementation details +//! of CPython and PyPy. +//! +//! # Setup +//! +//! To use this feature, add this to your **`Cargo.toml`**: +//! +//! ```toml +//! [dependencies] +//! # change * to the latest versions +//! indexmap = "*" +// workaround for `extended_key_value_attributes`: https://github.com/rust-lang/rust/issues/82768#issuecomment-803935643 +#![cfg_attr(docsrs, cfg_attr(docsrs, doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"indexmap\"] }")))] +#![cfg_attr( + not(docsrs), + doc = "pyo3 = { version = \"*\", features = [\"indexmap\"] }" +)] +//! ``` +//! +//! Note that you must use compatible versions of indexmap and PyO3. +//! The required indexmap version may vary based on the version of PyO3. +//! +//! # Examples +//! +//! Using [indexmap](https://docs.rs/indexmap) to return a dictionary with some statistics +//! about a list of numbers. Because of the insertion order guarantees, the Python code will +//! always print the same result, matching users' expectations about Python's dict. +//! +//! ```rust +//! use indexmap::{indexmap, IndexMap}; +//! use pyo3::prelude::*; +//! +//! fn median(data: &Vec) -> f32 { +//! let sorted_data = data.clone().sort(); +//! let mid = data.len() / 2; +//! if (data.len() % 2 == 0) { +//! data[mid] as f32 +//! } +//! else { +//! (data[mid] + data[mid - 1]) as f32 / 2.0 +//! } +//! } +//! +//! fn mean(data: &Vec) -> f32 { +//! data.iter().sum::() as f32 / data.len() as f32 +//! } +//! fn mode(data: &Vec) -> f32 { +//! let mut frequency = IndexMap::new(); // we can use IndexMap as any hash table +//! +//! for &element in data { +//! *frequency.entry(element).or_insert(0) += 1; +//! } +//! +//! frequency +//! .iter() +//! .max_by(|a, b| a.1.cmp(&b.1)) +//! .map(|(k, _v)| *k) +//! .unwrap() as f32 +//! } +//! +//! #[pyfunction] +//! fn calculate_statistics(data: Vec) -> IndexMap<&'static str, f32> { +//! indexmap!{ +//! "median" => median(&data), +//! "mean" => mean(&data), +//! "mode" => mode(&data), +//! } +//! } +//! +//! #[pymodule] +//! fn my_module(_py: Python, m: &PyModule) -> PyResult<()> { +//! m.add_function(wrap_pyfunction!(calculate_statistics, m)?)?; +//! Ok(()) +//! } +//! ``` +//! +//! Python code: +//! ```python +//! from my_module import calculate_statistics +//! +//! data = [1, 1, 1, 3, 4, 5] +//! print(calculate_statistics(data)) +//! # always prints {"median": 2.0, "mean": 2.5, "mode": 1.0} in the same order +//! # if another hash table was used, the order could be random +//! ``` + mod indexmap_indexmap_conversion { use crate::types::*; From 2dbfef5c73a7dc88bae84b3a3d67f71ca7385f1a Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Tue, 20 Jul 2021 09:17:49 -0700 Subject: [PATCH 22/23] Address PR comments --- src/conversions/indexmap.rs | 82 ++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 43 deletions(-) diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index 39dd828592b..89f0eda12ca 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -5,9 +5,8 @@ //! with the difference that it preserves the insertion order when iterating over keys. It was inspired //! by Python's 3.6+ dict implementation. //! -//! Dictionary order is guaranteed to be insertion order since Python 3.7, and the conversions with IndexMap will reflect -//! that guarantee. In practice, the guarantee will also be mainted in Python 3.6 because of the implementation details -//! of CPython and PyPy. +//! Dictionary order is guaranteed to be insertion order in Python, hence IndexMap is a good candidate +//! for maintaining an equivalent behaviour in Rust. //! //! # Setup //! @@ -92,51 +91,48 @@ //! # if another hash table was used, the order could be random //! ``` -mod indexmap_indexmap_conversion { - - use crate::types::*; - use crate::{FromPyObject, IntoPy, PyErr, PyObject, PyTryFrom, Python, ToPyObject}; - use std::{cmp, hash}; - - impl ToPyObject for indexmap::IndexMap - 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() - } +use crate::types::*; +use crate::{FromPyObject, IntoPy, PyErr, PyObject, PyTryFrom, Python, ToPyObject}; +use std::{cmp, hash}; + +impl ToPyObject for indexmap::IndexMap +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 IntoPy for indexmap::IndexMap - where - K: hash::Hash + cmp::Eq + IntoPy, - V: IntoPy, - 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 IntoPy for indexmap::IndexMap +where + K: hash::Hash + cmp::Eq + IntoPy, + V: IntoPy, + 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 - where - K: FromPyObject<'source> + cmp::Eq + hash::Hash, - V: FromPyObject<'source>, - S: hash::BuildHasher + Default, - { - fn extract(ob: &'source PyAny) -> Result { - let dict = ::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) +impl<'source, K, V, S> FromPyObject<'source> for indexmap::IndexMap +where + K: FromPyObject<'source> + cmp::Eq + hash::Hash, + V: FromPyObject<'source>, + S: hash::BuildHasher + Default, +{ + fn extract(ob: &'source PyAny) -> Result { + let dict = ::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) } } From bc6c6fbb8cc0c1c22b6aa9f191490d0ff6c08279 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho Date: Tue, 20 Jul 2021 11:55:30 -0700 Subject: [PATCH 23/23] Export indexmap for docs --- src/conversions/mod.rs | 2 +- src/lib.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs index 5bf6d4c8cb6..ad2e0b9133f 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -3,6 +3,6 @@ mod array; #[cfg(feature = "indexmap")] #[cfg_attr(docsrs, doc(cfg(feature = "indexmap")))] -mod indexmap; +pub mod indexmap; mod osstr; mod path; diff --git a/src/lib.rs b/src/lib.rs index bfb587e8076..620a14f1dfd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,7 +74,7 @@ //! [`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`](crate::indexmap): Enables conversions between Python dictionary and //! [indexmap](https://docs.rs/indexmap)'s //! [`IndexMap`](https://docs.rs/indexmap/latest/indexmap/map/struct.IndexMap.html). // @@ -307,6 +307,10 @@ pub mod num_bigint; pub mod num_complex; +#[cfg_attr(docsrs, doc(cfg(feature = "indexmap")))] +#[cfg(feature = "indexmap")] +pub use crate::conversions::indexmap; + #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] #[cfg(feature = "serde")] pub mod serde;