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..8394ba7244ad43c5f548a241979338f24fafa8af --- /dev/null +++ b/include/eigenpy/optional.hpp @@ -0,0 +1,144 @@ +/// 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> +#ifdef EIGENPY_WITH_CXX17_SUPPORT +#include <optional> +#endif + +#ifndef EIGENPY_DEFAULT_OPTIONAL +#define EIGENPY_DEFAULT_OPTIONAL boost::optional +#endif + +namespace boost { +namespace python { +namespace converter { + +template <typename T> +struct expected_pytype_for_arg<boost::optional<T> > + : expected_pytype_for_arg<T> {}; + +#ifdef EIGENPY_WITH_CXX17_SUPPORT +template <typename T> +struct expected_pytype_for_arg<std::optional<T> > : expected_pytype_for_arg<T> { +}; +#endif + +} // namespace converter +} // namespace python +} // namespace boost + +namespace eigenpy { +namespace detail { + +/// Helper struct to decide which type is the "none" type for a specific +/// optional<T> implementation. +template <template <typename> class OptionalTpl> +struct nullopt_helper {}; + +template <> +struct nullopt_helper<boost::optional> { + typedef boost::none_t type; + static type value() { return boost::none; } +}; + +#ifdef EIGENPY_WITH_CXX17_SUPPORT +template <> +struct nullopt_helper<std::optional> { + typedef std::nullopt_t type; + static type value() { return std::nullopt; } +}; +#endif + +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>(nullopt_helper<OptionalTpl>::value()); + } 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..9736ded071268fcf0453927f8d55d9fc69c96945 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -40,6 +40,28 @@ endif() add_lib_unit_test(std_vector) add_lib_unit_test(user_struct) +function(config_bind_optional tagname opttype) + set(MODNAME bind_optional_${tagname}) + set(OPTIONAL ${opttype}) + configure_file(bind_optional.cpp.in ${MODNAME}.cpp) + + set(py_file test_optional_${tagname}.py) + configure_file(python/test_optional.py.in + ${CMAKE_CURRENT_SOURCE_DIR}/python/${py_file}) + add_lib_unit_test(${MODNAME}) + message( + STATUS + "Adding unit test py-optional-${tagname} with file ${py_file} and module ${MODNAME}" + ) + add_python_unit_test("py-optional-${tagname}" "unittest/python/${py_file}" + "unittest") +endfunction() + +config_bind_optional(boost "boost::optional") +if(CMAKE_CXX_STANDARD GREATER 14 AND CMAKE_CXX_STANDARD LESS 98) + config_bind_optional(std "std::optional") +endif() + add_python_unit_test("py-matrix" "unittest/python/test_matrix.py" "unittest") add_python_unit_test("py-tensor" "unittest/python/test_tensor.py" "unittest") diff --git a/unittest/bind_optional.cpp.in b/unittest/bind_optional.cpp.in new file mode 100644 index 0000000000000000000000000000000000000000..f71773d6505fa7dd5c76174c8b9ac6db5359ab9e --- /dev/null +++ b/unittest/bind_optional.cpp.in @@ -0,0 +1,79 @@ +/// +/// Copyright (c) 2023 CNRS INRIA +/// + +#include "eigenpy/eigenpy.hpp" +#include "eigenpy/optional.hpp" +#ifdef EIGENPY_WITH_CXX17_SUPPORT +#include <optional> +#endif + +#cmakedefine OPTIONAL @OPTIONAL@ + +typedef eigenpy::detail::nullopt_helper<OPTIONAL> none_helper; +static auto OPT_NONE = none_helper::value(); +typedef OPTIONAL<double> opt_dbl; + +struct mystruct { + OPTIONAL<int> a; + opt_dbl b; + OPTIONAL<std::string> msg{"i am struct"}; + mystruct() : a(OPT_NONE), b(OPT_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(@MODNAME@) { + using namespace eigenpy; + OptionalConverter<int, OPTIONAL>::registration(); + OptionalConverter<double, OPTIONAL>::registration(); + OptionalConverter<std::string, OPTIONAL>::registration(); + OptionalConverter<mystruct, OPTIONAL>::registration(); + OptionalConverter<Eigen::MatrixXd, OPTIONAL>::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.in b/unittest/python/test_optional.py.in new file mode 100644 index 0000000000000000000000000000000000000000..bd5e085ec076bf2a5ce3ed8645f3b3624b95b494 --- /dev/null +++ b/unittest/python/test_optional.py.in @@ -0,0 +1,67 @@ +import importlib + +bind_optional = importlib.import_module("@MODNAME@") + + +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) + + +test_none_if_zero() +test_struct_ctors() +test_struct_setters() +test_factory() +test_random_mat() diff --git a/unittest/python/test_optional_boost.py b/unittest/python/test_optional_boost.py new file mode 100644 index 0000000000000000000000000000000000000000..fc818739d90e89be9d41f8f26c0f1f560ef5f4b7 --- /dev/null +++ b/unittest/python/test_optional_boost.py @@ -0,0 +1,67 @@ +import importlib + +bind_optional = importlib.import_module("bind_optional_boost") + + +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) + + +test_none_if_zero() +test_struct_ctors() +test_struct_setters() +test_factory() +test_random_mat() diff --git a/unittest/python/test_optional_std.py b/unittest/python/test_optional_std.py new file mode 100644 index 0000000000000000000000000000000000000000..69949a44f46b3f2ea010af24a3478d0a67fb8e5f --- /dev/null +++ b/unittest/python/test_optional_std.py @@ -0,0 +1,67 @@ +import importlib + +bind_optional = importlib.import_module("bind_optional_std") + + +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) + + +test_none_if_zero() +test_struct_ctors() +test_struct_setters() +test_factory() +test_random_mat()