Skip to content

Commit 9608a04

Browse files
authored
Merge pull request #507 from ManifoldFR/topic/map-enhancements
Map enhancements: add visitor arg to ::expose() with overloads, rename `overload_base_get_item_for_std_map`
2 parents 9dde142 + e03f6b2 commit 9608a04

File tree

8 files changed

+310
-229
lines changed

8 files changed

+310
-229
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- `GenericMapPythonVisitor`/`StdMapPythonVisitor` can now take an extra visitor argument in the `expose()` method, similar to `StdVectorPythonVisitor`
12+
13+
### Changed
14+
15+
- Move `GenericMapPythonVisitor` to its own header `eigenpy/map.hpp`
16+
- Rename `overload_base_get_item_for_std_map` to `overload_base_get_item_for_map`, move out of `eigenpy::details` namespace
17+
- Move `EmptyPythonVisitor` to new header `eigenpy/utils/empty-visitor.hpp`
18+
919
## [3.9.1] - 2024-09-19
1020

1121
### Added

CMakeLists.txt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,12 @@ endif(BUILD_WITH_ACCELERATE_SUPPORT)
170170
# --- INCLUDE ----------------------------------------
171171
# ----------------------------------------------------
172172
set(${PROJECT_NAME}_UTILS_HEADERS
173-
include/eigenpy/utils/scalar-name.hpp include/eigenpy/utils/is-approx.hpp
174-
include/eigenpy/utils/is-aligned.hpp include/eigenpy/utils/traits.hpp
175-
include/eigenpy/utils/python-compat.hpp)
173+
include/eigenpy/utils/scalar-name.hpp
174+
include/eigenpy/utils/is-approx.hpp
175+
include/eigenpy/utils/is-aligned.hpp
176+
include/eigenpy/utils/traits.hpp
177+
include/eigenpy/utils/python-compat.hpp
178+
include/eigenpy/utils/empty-visitor.hpp)
176179

