Skip to content

Commit 5e2e7b7

Browse files
authored
Merge pull request #357 from ManifoldFR/wjallet/bind-boost-optional
Add util to expose optional types
2 parents 8c25238 + f4952d5 commit 5e2e7b7

File tree

7 files changed

+447
-0
lines changed

7 files changed

+447
-0
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ set(${PROJECT_NAME}_HEADERS
163163
include/eigenpy/register.hpp
164164
include/eigenpy/std-map.hpp
165165
include/eigenpy/std-vector.hpp
166+
include/eigenpy/optional.hpp
166167
include/eigenpy/pickle-vector.hpp
167168
include/eigenpy/stride.hpp
168169
include/eigenpy/tensor/eigen-from-python.hpp

include/eigenpy/optional.hpp

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/// Copyright (c) 2023 CNRS INRIA
2+
/// Definitions for exposing boost::optional<T> types.
3+
/// Also works with std::optional.
4+
5+
#ifndef __eigenpy_optional_hpp__
6+
#define __eigenpy_optional_hpp__
7+
8+
#include "eigenpy/fwd.hpp"
9+
#include "eigenpy/eigen-from-python.hpp"
10+
#include <boost/optional.hpp>
11+
#ifdef EIGENPY_WITH_CXX17_SUPPORT
12+
#include <optional>
13+
#endif
14+
15+
#ifndef EIGENPY_DEFAULT_OPTIONAL
16+
#define EIGENPY_DEFAULT_OPTIONAL boost::optional
17+
#endif
18+
19+
namespace boost {
20+
namespace python {
21+
namespace converter {
22+
23+
template <typename T>
24+
struct expected_pytype_for_arg<boost::optional<T> >
25+
: expected_pytype_for_arg<T> {};
26+
27+
#ifdef EIGENPY_WITH_CXX17_SUPPORT
28+
template <typename T>
29+
struct expected_pytype_for_arg<std::optional<T> > : expected_pytype_for_arg<T> {
30+
};
31+
#endif
32+
33+
} // namespace converter
34+
} // namespace python
35+
} // namespace boost
36+
37+
namespace eigenpy {
38+
namespace detail {
39+
40+
/// Helper struct to decide which type is the "none" type for a specific
41+
/// optional<T> implementation.
42+
template <template <typename> class OptionalTpl>
43+
struct nullopt_helper {};
44+
45+
template <>
46+
struct nullopt_helper<boost::optional> {
47+
typedef boost::none_t type;
48+
static type value() { return boost::none; }
49+
};
50+
51+
#ifdef EIGENPY_WITH_CXX17_SUPPORT
52+
template <>
53+
struct nullopt_helper<std::optional> {
54+
typedef std::nullopt_t type;
55+
static type value() { return std::nullopt; }
56+
};
57+
#endif
58+
59+
template <typename T,
60+
template <typename> class OptionalTpl = EIGENPY_DEFAULT_OPTIONAL>
61+
struct OptionalToPython {
62+
static PyObject *convert(const OptionalTpl<T> &obj) {
63+
if (obj)
64+
return bp::incref(bp::object(*obj).ptr());
65+
else {
66+
return bp::incref(bp::object().ptr()); // None
67+
}
68+
}
69+
70+
static PyTypeObject const *get_pytype() {
71+
return bp::converter::registered_pytype<T>::get_pytype();
72+
}
73+
74+
static void registration() {
75+
bp::to_python_converter<OptionalTpl<T>, OptionalToPython, true>();
76+
}
77+
};
78+
79+
template <typename T,
80+
template <typename> class OptionalTpl = EIGENPY_DEFAULT_OPTIONAL>
81+
struct OptionalFromPython {
82+
static void *convertible(PyObject *obj_ptr);
83+
84+
static void construct(PyObject *obj_ptr,
85+
bp::converter::rvalue_from_python_stage1_data *memory);
86+
87+
static void registration();
88+
};
89+
90+
template <typename T, template <typename> class OptionalTpl>
91+
void *OptionalFromPython<T, OptionalTpl>::convertible(PyObject *obj_ptr) {
92+
if (obj_ptr == Py_None) {
93+
return obj_ptr;
94+
}
95+
bp::extract<T> bp_obj(obj_ptr);
96+
if (!bp_obj.check())
97+
return 0;
98+
else
99+
return obj_ptr;
100+
}
101+
102+
template <typename T, template <typename> class OptionalTpl>
103+
void OptionalFromPython<T, OptionalTpl>::construct(
104+
PyObject *obj_ptr, bp::converter::rvalue_from_python_stage1_data *memory) {
105+
// create storage
106+
using rvalue_storage_t =
107+
bp::converter::rvalue_from_python_storage<OptionalTpl<T> >;
108+
void *storage =
109+
reinterpret_cast<rvalue_storage_t *>(reinterpret_cast<void *>(memory))
110+
->storage.bytes;
111+
112+
if (obj_ptr == Py_None) {
113+
new (storage) OptionalTpl<T>(nullopt_helper<OptionalTpl>::value());
114+
} else {
115+
const T value = bp::extract<T>(obj_ptr);
116+
new (storage) OptionalTpl<T>(value);
117+
}
118+
119+
memory->convertible = storage;
120+
}
121+
122+
template <typename T, template <typename> class OptionalTpl>
123+
void OptionalFromPython<T, OptionalTpl>::registration() {
124+
bp::converter::registry::push_back(
125+
&convertible, &construct, bp::type_id<OptionalTpl<T> >(),
126+
bp::converter::expected_pytype_for_arg<OptionalTpl<T> >::get_pytype);
127+
}
128+
129+
} // namespace detail
130+
131+
/// Register converters for the type `optional<T>` to Python.
132+
/// By default \tparam optional is `EIGENPY_DEFAULT_OPTIONAL`.
133+
template <typename T,
134+
template <typename> class OptionalTpl = EIGENPY_DEFAULT_OPTIONAL>
135+
struct OptionalConverter {
136+
static void registration() {
137+
detail::OptionalToPython<T, OptionalTpl>::registration();
138+
detail::OptionalFromPython<T, OptionalTpl>::registration();
139+
}
140+
};
141+
142+
} // namespace eigenpy
143+
144+
#endif // __eigenpy_optional_hpp__

unittest/CMakeLists.txt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,28 @@ endif()
4040
add_lib_unit_test(std_vector)
4141
add_lib_unit_test(user_struct)
4242

43+
function(config_bind_optional tagname opttype)
44+
set(MODNAME bind_optional_${tagname})
45+
set(OPTIONAL ${opttype})
46+
configure_file(bind_optional.cpp.in ${MODNAME}.cpp)
47+
48+
set(py_file test_optional_${tagname}.py)
49+
configure_file(python/test_optional.py.in
50+
${CMAKE_CURRENT_SOURCE_DIR}/python/${py_file})
51+
add_lib_unit_test(${MODNAME})
52+
message(
53+
STATUS
54+
"Adding unit test py-optional-${tagname} with file ${py_file} and module ${MODNAME}"
55+
)
56+
add_python_unit_test("py-optional-${tagname}" "unittest/python/${py_file}"
57+
"unittest")
58+
endfunction()
59+
60+
config_bind_optional(boost "boost::optional")
61+
if(CMAKE_CXX_STANDARD GREATER 14 AND CMAKE_CXX_STANDARD LESS 98)
62+
config_bind_optional(std "std::optional")
63+
endif()
64+
4365
add_python_unit_test("py-matrix" "unittest/python/test_matrix.py" "unittest")
4466

4567
add_python_unit_test("py-tensor" "unittest/python/test_tensor.py" "unittest")

unittest/bind_optional.cpp.in

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
///
2+
/// Copyright (c) 2023 CNRS INRIA
3+
///
4+
5+
#include "eigenpy/eigenpy.hpp"
6+
#include "eigenpy/optional.hpp"
7+
#ifdef EIGENPY_WITH_CXX17_SUPPORT
8+
#include <optional>
9+
#endif
10+
11+
#cmakedefine OPTIONAL @OPTIONAL@
12+
13+
typedef eigenpy::detail::nullopt_helper<OPTIONAL> none_helper;
14+
static auto OPT_NONE = none_helper::value();
15+
typedef OPTIONAL<double> opt_dbl;
16+
17+
struct mystruct {
18+
OPTIONAL<int> a;
19+
opt_dbl b;
20+
OPTIONAL<std::string> msg{"i am struct"};
21+
mystruct() : a(OPT_NONE), b(OPT_NONE) {}
22+
mystruct(int a, const opt_dbl &b = OPT_NONE) : a(a), b(b) {}
23+
};
24+
25+
OPTIONAL<int> none_if_zero(int i) {
26+
if (i == 0)
27+
return OPT_NONE;
28+
else
29+
return i;
30+
}
31+
32+
OPTIONAL<mystruct> create_if_true(bool flag, opt_dbl b = OPT_NONE) {
33+
if (flag) {
34+
return mystruct(0, b);
35+
} else {
36+
return OPT_NONE;
37+
}
38+
}
39+
40+
OPTIONAL<Eigen::MatrixXd> random_mat_if_true(bool flag) {
41+
if (flag)
42+
return Eigen::MatrixXd(Eigen::MatrixXd::Random(4, 4));
43+
else
44+
return OPT_NONE;
45+
}
46+
47+
BOOST_PYTHON_MODULE(@MODNAME@) {
48+
using namespace eigenpy;
49+
OptionalConverter<int, OPTIONAL>::registration();
50+
OptionalConverter<double, OPTIONAL>::registration();
51+
OptionalConverter<std::string, OPTIONAL>::registration();
52+
OptionalConverter<mystruct, OPTIONAL>::registration();
53+
OptionalConverter<Eigen::MatrixXd, OPTIONAL>::registration();
54+
enableEigenPy();
55+
56+
bp::class_<mystruct>("mystruct", bp::no_init)
57+
.def(bp::init<>(bp::args("self")))
58+
.def(bp::init<int, bp::optional<const opt_dbl &> >(
59+
bp::args("self", "a", "b")))
60+
.add_property(
61+
"a",
62+
bp::make_getter(&mystruct::a,
63+
bp::return_value_policy<bp::return_by_value>()),
64+
bp::make_setter(&mystruct::a))
65+
.add_property(
66+
"b",
67+
bp::make_getter(&mystruct::b,
68+
bp::return_value_policy<bp::return_by_value>()),
69+
bp::make_setter(&mystruct::b))
70+
.add_property(
71+
"msg",
72+
bp::make_getter(&mystruct::msg,
73+
bp::return_value_policy<bp::return_by_value>()),
74+
bp::make_setter(&mystruct::msg));
75+
76+
bp::def("none_if_zero", none_if_zero, bp::args("i"));
77+
bp::def("create_if_true", create_if_true, bp::args("flag", "b"));
78+
bp::def("random_mat_if_true", random_mat_if_true, bp::args("flag"));
79+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import importlib
2+
3+
bind_optional = importlib.import_module("@MODNAME@")
4+
5+
6+
def test_none_if_zero():
7+
x = bind_optional.none_if_zero(0)
8+
y = bind_optional.none_if_zero(-1)
9+
assert x is None
10+
assert y == -1
11+
12+
13+
def test_struct_ctors():
14+
# test struct ctors
15+
16+
struct = bind_optional.mystruct()
17+
assert struct.a is None
18+
assert struct.b is None
19+
assert struct.msg == "i am struct"
20+
21+
## no 2nd arg automatic overload using bp::optional
22+
struct = bind_optional.mystruct(2)
23+
assert struct.a == 2
24+
assert struct.b is None
25+
26+
struct = bind_optional.mystruct(13, -1.0)
27+
assert struct.a == 13
28+
assert struct.b == -1.0
29+
30+
31+
def test_struct_setters():
32+
struct = bind_optional.mystruct()
33+
struct.a = 1
34+
assert struct.a == 1
35+
36+
struct.b = -3.14
37+
assert struct.b == -3.14
38+
39+
# set to None
40+
struct.a = None
41+
struct.b = None
42+
struct.msg = None
43+
assert struct.a is None
44+
assert struct.b is None
45+
assert struct.msg is None
46+
47+
48+
def test_factory():
49+
struct = bind_optional.create_if_true(False, None)
50+
assert struct is None
51+
struct = bind_optional.create_if_true(True, None)
52+
assert struct.a == 0
53+
assert struct.b is None
54+
55+
56+
def test_random_mat():
57+
M = bind_optional.random_mat_if_true(False)
58+
assert M is None
59+
M = bind_optional.random_mat_if_true(True)
60+
assert M.shape == (4, 4)
61+
62+
63+
test_none_if_zero()
64+
test_struct_ctors()
65+
test_struct_setters()
66+
test_factory()
67+
test_random_mat()
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import importlib
2+
3+
bind_optional = importlib.import_module("bind_optional_boost")
4+
5+
6+
def test_none_if_zero():
7+
x = bind_optional.none_if_zero(0)
8+
y = bind_optional.none_if_zero(-1)
9+
assert x is None
10+
assert y == -1
11+
12+
13+
def test_struct_ctors():
14+
# test struct ctors
15+
16+
struct = bind_optional.mystruct()
17+
assert struct.a is None
18+
assert struct.b is None
19+
assert struct.msg == "i am struct"
20+
21+
## no 2nd arg automatic overload using bp::optional
22+
struct = bind_optional.mystruct(2)
23+
assert struct.a == 2
24+
assert struct.b is None
25+
26+
struct = bind_optional.mystruct(13, -1.0)
27+
assert struct.a == 13
28+
assert struct.b == -1.0
29+
30+
31+
def test_struct_setters():
32+
struct = bind_optional.mystruct()
33+
struct.a = 1
34+
assert struct.a == 1
35+
36+
struct.b = -3.14
37+
assert struct.b == -3.14
38+
39+
# set to None
40+
struct.a = None
41+
struct.b = None
42+
struct.msg = None
43+
assert struct.a is None
44+
assert struct.b is None
45+
assert struct.msg is None
46+
47+
48+
def test_factory():
49+
struct = bind_optional.create_if_true(False, None)
50+
assert struct is None
51+
struct = bind_optional.create_if_true(True, None)
52+
assert struct.a == 0
53+
assert struct.b is None
54+
55+
56+
def test_random_mat():
57+
M = bind_optional.random_mat_if_true(False)
58+
assert M is None
59+
M = bind_optional.random_mat_if_true(True)
60+
assert M.shape == (4, 4)
61+
62+
63+
test_none_if_zero()
64+
test_struct_ctors()
65+
test_struct_setters()
66+
test_factory()
67+
test_random_mat()

0 commit comments

Comments
 (0)