diff --git a/CHANGELOG.md b/CHANGELOG.md index c9782887e8ac4221f47dea30a621894a38fcdc8c..e90917d1efbdbbcb4b5f821b3d0337a1f91f397b 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 diff --git a/include/eigenpy/std-map.hpp b/include/eigenpy/std-map.hpp index 9b010e900426ecd31849bcbd9cffd5c9b9e6e777..0d7123577b1a337f5aefe7fd372b6c8ec8209385 100644 --- a/include/eigenpy/std-map.hpp +++ b/include/eigenpy/std-map.hpp @@ -6,7 +6,13 @@ #ifndef __eigenpy_utils_map_hpp__ #define __eigenpy_utils_map_hpp__ +#include "eigenpy/pickle-vector.hpp" + +#include <boost/python/suite/indexing/map_indexing_suite.hpp> +#include <boost/python/stl_iterator.hpp> #include <boost/python/suite/indexing/map_indexing_suite.hpp> +#include <boost/python/to_python_converter.hpp> +#include <map> namespace eigenpy { namespace details { @@ -31,31 +37,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<data_type&, - bp::detail::make_reference_holder> + 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<key_type const&> i(i_); + boost::python::extract<key_type const&> i(i_); if (i.check()) { return i(); } else { - bp::extract<key_type> i(i_); + boost::python::extract<key_type> 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 <typename Container> +struct PickleMap : public PickleVector<Container> { + static void setstate(bp::object op, bp::tuple tup) { + Container& o = bp::extract<Container&>(op)(); + bp::stl_input_iterator<typename Container::value_type> 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 <typename Container> +struct dict_to_map { + static void register_converter() { + bp::converter::registry::push_back(&dict_to_map::convertible, + &dict_to_map::construct, + bp::type_id<Container>()); + } + + /// 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<Container> storage_type; + void* storage = reinterpret_cast<storage_type*>(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<Container*>(storage))); + bp::list keys(dict.keys()); + int keycount(static_cast<int>(bp::len(keys))); + for (int i = 0; i < keycount; ++i) { + // get the key + bp::object keyobj(keys[i]); + bp::extract<typename Container::key_type> 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<typename Container::mapped_type> 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<T>. + * @param[in] Compare Type for the Compare in std::map<T,Compare,Allocator>. + * @param[in] Allocator Type for the Allocator in + * std::map<T,Compare,Allocator>. + * @param[in] NoProxy When set to false, the elements will be copied when + * returned to Python. + */ +template <class Key, class T, class Compare = std::less<Key>, + class Allocator = std::allocator<std::pair<const Key, T> >, + bool NoProxy = false> +struct StdMapPythonVisitor + : public bp::map_indexing_suite< + typename std::map<Key, T, Compare, Allocator>, NoProxy>, + public dict_to_map<std::map<Key, T, Compare, Allocator> > { + typedef std::map<Key, T, Compare, Allocator> Container; + typedef dict_to_map<Container> FromPythonDictConverter; + + static void expose(const std::string& class_name, + const std::string& doc_string = "") { + namespace bp = bp; + + bp::class_<Container>(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<Container>()); + // 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 a65ac2e0287aec6c051f07e046c959a37ee3729a..2c1eec31b35a58ef3927ede297b7619d4b60025c 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 0000000000000000000000000000000000000000..091027b21b063eb3cfe0e33659a5fc7e96096943 --- /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 0000000000000000000000000000000000000000..5997e39cc82df6deb68ceb355e3613c2cb122b83 --- /dev/null +++ b/unittest/std_map.cpp @@ -0,0 +1,35 @@ +/// @file +/// @copyright Copyright 2023 CNRS INRIA + +#include <eigenpy/eigenpy.hpp> +#include <eigenpy/std-map.hpp> +#include <iostream> + +namespace bp = boost::python; + +template <typename T1> +bp::dict std_map_to_dict(const std::map<std::string, T1>& map) { + bp::dict dictionnary; + for (auto const& x : map) { + dictionnary[x.first] = x.second; + } + return dictionnary; +} + +template <typename T1> +std::map<std::string, T1> copy(const std::map<std::string, T1>& map) { + std::map<std::string, T1> out = map; + return out; +} + +BOOST_PYTHON_MODULE(std_map) { + eigenpy::enableEigenPy(); + + eigenpy::python::StdMapPythonVisitor< + std::string, double, std::less<std::string>, + std::allocator<std::pair<const std::string, double> >, + true>::expose("StdMap_Double"); + + bp::def("std_map_to_dict", std_map_to_dict<double>); + bp::def("copy", copy<double>); +}