177180
set(${PROJECT_NAME}_SOLVERS_HEADERS
178181
include/eigenpy/solvers/solvers.hpp
@@ -250,6 +253,7 @@ set(${PROJECT_NAME}_HEADERS
250253
include/eigenpy/numpy-map.hpp
251254
include/eigenpy/geometry.hpp
252255
include/eigenpy/geometry-conversion.hpp
256+
include/eigenpy/map.hpp
253257
include/eigenpy/memory.hpp
254258
include/eigenpy/numpy.hpp
255259
include/eigenpy/numpy-allocator.hpp

include/eigenpy/map.hpp

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/// Copyright (c) 2016-2024 CNRS INRIA
2+
/// This file was originally taken from Pinocchio (header
3+
/// <pinocchio/bindings/python/utils/std-vector.hpp>)
4+
///
5+
6+
#ifndef __eigenpy_map_hpp__
7+
#define __eigenpy_map_hpp__
8+
9+
#include "eigenpy/pickle-vector.hpp"
10+
#include "eigenpy/registration.hpp"
11+
#include "eigenpy/utils/empty-visitor.hpp"
12+
13+
#include <boost/python/suite/indexing/map_indexing_suite.hpp>
14+
#include <boost/python/stl_iterator.hpp>
15+
#include <boost/python/to_python_converter.hpp>
16+
17+
namespace eigenpy {
18+
19+
/// \brief Change the behavior of indexing (method __getitem__ in Python).
20+
/// This is suitable e.g. for container of Eigen matrix objects if you want to
21+
/// mutate them.
22+
/// \sa overload_base_get_item_for_std_vector
23+
template <typename Container>
24+
struct overload_base_get_item_for_map
25+
: public boost::python::def_visitor<
26+
overload_base_get_item_for_map<Container> > {
27+
typedef typename Container::value_type value_type;
28+
typedef typename Container::value_type::second_type data_type;
29+
typedef typename Container::key_type key_type;
30+
typedef typename Container::key_type index_type;
31+
32+
template <class Class>
33+
void visit(Class& cl) const {
34+
cl.def("__getitem__", &base_get_item);
35+
}
36+
37+
private:
38+
static boost::python::object base_get_item(
39+
boost::python::back_reference<Container&> container, PyObject* i_) {
40+
index_type idx = convert_index(container.get(), i_);
41+
typename Container::iterator i = container.get().find(idx);
42+
if (i == container.get().end()) {
43+
PyErr_SetString(PyExc_KeyError, "Invalid key");
44+
boost::python::throw_error_already_set();
45+
}
46+
47+
typename boost::python::to_python_indirect<
48+
data_type&, boost::python::detail::make_reference_holder>
49+
convert;
50+
return boost::python::object(boost::python::handle<>(convert(i->second)));
51+
}
52+
53+
static index_type convert_index(Container& /*container*/, PyObject* i_) {
54+
boost::python::extract<key_type const&> i(i_);
55+
if (i.check()) {
56+
return i();
57+
} else {
58+
boost::python::extract<key_type> i(i_);
59+
if (i.check()) return i();
60+
}
61+
62+
PyErr_SetString(PyExc_TypeError, "Invalid index type");
63+
boost::python::throw_error_already_set();
64+
return index_type();
65+
}
66+
};
67+
68+
///////////////////////////////////////////////////////////////////////////////
69+
// The following snippet of code has been taken from the header
70+
// https://github.com/loco-3d/crocoddyl/blob/v2.1.0/bindings/python/crocoddyl/utils/map-converter.hpp
71+
// The Crocoddyl library is written by Carlos Mastalli, Nicolas Mansard and
72+
// Rohan Budhiraja.
73+
///////////////////////////////////////////////////////////////////////////////
74+
75+
namespace bp = boost::python;
76+
77+
/**
78+
* @brief Create a pickle interface for the map type
79+
*
80+
* @param[in] Container Map type to be pickled
81+
* \sa Pickle
82+
*/
83+
template <typename Container>
84+
struct PickleMap : public PickleVector<Container> {
85+
static void setstate(bp::object op, bp::tuple tup) {
86+
Container& o = bp::extract<Container&>(op)();
87+
bp::stl_input_iterator<typename Container::value_type> begin(tup[0]), end;
88+
o.insert(begin, end);
89+
}
90+
};
91+
92+
/// Conversion from dict to map solution proposed in
93+
/// https://stackoverflow.com/questions/6116345/boostpython-possible-to-automatically-convert-from-dict-stdmap
94+
/// This template encapsulates the conversion machinery.
95+
template <typename Container>
96+
struct dict_to_map {
97+
static void register_converter() {
98+
bp::converter::registry::push_back(&dict_to_map::convertible,
99+
&dict_to_map::construct,
100+
bp::type_id<Container>());
101+
}
102+
103+
/// Check if conversion is possible
104+
static void* convertible(PyObject* object) {
105+
// Check if it is a list
106+
if (!PyObject_GetIter(object)) return 0;
107+
return object;
108+
}
109+
110+
/// Perform the conversion
111+
static void construct(PyObject* object,
112+
bp::converter::rvalue_from_python_stage1_data* data) {
113+
// convert the PyObject pointed to by `object` to a bp::dict
114+
bp::handle<> handle(bp::borrowed(object)); // "smart ptr"
115+
bp::dict dict(handle);
116+
117+
// get a pointer to memory into which we construct the map
118+
// this is provided by the Python runtime
119+
typedef bp::converter::rvalue_from_python_storage<Container> storage_type;
120+
void* storage = reinterpret_cast<storage_type*>(data)->storage.bytes;
121+
122+
// placement-new allocate the result
123+
new (storage) Container();
124+
125+
// iterate over the dictionary `dict`, fill up the map `map`
126+
Container& map(*(static_cast<Container*>(storage)));
127+
bp::list keys(dict.keys());
128+
int keycount(static_cast<int>(bp::len(keys)));
129+
for (int i = 0; i < keycount; ++i) {
130+
// get the key
131+
bp::object keyobj(keys[i]);
132+
bp::extract<typename Container::key_type> keyproxy(keyobj);
133+
if (!keyproxy.check()) {
134+
PyErr_SetString(PyExc_KeyError, "Bad key type");
135+
bp::throw_error_already_set();
136+
}
137+
typename Container::key_type key = keyproxy();
138+
139+
// get the corresponding value
140+
bp::object valobj(dict[keyobj]);
141+
bp::extract<typename Container::mapped_type> valproxy(valobj);
142+
if (!valproxy.check()) {
143+
PyErr_SetString(PyExc_ValueError, "Bad value type");
144+
bp::throw_error_already_set();
145+
}
146+
typename Container::mapped_type val = valproxy();
147+
map.emplace(key, val);
148+
}
149+
150+
// remember the location for later
151+
data->convertible = storage;
152+
}
153+
154+
static bp::dict todict(Container& self) {
155+
bp::dict dict;
156+
typename Container::const_iterator it;
157+
for (it = self.begin(); it != self.end(); ++it) {
158+
dict.setdefault(it->first, it->second);
159+
}
160+
return dict;
161+
}
162+
};
163+
164+
/// Policies which handle the non-default constructible case
165+
/// and set_item() using emplace().
166+
template <class Container, bool NoProxy>
167+
struct emplace_set_derived_policies
168+
: bp::map_indexing_suite<
169+
Container, NoProxy,
170+
emplace_set_derived_policies<Container, NoProxy> > {
171+
typedef typename Container::key_type index_type;
172+
typedef typename Container::value_type::second_type data_type;
173+
typedef typename Container::value_type value_type;
174+
using DerivedPolicies =
175+
bp::detail::final_map_derived_policies<Container, NoProxy>;
176+
177+
template <class Class>
178+
static void extension_def(Class& cl) {
179+
// Wrap the map's element (value_type)
180+
std::string elem_name = "map_indexing_suite_";
181+
bp::object class_name(cl.attr("__name__"));
182+
bp::extract<std::string> class_name_extractor(class_name);
183+
elem_name += class_name_extractor();
184+
elem_name += "_entry";
185+
namespace mpl = boost::mpl;
186+
187+
typedef typename mpl::if_<
188+
mpl::and_<boost::is_class<data_type>, mpl::bool_<!NoProxy> >,
189+
bp::return_internal_reference<>, bp::default_call_policies>::type
190+
get_data_return_policy;
191+
192+
bp::class_<value_type>(elem_name.c_str(), bp::no_init)
193+
.def("__repr__", &DerivedPolicies::print_elem)
194+
.def("data", &DerivedPolicies::get_data, get_data_return_policy())
195+
.def("key", &DerivedPolicies::get_key);
196+
}
197+
198+
static void set_item(Container& container, index_type i, data_type const& v) {
199+
container.emplace(i, v);
200+
}
201+
};
202+
203+
/**
204+
* @brief Expose the map-like container, e.g. (std::map).
205+
*
206+
* @param[in] Container Container to expose.
207+
* @param[in] NoProxy When set to false, the elements will be copied when
208+
* returned to Python.
209+
*/
210+
template <class Container, bool NoProxy = false>
211+
struct GenericMapVisitor
212+
: public emplace_set_derived_policies<Container, NoProxy>,
213+
public dict_to_map<Container> {
214+
typedef dict_to_map<Container> FromPythonDictConverter;
215+
216+
template <typename DerivedVisitor>
217+
static void expose(const std::string& class_name,
218+
const std::string& doc_string,
219+
const bp::def_visitor<DerivedVisitor>& visitor) {
220+
namespace bp = bp;
221+
222+
if (!register_symbolic_link_to_registered_type<Container>()) {
223+
bp::class_<Container>(class_name.c_str(), doc_string.c_str())
224+
.def(GenericMapVisitor())
225+
.def("todict", &FromPythonDictConverter::todict, bp::arg("self"),
226+
"Returns the map type as a Python dictionary.")
227+
.def_pickle(PickleMap<Container>())
228+
.def(visitor);
229+
// Register conversion
230+
FromPythonDictConverter::register_converter();
231+
}
232+
}
233+
234+
static void expose(const std::string& class_name,
235+
const std::string& doc_string = "") {
236+
expose(class_name, doc_string, EmptyPythonVisitor());
237+
}
238+
239+
template <typename DerivedVisitor>
240+
static void expose(const std::string& class_name,
241+
const bp::def_visitor<DerivedVisitor>& visitor) {
242+
expose(class_name, "", visitor);
243+
}
244+
};
245+
246+
} // namespace eigenpy
247+
248+
#endif // ifndef __eigenpy_map_hpp__

0 commit comments

Comments
 (0)