Skip to content

Commit 3666ff0

Browse files
committed
Add util to expose optional types
+ default is boost::optional<T> + add test for boost::optional
1 parent 8c25238 commit 3666ff0

File tree

5 files changed

+255
-0
lines changed

5 files changed

+255
-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: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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+
12+
#define EIGENPY_DEFAULT_OPTIONAL boost::optional
13+
14+
namespace boost {
15+
namespace python {
16+
namespace converter {
17+
18+
template <typename T>
19+
struct expected_pytype_for_arg<EIGENPY_DEFAULT_OPTIONAL<T> >
20+
: expected_pytype_for_arg<T> {};
21+
22+
} // namespace converter
23+
} // namespace python
24+
} // namespace boost
25+
26+
namespace eigenpy {
27+
namespace detail {
28+
29+
template <typename T,
30+
template <typename> class OptionalTpl = EIGENPY_DEFAULT_OPTIONAL>
31+
struct OptionalToPython {
32+
static PyObject *convert(const OptionalTpl<T> &obj) {
33+
if (obj)
34+
return bp::incref(bp::object(*obj).ptr());
35+
else {
36+
return bp::incref(bp::object().ptr()); // None
37+
}
38+
}
39+
40+
static PyTypeObject const *get_pytype() {
41+
return bp::converter::registered_pytype<T>::get_pytype();
42+
}
43+
44+
static void registration() {
45+
bp::to_python_converter<OptionalTpl<T>, OptionalToPython, true>();
46+
}
47+
};
48+
49+
template <typename T,
50+
template <typename> class OptionalTpl = EIGENPY_DEFAULT_OPTIONAL>
51+
struct OptionalFromPython {
52+
static void *convertible(PyObject *obj_ptr);
53+
54+
static void construct(PyObject *obj_ptr,
55+
bp::converter::rvalue_from_python_stage1_data *memory);
56+
57+
static void registration();
58+
};
59+
60+
template <typename T, template <typename> class OptionalTpl>
61+
void *OptionalFromPython<T, OptionalTpl>::convertible(PyObject *obj_ptr) {
62+
if (obj_ptr == Py_None) {
63+
return obj_ptr;
64+
}
65+
bp::extract<T> bp_obj(obj_ptr);
66+
if (!bp_obj.check())
67+
return 0;
68+
else
69+
return obj_ptr;
70+
}
71+
72+
template <typename T, template <typename> class OptionalTpl>
73+
void OptionalFromPython<T, OptionalTpl>::construct(
74+
PyObject *obj_ptr, bp::converter::rvalue_from_python_stage1_data *memory) {
75+
// create storage
76+
using rvalue_storage_t =
77+
bp::converter::rvalue_from_python_storage<OptionalTpl<T> >;
78+
void *storage =
79+
reinterpret_cast<rvalue_storage_t *>(reinterpret_cast<void *>(memory))
80+
->storage.bytes;
81+
82+
if (obj_ptr == Py_None) {
83+
new (storage) OptionalTpl<T>(boost::none);
84+
} else {
85+
const T value = bp::extract<T>(obj_ptr);
86+
new (storage) OptionalTpl<T>(value);
87+
}
88+
89+
memory->convertible = storage;
90+
}
91+
92+
template <typename T, template <typename> class OptionalTpl>
93+
void OptionalFromPython<T, OptionalTpl>::registration() {
94+
bp::converter::registry::push_back(
95+
&convertible, &construct, bp::type_id<OptionalTpl<T> >(),
96+
bp::converter::expected_pytype_for_arg<OptionalTpl<T> >::get_pytype);
97+
}
98+
99+
} // namespace detail
100+
101+
/// Register converters for the type `optional<T>` to Python.
102+
/// By default \tparam optional is `EIGENPY_DEFAULT_OPTIONAL`.
103+
template <typename T,
104+
template <typename> class OptionalTpl = EIGENPY_DEFAULT_OPTIONAL>
105+
struct OptionalConverter {
106+
static void registration() {
107+
detail::OptionalToPython<T, OptionalTpl>::registration();
108+
detail::OptionalFromPython<T, OptionalTpl>::registration();
109+
}
110+
};
111+
112+
} // namespace eigenpy
113+
114+
#endif // __eigenpy_optional_hpp__

unittest/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ if(NOT NUMPY_WITH_BROKEN_UFUNC_SUPPORT)
3939
endif()
4040
add_lib_unit_test(std_vector)
4141
add_lib_unit_test(user_struct)
42+
add_lib_unit_test(bind_optional)
4243

4344
add_python_unit_test("py-matrix" "unittest/python/test_matrix.py" "unittest")
4445

@@ -97,3 +98,6 @@ set_tests_properties("py-std-vector" PROPERTIES DEPENDS ${PYWRAP})
9798
add_python_unit_test("py-user-struct" "unittest/python/test_user_struct.py"
9899
"python;unittest")
99100
set_tests_properties("py-std-vector" PROPERTIES DEPENDS ${PYWRAP})
101+
102+
add_python_unit_test("py-optional" "unittest/python/test_optional.py"
103+
"unittest")

