diff --git a/CMakeLists.txt b/CMakeLists.txt index d1187c1bfb69fb0a9a50b81dc873da948243d1f0..fe143b1f57c2de58a915fb2bfaa02c77efa01c6d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -163,6 +163,7 @@ set(${PROJECT_NAME}_HEADERS include/eigenpy/register.hpp include/eigenpy/std-map.hpp include/eigenpy/std-vector.hpp + include/eigenpy/optional.hpp include/eigenpy/pickle-vector.hpp include/eigenpy/stride.hpp include/eigenpy/tensor/eigen-from-python.hpp diff --git a/include/eigenpy/optional.hpp b/include/eigenpy/optional.hpp new file mode 100644 index 0000000000000000000000000000000000000000..38f167db75f13b7112fa904ecee787e87dad8825 --- /dev/null +++ b/include/eigenpy/optional.hpp @@ -0,0 +1,114 @@ +/// Copyright (c) 2023 CNRS INRIA +/// Definitions for exposing boost::optional<T> types. +/// Also works with std::optional. + +#ifndef __eigenpy_optional_hpp__ +#define __eigenpy_optional_hpp__ + +#include "eigenpy/fwd.hpp" +#include "eigenpy/eigen-from-python.hpp" +#include <boost/optional.hpp> + +#define EIGENPY_DEFAULT_OPTIONAL boost::optional + +namespace boost { +namespace python { +namespace converter { + +template <typename T> +struct expected_pytype_for_arg<EIGENPY_DEFAULT_OPTIONAL<T> > + : expected_pytype_for_arg<T> {}; + +} // namespace converter +} // namespace python +} // namespace boost + +namespace eigenpy { +namespace detail { + +template <typename T, + template <typename> class OptionalTpl = EIGENPY_DEFAULT_OPTIONAL> +struct OptionalToPython { + static PyObject *convert(const OptionalTpl<T> &obj) { + if (obj) + return bp::incref(bp::object(*obj).ptr()); + else { + return bp::incref(bp::object().ptr()); // None + } + } + + static PyTypeObject const *get_pytype() { + return bp::converter::registered_pytype<T>::get_pytype(); + } + + static void registration() { + bp::to_python_converter<OptionalTpl<T>, OptionalToPython, true>(); + } +}; + +template <typename T, + template <typename> class OptionalTpl = EIGENPY_DEFAULT_OPTIONAL> +struct OptionalFromPython { + static void *convertible(PyObject *obj_ptr); + + static void construct(PyObject *obj_ptr, + bp::converter::rvalue_from_python_stage1_data *memory); + + static void registration(); +}; + +template <typename T, template <typename> class OptionalTpl> +void *OptionalFromPython<T, OptionalTpl>::convertible(PyObject *obj_ptr) { + if (obj_ptr == Py_None) { + return obj_ptr; + } + bp::extract<T> bp_obj(obj_ptr); + if (!bp_obj.check()) + return 0; + else + return obj_ptr; +} + +template <typename T, template <typename> class OptionalTpl> +void OptionalFromPython<T, OptionalTpl>::construct( + PyObject *obj_ptr, bp::converter::rvalue_from_python_stage1_data *memory) { + // create storage + using rvalue_storage_t = + bp::converter::rvalue_from_python_storage<OptionalTpl<T> >; + void *storage = + reinterpret_cast<rvalue_storage_t *>(reinterpret_cast<void *>(memory)) + ->storage.bytes; + + if (obj_ptr == Py_None) { + new (storage) OptionalTpl<T>(boost::none); + } else { + const T value = bp::extract<T>(obj_ptr); + new (storage) OptionalTpl<T>(value); + } + + memory->convertible = storage; +} + +template <typename T, template <typename> class OptionalTpl> +void OptionalFromPython<T, OptionalTpl>::registration() { + bp::converter::registry::push_back( + &convertible, &construct, bp::type_id<OptionalTpl<T> >(), + bp::converter::expected_pytype_for_arg<OptionalTpl<T> >::get_pytype); +} + +} // namespace detail + +/// Register converters for the type `optional<T>` to Python. +/// By default \tparam optional is `EIGENPY_DEFAULT_OPTIONAL`. +template <typename T, + template <typename> class OptionalTpl = EIGENPY_DEFAULT_OPTIONAL> +struct OptionalConverter { + static void registration() { + detail::OptionalToPython<T, OptionalTpl>::registration(); + detail::OptionalFromPython<T, OptionalTpl>::registration(); + } +}; + +} // namespace eigenpy + +#endif // __eigenpy_optional_hpp__ diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index cccdc44ead6ac200929ad5fd9327b182875afd41..9d62420f240338d5926f7f3963234643e514393f 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -39,6 +39,7 @@ if(NOT NUMPY_WITH_BROKEN_UFUNC_SUPPORT) endif() add_lib_unit_test(std_vector) add_lib_unit_test(user_struct) +add_lib_unit_test(bind_optional) add_python_unit_test("py-matrix" "unittest/python/test_matrix.py" "unittest") @@ -97,3 +98,6 @@ set_tests_properties("py-std-vector" PROPERTIES DEPENDS ${PYWRAP}) add_python_unit_test("py-user-struct" "unittest/python/test_user_struct.py" "python;unittest") set_tests_properties("py-std-vector" PROPERTIES DEPENDS ${PYWRAP}) + +add_python_unit_test("py-optional" "unittest/python/test_optional.py" + "unittest") diff --git a/unittest/bind_optional.cpp b/unittest/bind_optional.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6d85795c4306e2f435250f6deffcceb14aba9f33 --- /dev/null +++ b/unittest/bind_optional.cpp @@ -0,0 +1,71 @@ +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/optional.hpp" + +#define OPTIONAL boost::optional +#define OPT_NONE boost::none + +using opt_dbl = OPTIONAL<double>; + +struct mystruct { + OPTIONAL<int> a; + opt_dbl b; + OPTIONAL<std::string> msg{"i am struct"}; + mystruct() : a(OPT_NONE), b(boost::none) {} + mystruct(int a, const opt_dbl &b = OPT_NONE) : a(a), b(b) {} +}; + +OPTIONAL<int> none_if_zero(int i) { + if (i == 0) + return OPT_NONE; + else + return i; +} + +OPTIONAL<mystruct> create_if_true(bool flag, opt_dbl b = OPT_NONE) { + if (flag) { + return mystruct(0, b); + } else { + return OPT_NONE; + } +} + +OPTIONAL<Eigen::MatrixXd> random_mat_if_true(bool flag) { + if (flag) + return Eigen::MatrixXd(Eigen::MatrixXd::Random(4, 4)); + else + return OPT_NONE; +} + +BOOST_PYTHON_MODULE(bind_optional) { + using namespace eigenpy; + OptionalConverter<int>::registration(); + OptionalConverter<double>::registration(); + OptionalConverter<std::string>::registration(); + OptionalConverter<mystruct>::registration(); + OptionalConverter<Eigen::MatrixXd>::registration(); + enableEigenPy(); + + bp::class_<mystruct>("mystruct", bp::no_init) + .def(bp::init<>(bp::args("self"))) + .def(bp::init<int, bp::optional<const opt_dbl &> >( + bp::args("self", "a", "b"))) + .add_property( + "a", + bp::make_getter(&mystruct::a, + bp::return_value_policy<bp::return_by_value>()), + bp::make_setter(&mystruct::a)) + .add_property( + "b", + bp::make_getter(&mystruct::b, + bp::return_value_policy<bp::return_by_value>()), + bp::make_setter(&mystruct::b)) + .add_property( + "msg", + bp::make_getter(&mystruct::msg, + bp::return_value_policy<bp::return_by_value>()), + bp::make_setter(&mystruct::msg)); + + bp::def("none_if_zero", none_if_zero, bp::args("i")); + bp::def("create_if_true", create_if_true, bp::args("flag", "b")); + bp::def("random_mat_if_true", random_mat_if_true, bp::args("flag")); +} diff --git a/unittest/python/test_optional.py b/unittest/python/test_optional.py new file mode 100644 index 0000000000000000000000000000000000000000..d6a29738c2b4c77edfadba79b22d3a6c334fc8e3 --- /dev/null +++ b/unittest/python/test_optional.py @@ -0,0 +1,65 @@ +import bind_optional + + +def test_none_if_zero(): + x = bind_optional.none_if_zero(0) + y = bind_optional.none_if_zero(-1) + assert x is None + assert y == -1 + + +def test_struct_ctors(): + # test struct ctors + + struct = bind_optional.mystruct() + assert struct.a is None + assert struct.b is None + assert struct.msg == "i am struct" + + ## no 2nd arg automatic overload using bp::optional + struct = bind_optional.mystruct(2) + assert struct.a == 2 + assert struct.b is None + + struct = bind_optional.mystruct(13, -1.0) + assert struct.a == 13 + assert struct.b == -1.0 + + +def test_struct_setters(): + struct = bind_optional.mystruct() + struct.a = 1 + assert struct.a == 1 + + struct.b = -3.14 + assert struct.b == -3.14 + + # set to None + struct.a = None + struct.b = None + struct.msg = None + assert struct.a is None + assert struct.b is None + assert struct.msg is None + + +def test_factory(): + struct = bind_optional.create_if_true(False, None) + assert struct is None + struct = bind_optional.create_if_true(True, None) + assert struct.a == 0 + assert struct.b is None + + +def test_random_mat(): + M = bind_optional.random_mat_if_true(False) + assert M is None + M = bind_optional.random_mat_if_true(True) + assert M.shape == (4, 4) + + +if __name__ == "__main__": + import pytest + import sys + + sys.exit(pytest.main(sys.argv))