From 7c9c10fe088d1e720e3d403b87e99fb754d5776b Mon Sep 17 00:00:00 2001 From: Ewen Dantec Date: Fri, 30 Aug 2024 13:51:01 +0200 Subject: [PATCH 1/3] Add bp::dist to std::map converter + unittest --- include/eigenpy/std-map.hpp | 153 ++++++++++++++++++++++++++++++-- unittest/CMakeLists.txt | 3 + unittest/python/test_std_map.py | 6 ++ unittest/std_map.cpp | 35 ++++++++ 4 files changed, 190 insertions(+), 7 deletions(-) create mode 100644 unittest/python/test_std_map.py create mode 100644 unittest/std_map.cpp diff --git a/include/eigenpy/std-map.hpp b/include/eigenpy/std-map.hpp index 9b010e900..c6e1b0e6e 100644 --- a/include/eigenpy/std-map.hpp +++ b/include/eigenpy/std-map.hpp @@ -7,6 +7,11 @@ #define __eigenpy_utils_map_hpp__ #include +#include "eigenpy/pickle-vector.hpp" +#include +#include +#include +#include namespace eigenpy { namespace details { @@ -31,31 +36,165 @@ struct overload_base_get_item_for_std_map typename Container::iterator i = container.get().find(idx); if (i == container.get().end()) { PyErr_SetString(PyExc_KeyError, "Invalid key"); - bp::throw_error_already_set(); + boost::python::throw_error_already_set(); } - typename bp::to_python_indirect + typename boost::python::to_python_indirect< + data_type&, boost::python::detail::make_reference_holder> convert; - return bp::object(bp::handle<>(convert(i->second))); + return boost::python::object(boost::python::handle<>(convert(i->second))); } static index_type convert_index(Container& /*container*/, PyObject* i_) { - bp::extract i(i_); + boost::python::extract i(i_); if (i.check()) { return i(); } else { - bp::extract i(i_); + boost::python::extract i(i_); if (i.check()) return i(); } PyErr_SetString(PyExc_TypeError, "Invalid index type"); - bp::throw_error_already_set(); + boost::python::throw_error_already_set(); return index_type(); } }; } // namespace details + +/////////////////////////////////////////////////////////////////////////////// +// The following snippet of code has been taken from the header +// https://github.com/loco-3d/crocoddyl/blob/v2.1.0/bindings/python/crocoddyl/utils/map-converter.hpp +// The Crocoddyl library is written by Carlos Mastalli, Nicolas Mansard and +// Rohan Budhiraja. +/////////////////////////////////////////////////////////////////////////////// + +namespace python { + +namespace bp = boost::python; + +/** + * @brief Create a pickle interface for the std::map + * + * @param[in] Container Map type to be pickled + * \sa Pickle + */ +template +struct PickleMap : public PickleVector { + static void setstate(bp::object op, bp::tuple tup) { + Container& o = bp::extract(op)(); + bp::stl_input_iterator begin(tup[0]), end; + o.insert(begin, end); + } +}; + +/// Conversion from dict to map solution proposed in +/// https://stackoverflow.com/questions/6116345/boostpython-possible-to-automatically-convert-from-dict-stdmap +/// This template encapsulates the conversion machinery. +template +struct dict_to_map { + static void register_converter() { + bp::converter::registry::push_back(&dict_to_map::convertible, + &dict_to_map::construct, + bp::type_id()); + } + + /// Check if conversion is possible + static void* convertible(PyObject* object) { + // Check if it is a list + if (!PyObject_GetIter(object)) return 0; + return object; + } + + /// Perform the conversion + static void construct(PyObject* object, + bp::converter::rvalue_from_python_stage1_data* data) { + // convert the PyObject pointed to by `object` to a bp::dict + bp::handle<> handle(bp::borrowed(object)); // "smart ptr" + bp::dict dict(handle); + + // get a pointer to memory into which we construct the map + // this is provided by the Python runtime + typedef bp::converter::rvalue_from_python_storage storage_type; + void* storage = reinterpret_cast(data)->storage.bytes; + + // placement-new allocate the result + new (storage) Container(); + + // iterate over the dictionary `dict`, fill up the map `map` + Container& map(*(static_cast(storage))); + bp::list keys(dict.keys()); + int keycount(static_cast(bp::len(keys))); + for (int i = 0; i < keycount; ++i) { + // get the key + bp::object keyobj(keys[i]); + bp::extract keyproxy(keyobj); + if (!keyproxy.check()) { + PyErr_SetString(PyExc_KeyError, "Bad key type"); + bp::throw_error_already_set(); + } + typename Container::key_type key = keyproxy(); + + // get the corresponding value + bp::object valobj(dict[keyobj]); + bp::extract valproxy(valobj); + if (!valproxy.check()) { + PyErr_SetString(PyExc_ValueError, "Bad value type"); + bp::throw_error_already_set(); + } + typename Container::mapped_type val = valproxy(); + map[key] = val; + } + + // remember the location for later + data->convertible = storage; + } + + static bp::dict todict(Container& self) { + bp::dict dict; + typename Container::const_iterator it; + for (it = self.begin(); it != self.end(); ++it) { + dict.setdefault(it->first, it->second); + } + return dict; + } +}; + +/** + * @brief Expose an std::map from a type given as template argument. + * + * @param[in] T Type to expose as std::map. + * @param[in] Compare Type for the Compare in std::map. + * @param[in] Allocator Type for the Allocator in + * std::map. + * @param[in] NoProxy When set to false, the elements will be copied when + * returned to Python. + */ +template , + class Allocator = std::allocator >, + bool NoProxy = false> +struct StdMapPythonVisitor + : public bp::map_indexing_suite< + typename std::map, NoProxy>, + public dict_to_map > { + typedef std::map Container; + typedef dict_to_map FromPythonDictConverter; + + static void expose(const std::string& class_name, + const std::string& doc_string = "") { + namespace bp = bp; + + bp::class_(class_name.c_str(), doc_string.c_str()) + .def(StdMapPythonVisitor()) + .def("todict", &FromPythonDictConverter::todict, bp::arg("self"), + "Returns the std::map as a Python dictionary.") + .def_pickle(PickleMap()); + // Register conversion + FromPythonDictConverter::register_converter(); + } +}; + +} // namespace python } // namespace eigenpy #endif // ifndef __eigenpy_utils_map_hpp__ diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index a65ac2e02..2c1eec31b 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -56,6 +56,7 @@ endif() add_lib_unit_test(std_vector) add_lib_unit_test(std_array) add_lib_unit_test(std_pair) +add_lib_unit_test(std_map) add_lib_unit_test(user_struct) if(CMAKE_CXX_STANDARD GREATER 14 AND CMAKE_CXX_STANDARD LESS 98) @@ -155,6 +156,8 @@ add_python_eigenpy_lib_unit_test("py-std-vector" add_python_lib_unit_test("py-std-array" "unittest/python/test_std_array.py") +add_python_lib_unit_test("py-std-map" "unittest/python/test_std_map.py") + add_python_lib_unit_test("py-std-pair" "unittest/python/test_std_pair.py") add_python_lib_unit_test("py-user-struct" "unittest/python/test_user_struct.py") diff --git a/unittest/python/test_std_map.py b/unittest/python/test_std_map.py new file mode 100644 index 000000000..091027b21 --- /dev/null +++ b/unittest/python/test_std_map.py @@ -0,0 +1,6 @@ +from std_map import copy, std_map_to_dict + +t = {"one": 1.0, "two": 2.0} + +assert std_map_to_dict(t) == t +assert std_map_to_dict(copy(t)) == t diff --git a/unittest/std_map.cpp b/unittest/std_map.cpp new file mode 100644 index 000000000..5997e39cc --- /dev/null +++ b/unittest/std_map.cpp @@ -0,0 +1,35 @@ +/// @file +/// @copyright Copyright 2023 CNRS INRIA + +#include +#include +#include + +namespace bp = boost::python; + +template +bp::dict std_map_to_dict(const std::map& map) { + bp::dict dictionnary; + for (auto const& x : map) { + dictionnary[x.first] = x.second; + } + return dictionnary; +} + +template +std::map copy(const std::map& map) { + std::map out = map; + return out; +} + +BOOST_PYTHON_MODULE(std_map) { + eigenpy::enableEigenPy(); + + eigenpy::python::StdMapPythonVisitor< + std::string, double, std::less, + std::allocator >, + true>::expose("StdMap_Double"); + + bp::def("std_map_to_dict", std_map_to_dict); + bp::def("copy", copy); +} From fb926bdb769b1cdaaa052476c495a1cdb451f130 Mon Sep 17 00:00:00 2001 From: Justin Carpentier Date: Fri, 30 Aug 2024 15:35:16 +0200 Subject: [PATCH 2/3] changelog: sync --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9782887e..e90917d1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Fix function signature on Windows ([#494](https://github.com/stack-of-tasks/eigenpy/pull/494)) +### Added +- Add bp::dist to std::map converter ([#499](https://github.com/stack-of-tasks/eigenpy/pull/499)) + ## [3.8.1] - 2024-08-25 ### Fixed From ebfacfd74a1c89c349a5c88193be53e8c47a41e7 Mon Sep 17 00:00:00 2001 From: Justin Carpentier Date: Fri, 30 Aug 2024 20:48:40 +0200 Subject: [PATCH 3/3] Apply review change --- include/eigenpy/std-map.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/eigenpy/std-map.hpp b/include/eigenpy/std-map.hpp index c6e1b0e6e..0d7123577 100644 --- a/include/eigenpy/std-map.hpp +++ b/include/eigenpy/std-map.hpp @@ -6,8 +6,9 @@ #ifndef __eigenpy_utils_map_hpp__ #define __eigenpy_utils_map_hpp__ -#include #include "eigenpy/pickle-vector.hpp" + +#include #include #include #include