unittest/bind_optional.cpp

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#include "eigenpy/eigenpy.hpp"
2+
#include "eigenpy/optional.hpp"
3+
4+
#define OPTIONAL boost::optional
5+
#define OPT_NONE boost::none
6+
7+
using opt_dbl = OPTIONAL<double>;
8+
9+
struct mystruct {
10+
OPTIONAL<int> a;
11+
opt_dbl b;
12+
OPTIONAL<std::string> msg{"i am struct"};
13+
mystruct() : a(OPT_NONE), b(boost::none) {}
14+
mystruct(int a, const opt_dbl &b = OPT_NONE) : a(a), b(b) {}
15+
};
16+
17+
OPTIONAL<int> none_if_zero(int i) {
18+
if (i == 0)
19+
return OPT_NONE;
20+
else
21+
return i;
22+
}
23+
24+
OPTIONAL<mystruct> create_if_true(bool flag, opt_dbl b = OPT_NONE) {
25+
if (flag) {
26+
return mystruct(0, b);
27+
} else {
28+
return OPT_NONE;
29+
}
30+
}
31+
32+
OPTIONAL<Eigen::MatrixXd> random_mat_if_true(bool flag) {
33+
if (flag)
34+
return Eigen::MatrixXd(Eigen::MatrixXd::Random(4, 4));
35+
else
36+
return OPT_NONE;
37+
}
38+
39+
BOOST_PYTHON_MODULE(bind_optional) {
40+
using namespace eigenpy;
41+
OptionalConverter<int>::registration();
42+
OptionalConverter<double>::registration();
43+
OptionalConverter<std::string>::registration();
44+
OptionalConverter<mystruct>::registration();
45+
OptionalConverter<Eigen::MatrixXd>::registration();
46+
enableEigenPy();
47+
48+
bp::class_<mystruct>("mystruct", bp::no_init)
49+
.def(bp::init<>(bp::args("self")))
50+
.def(bp::init<int, bp::optional<const opt_dbl &> >(
51+
bp::args("self", "a", "b")))
52+
.add_property(
53+
"a",
54+
bp::make_getter(&mystruct::a,
55+
bp::return_value_policy<bp::return_by_value>()),
56+
bp::make_setter(&mystruct::a))
57+
.add_property(
58+
"b",
59+
bp::make_getter(&mystruct::b,
60+
bp::return_value_policy<bp::return_by_value>()),
61+
bp::make_setter(&mystruct::b))
62+
.add_property(
63+
"msg",
64+
bp::make_getter(&mystruct::msg,
65+
bp::return_value_policy<bp::return_by_value>()),
66+
bp::make_setter(&mystruct::msg));
67+
68+
bp::def("none_if_zero", none_if_zero, bp::args("i"));
69+
bp::def("create_if_true", create_if_true, bp::args("flag", "b"));
70+
bp::def("random_mat_if_true", random_mat_if_true, bp::args("flag"));
71+
}

unittest/python/test_optional.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import bind_optional
2+
3+
4+
def test_none_if_zero():
5+
x = bind_optional.none_if_zero(0)
6+
y = bind_optional.none_if_zero(-1)
7+
assert x is None
8+
assert y == -1
9+
10+
11+
def test_struct_ctors():
12+
# test struct ctors
13+
14+
struct = bind_optional.mystruct()
15+
assert struct.a is None
16+
assert struct.b is None
17+
assert struct.msg == "i am struct"
18+
19+
## no 2nd arg automatic overload using bp::optional
20+
struct = bind_optional.mystruct(2)
21+
assert struct.a == 2
22+
assert struct.b is None
23+
24+
struct = bind_optional.mystruct(13, -1.0)
25+
assert struct.a == 13
26+
assert struct.b == -1.0
27+
28+
29+
def test_struct_setters():
30+
struct = bind_optional.mystruct()
31+
struct.a = 1
32+
assert struct.a == 1
33+
34+
struct.b = -3.14
35+
assert struct.b == -3.14
36+
37+
# set to None
38+
struct.a = None
39+
struct.b = None
40+
struct.msg = None
41+
assert struct.a is None
42+
assert struct.b is None
43+
assert struct.msg is None
44+
45+
46+
def test_factory():
47+
struct = bind_optional.create_if_true(False, None)
48+
assert struct is None
49+
struct = bind_optional.create_if_true(True, None)
50+
assert struct.a == 0
51+
assert struct.b is None
52+
53+
54+
def test_random_mat():
55+
M = bind_optional.random_mat_if_true(False)
56+
assert M is None
57+
M = bind_optional.random_mat_if_true(True)
58+
assert M.shape == (4, 4)
59+
60+
61+
if __name__ == "__main__":
62+
import pytest
63+
import sys
64+
65+
sys.exit(pytest.main(sys.argv))

0 commit comments

Comments
 (0)