diff --git a/.github/workflows/macos-linux-conda.yml b/.github/workflows/macos-linux-conda.yml index edc178441..3f4f23f66 100644 --- a/.github/workflows/macos-linux-conda.yml +++ b/.github/workflows/macos-linux-conda.yml @@ -40,7 +40,7 @@ jobs: with: path: .ccache key: ccache-macos-linux-conda-${{ matrix.os }}-${{ matrix.build_type }}-${{ matrix.cxx_options }}-${{ matrix.python-version }}-${{ github.sha }} - restore-keys: ccache-macos-linux-conda-${{ matrix.os }}-${{ matrix.build_type }}-${{ matrix.python-version }}-${{ matrix.cxx_options }}-${{ matrix.python-version }}- + restore-keys: ccache-macos-linux-conda-${{ matrix.os }}-${{ matrix.build_type }}-${{ matrix.cxx_options }}-${{ matrix.python-version }}- - uses: conda-incubator/setup-miniconda@v3 with: diff --git a/.github/workflows/windows-conda.yml b/.github/workflows/windows-conda.yml index 74d923fd7..b3f8ad815 100644 --- a/.github/workflows/windows-conda.yml +++ b/.github/workflows/windows-conda.yml @@ -34,8 +34,8 @@ jobs: - uses: actions/cache@v3 with: path: .ccache - key: ccache-windows-conda-${{ matrix.os }}-${{ matrix.build_type }}-${{ matrix.cxx_options }}-${{ matrix.python-version }}-${{ github.sha }} - restore-keys: ccache-windows-conda-${{ matrix.os }}-${{ matrix.build_type }}-${{ matrix.cxx_options }}-${{ matrix.python-version }}- + key: ccache-windows-conda-${{ matrix.os }}-${{ github.sha }} + restore-keys: ccache-windows-conda-${{ matrix.os }}- - name: Build Eigenpy shell: cmd /C CALL {0} diff --git a/CHANGELOG.md b/CHANGELOG.md index e568f1ae6..8c936ecb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Support for `Eigen::SparseMatrix` types ([#426](https://github.com/stack-of-tasks/eigenpy/pull/426)) - Support for `boost::variant` types with `VariantConverter` ([#430](https://github.com/stack-of-tasks/eigenpy/pull/430)) - Support for `std::variant` types with `VariantConverter` ([#431](https://github.com/stack-of-tasks/eigenpy/pull/431)) +- Support for `std::unique_ptr` as a return types with `StdUniquePtrCallPolicies` and `boost::python::default_call_policies` ([#433](https://github.com/stack-of-tasks/eigenpy/pull/433)) +- Support for `std::unique_ptr` as an internal reference with `ReturnInternalStdUniquePtr` ([#433](https://github.com/stack-of-tasks/eigenpy/pull/433)) ### Fixed - Fix the issue of missing exposition of Eigen types with __int64 scalar type ([#426](https://github.com/stack-of-tasks/eigenpy/pull/426)) diff --git a/CMakeLists.txt b/CMakeLists.txt index d8d8a79e6..e48c9105e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,7 +104,8 @@ search_for_boost_python(REQUIRED) # ---------------------------------------------------- set(${PROJECT_NAME}_UTILS_HEADERS include/eigenpy/utils/scalar-name.hpp include/eigenpy/utils/is-approx.hpp - include/eigenpy/utils/is-aligned.hpp) + include/eigenpy/utils/is-aligned.hpp include/eigenpy/utils/traits.hpp + include/eigenpy/utils/python-compat.hpp) set(${PROJECT_NAME}_SOLVERS_HEADERS include/eigenpy/solvers/solvers.hpp @@ -167,6 +168,7 @@ set(${PROJECT_NAME}_HEADERS include/eigenpy/scipy-allocator.hpp include/eigenpy/scipy-type.hpp include/eigenpy/variant.hpp + include/eigenpy/std-unique-ptr.hpp include/eigenpy/swig.hpp include/eigenpy/version.hpp) diff --git a/include/eigenpy/eigenpy.hpp b/include/eigenpy/eigenpy.hpp index f4edf090d..97407d349 100644 --- a/include/eigenpy/eigenpy.hpp +++ b/include/eigenpy/eigenpy.hpp @@ -10,6 +10,9 @@ #include "eigenpy/eigen-typedef.hpp" #include "eigenpy/expose.hpp" +/// Custom CallPolicies +#include "eigenpy/std-unique-ptr.hpp" + #define ENABLE_SPECIFIC_MATRIX_TYPE(TYPE) \ ::eigenpy::enableEigenPySpecific(); diff --git a/include/eigenpy/std-unique-ptr.hpp b/include/eigenpy/std-unique-ptr.hpp new file mode 100644 index 000000000..dfc9f5a13 --- /dev/null +++ b/include/eigenpy/std-unique-ptr.hpp @@ -0,0 +1,145 @@ +// +// Copyright (c) 2024 INRIA +// + +#ifndef __eigenpy_utils_std_unique_ptr_hpp__ +#define __eigenpy_utils_std_unique_ptr_hpp__ + +#include "eigenpy/fwd.hpp" +#include "eigenpy/utils/traits.hpp" +#include "eigenpy/utils/python-compat.hpp" + +#include + +#include +#include + +namespace eigenpy { + +namespace details { + +/// Transfer std::unique_ptr ownership to an owning holder +template +typename std::enable_if::value, PyObject*>::type +unique_ptr_to_python(std::unique_ptr&& x) { + typedef bp::objects::pointer_holder, T> holder_t; + if (!x) { + return bp::detail::none(); + } else { + return bp::objects::make_ptr_instance::execute(x); + } +} + +/// Convert and copy the primitive value to python +template +typename std::enable_if::value, PyObject*>::type +unique_ptr_to_python(std::unique_ptr&& x) { + if (!x) { + return bp::detail::none(); + } else { + return bp::to_python_value()(*x); + } +} + +/// std::unique_ptr keep the ownership but a reference to the std::unique_ptr +/// value is created +template +typename std::enable_if::value, PyObject*>::type +internal_unique_ptr_to_python(std::unique_ptr& x) { + if (!x) { + return bp::detail::none(); + } else { + return bp::detail::make_reference_holder::execute(x.get()); + } +} + +/// Convert and copy the primitive value to python +template +typename std::enable_if::value, PyObject*>::type +internal_unique_ptr_to_python(std::unique_ptr& x) { + if (!x) { + return bp::detail::none(); + } else { + return bp::to_python_value()(*x); + } +} + +/// result_converter of StdUniquePtrCallPolicies +struct StdUniquePtrResultConverter { + template + struct apply { + struct type { + typedef typename T::element_type element_type; + + PyObject* operator()(T&& x) const { + return unique_ptr_to_python(std::forward(x)); + } +#ifndef BOOST_PYTHON_NO_PY_SIGNATURES + PyTypeObject const* get_pytype() const { + return bp::to_python_value().get_pytype(); + } +#endif + }; + }; +}; + +/// result_converter of ReturnInternalStdUniquePtr +struct InternalStdUniquePtrConverter { + template + struct apply { + struct type { + typedef typename remove_cvref::type::element_type element_type; + + PyObject* operator()(T x) const { + return internal_unique_ptr_to_python(x); + } +#ifndef BOOST_PYTHON_NO_PY_SIGNATURES + PyTypeObject const* get_pytype() const { + return bp::to_python_value().get_pytype(); + } +#endif + }; + }; +}; + +} // namespace details + +/// CallPolicies to get std::unique_ptr value from a function +/// that return an std::unique_ptr. +/// If the object inside the std::unique_ptr is a class or an union +/// it will be moved. In other case, it will be copied. +struct StdUniquePtrCallPolicies : bp::default_call_policies { + typedef details::StdUniquePtrResultConverter result_converter; +}; + +/// Variant of \see bp::return_internal_reference that extract std::unique_ptr +/// content reference before converting it into a PyObject +struct ReturnInternalStdUniquePtr : bp::return_internal_reference<> { + typedef details::InternalStdUniquePtrConverter result_converter; + + template + static PyObject* postcall(ArgumentPackage const& args_, PyObject* result) { + // Don't run return_internal_reference postcall on primitive type + if (PyInt_Check(result) || PyBool_Check(result) || PyFloat_Check(result) || + PyStr_Check(result) || PyComplex_Check(result)) { + return result; + } + return bp::return_internal_reference<>::postcall(args_, result); + } +}; + +} // namespace eigenpy + +namespace boost { +namespace python { + +/// Specialize to_python_value for std::unique_ptr +template +struct to_python_value&> + : eigenpy::details::StdUniquePtrResultConverter::apply< + std::unique_ptr >::type {}; + +} // namespace python +} // namespace boost + +#endif // ifndef __eigenpy_utils_std_unique_ptr_hpp__ diff --git a/include/eigenpy/ufunc.hpp b/include/eigenpy/ufunc.hpp index cb6695ac9..129438cf1 100644 --- a/include/eigenpy/ufunc.hpp +++ b/include/eigenpy/ufunc.hpp @@ -9,6 +9,7 @@ #include "eigenpy/register.hpp" #include "eigenpy/user-type.hpp" +#include "eigenpy/utils/python-compat.hpp" namespace eigenpy { namespace internal { @@ -207,11 +208,7 @@ void registerCommonUfunc() { const int type_code = Register::getTypeCode(); PyObject *numpy_str; -#if PY_MAJOR_VERSION >= 3 - numpy_str = PyUnicode_FromString("numpy"); -#else - numpy_str = PyString_FromString("numpy"); -#endif + numpy_str = PyStr_FromString("numpy"); PyObject *numpy; numpy = PyImport_Import(numpy_str); Py_DECREF(numpy_str); diff --git a/include/eigenpy/utils/python-compat.hpp b/include/eigenpy/utils/python-compat.hpp new file mode 100644 index 000000000..7ffbc9de4 --- /dev/null +++ b/include/eigenpy/utils/python-compat.hpp @@ -0,0 +1,23 @@ +// +// Copyright (c) 2024 INRIA +// +// + +#ifndef __eigenpy_utils_python_compat_hpp__ +#define __eigenpy_utils_python_compat_hpp__ + +#if PY_MAJOR_VERSION >= 3 + +#define PyInt_Check PyLong_Check + +#define PyStr_Check PyUnicode_Check +#define PyStr_FromString PyUnicode_FromString + +#else + +#define PyStr_Check PyString_Check +#define PyStr_FromString PyString_FromString + +#endif + +#endif // ifndef __eigenpy_utils_python_compat_hpp__ diff --git a/include/eigenpy/utils/traits.hpp b/include/eigenpy/utils/traits.hpp new file mode 100644 index 000000000..b7525dea9 --- /dev/null +++ b/include/eigenpy/utils/traits.hpp @@ -0,0 +1,56 @@ +// +// Copyright (c) 2024 INRIA +// +// + +#ifndef __eigenpy_utils_traits_hpp__ +#define __eigenpy_utils_traits_hpp__ + +#include +#include +#include + +namespace eigenpy { + +namespace details { + +/// Trait to remove const& +template +struct remove_cvref : std::remove_cv::type> { +}; + +/// Trait to detect if T is a class or an union +template +struct is_class_or_union + : std::integral_constant::value || + std::is_union::value> {}; + +/// trait to detect if T is a std::complex managed by Boost Python +template +struct is_python_complex : std::false_type {}; + +/// From boost/python/converter/builtin_converters +template <> +struct is_python_complex > : std::true_type {}; +template <> +struct is_python_complex > : std::true_type {}; +template <> +struct is_python_complex > : std::true_type {}; + +template +struct is_python_primitive_type_helper + : std::integral_constant::value || + std::is_same::value || + std::is_same::value || + is_python_complex::value> {}; + +/// Trait to detect if T is a Python primitive type +template +struct is_python_primitive_type + : is_python_primitive_type_helper::type> {}; + +} // namespace details + +} // namespace eigenpy + +#endif // ifndef __eigenpy_utils_traits_hpp__ diff --git a/include/eigenpy/variant.hpp b/include/eigenpy/variant.hpp index 4028f856b..bf787c3fc 100644 --- a/include/eigenpy/variant.hpp +++ b/include/eigenpy/variant.hpp @@ -6,6 +6,8 @@ #define __eigenpy_utils_variant_hpp__ #include "eigenpy/fwd.hpp" +#include "eigenpy/utils/traits.hpp" +#include "eigenpy/utils/python-compat.hpp" #include #include @@ -146,7 +148,7 @@ struct NumericConvertibleImpl< std::is_integral::value>::type> { static void* convertible(PyObject* obj) { // PyLong return true for bool type - return (PyLong_Check(obj) && !PyBool_Check(obj)) ? obj : nullptr; + return (PyInt_Check(obj) && !PyBool_Check(obj)) ? obj : nullptr; } static PyTypeObject const* expected_pytype() { return &PyLong_Type; } @@ -204,18 +206,6 @@ struct VariantValueToObject : VariantVisitorType { using Base::operator(); }; -/// Trait to detect if T is a class or an union -template -struct is_class_or_union - : std::integral_constant::value || - std::is_union::value> {}; - -/// Trait to remove cvref and call is_class_or_union -template -struct is_class_or_union_remove_cvref - : is_class_or_union::type>::type> {}; - /// Convert {boost,std}::variant alternative reference to a Python /// object. This converter return the alternative reference. The code that /// create the reference holder is taken from \see @@ -231,14 +221,14 @@ struct VariantRefToObject : VariantVisitorType { } template ::value, + typename std::enable_if::value, bool>::type = true> result_type operator()(T t) const { return bp::incref(bp::object(t).ptr()); } template ::value, + typename std::enable_if::value, bool>::type = true> result_type operator()(T& t) const { return bp::detail::make_reference_holder::execute(&t); @@ -312,7 +302,8 @@ struct ReturnInternalVariant : bp::return_internal_reference<> { template static PyObject* postcall(ArgumentPackage const& args_, PyObject* result) { // Don't run return_internal_reference postcall on primitive type - if (PyLong_Check(result) || PyBool_Check(result) || PyFloat_Check(result)) { + if (PyInt_Check(result) || PyBool_Check(result) || PyFloat_Check(result) || + PyStr_Check(result) || PyComplex_Check(result)) { return result; } return bp::return_internal_reference<>::postcall(args_, result); diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index e1356f6d4..8dc425fa9 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -45,6 +45,7 @@ add_lib_unit_test(std_vector) add_lib_unit_test(std_array) add_lib_unit_test(std_pair) add_lib_unit_test(user_struct) +add_lib_unit_test(std_unique_ptr) function(config_test test tagname opttype) set(MODNAME ${test}_${tagname}) @@ -131,17 +132,16 @@ add_python_unit_test("py-std-vector" "unittest/python/test_std_vector.py" set_tests_properties("py-std-vector" PROPERTIES DEPENDS ${PYWRAP}) add_python_unit_test("py-std-array" "unittest/python/test_std_array.py" - "python;unittest") -set_tests_properties("py-std-array" PROPERTIES DEPENDS ${PYWRAP}) + "unittest") add_python_unit_test("py-std-pair" "unittest/python/test_std_pair.py" "unittest") -set_tests_properties("py-std-pair" PROPERTIES DEPENDS ${PYWRAP}) add_python_unit_test("py-user-struct" "unittest/python/test_user_struct.py" - "python;unittest") -set_tests_properties("py-user-struct" PROPERTIES DEPENDS ${PYWRAP}) + "unittest") + +add_python_unit_test("py-std-unique-ptr" + "unittest/python/test_std_unique_ptr.py" "unittest") add_python_unit_test("py-bind-virtual" "unittest/python/test_bind_virtual.py" - "python;unittest") -set_tests_properties("py-bind-virtual" PROPERTIES DEPENDS ${PYWRAP}) + "unittest") diff --git a/unittest/python/test_std_unique_ptr.py b/unittest/python/test_std_unique_ptr.py new file mode 100644 index 000000000..8b56460f3 --- /dev/null +++ b/unittest/python/test_std_unique_ptr.py @@ -0,0 +1,61 @@ +from std_unique_ptr import ( + make_unique_int, + make_unique_v1, + make_unique_null, + make_unique_str, + make_unique_complex, + V1, + UniquePtrHolder, +) + +v = make_unique_int() +assert isinstance(v, int) +assert v == 10 + +v = make_unique_v1() +assert isinstance(v, V1) +assert v.v == 10 + +v = make_unique_null() +assert v is None + +v = make_unique_str() +assert isinstance(v, str) +assert v == "str" + +v = make_unique_complex() +assert isinstance(v, complex) +assert v == 1 + 0j + +unique_ptr_holder = UniquePtrHolder() + +v = unique_ptr_holder.int_ptr +assert isinstance(v, int) +assert v == 20 +# v is a copy, int_ptr will not be updated +v = 10 +assert unique_ptr_holder.int_ptr == 20 + +v = unique_ptr_holder.v1_ptr +assert isinstance(v, V1) +assert v.v == 200 +# v is a ref, v1_ptr will be updated +v.v = 10 +assert unique_ptr_holder.v1_ptr.v == 10 + +v = unique_ptr_holder.null_ptr +assert v is None + +v = unique_ptr_holder.str_ptr +assert isinstance(v, str) +assert v == "str" +# v is a copy, str_ptr will not be updated +v = "str_updated" +assert unique_ptr_holder.str_ptr == "str" + +v = unique_ptr_holder.complex_ptr +assert isinstance(v, complex) +assert v == 1 + 0j +# v is a copy, complex_ptr will not be updated +v = 1 + 2j +assert unique_ptr_holder.complex_ptr == 1 + 0j diff --git a/unittest/python/test_variant.py.in b/unittest/python/test_variant.py.in index b019514cd..a7590a3fa 100644 --- a/unittest/python/test_variant.py.in +++ b/unittest/python/test_variant.py.in @@ -6,7 +6,12 @@ V2 = variant_module.V2 VariantHolder = variant_module.VariantHolder VariantFullHolder = variant_module.VariantFullHolder make_variant = variant_module.make_variant -make_variant_full = variant_module.make_variant_full +make_variant_full_none = variant_module.make_variant_full_none +make_variant_full_float = variant_module.make_variant_full_float +make_variant_full_int = variant_module.make_variant_full_int +make_variant_full_bool = variant_module.make_variant_full_bool +make_variant_full_str = variant_module.make_variant_full_str +make_variant_full_complex = variant_module.make_variant_full_complex variant = make_variant() assert isinstance(variant, V1) @@ -44,9 +49,34 @@ assert isinstance(variant_holder.variant, V2) assert variant_holder.variant.v == v2.v # Test variant that hold a None value -v_full = make_variant_full() +v_full = make_variant_full_none() assert v_full is None +# Test variant that hold a float value +v_full = make_variant_full_float() +assert v_full == 3.14 +assert isinstance(v_full, float) + +# Test variant that hold a int value +v_full = make_variant_full_int() +assert v_full == 3 +assert isinstance(v_full, int) + +# Test variant that hold a bool value +v_full = make_variant_full_bool() +assert not v_full +assert isinstance(v_full, bool) + +# Test variant that hold a str value +v_full = make_variant_full_str() +assert v_full == "str" +assert isinstance(v_full, str) + +# Test variant that hold a complex value +v_full = make_variant_full_complex() +assert v_full == 1 + 0j +assert isinstance(v_full, complex) + variant_full_holder = VariantFullHolder() # Test None @@ -81,3 +111,13 @@ assert isinstance(variant_full_holder.variant, int) variant_full_holder.variant = 3.14 assert variant_full_holder.variant == 3.14 assert isinstance(variant_full_holder.variant, float) + +# Test str +variant_full_holder.variant = "str" +assert variant_full_holder.variant == "str" +assert isinstance(variant_full_holder.variant, str) + +# Test complex +variant_full_holder.variant = 1 + 0j +assert variant_full_holder.variant == 1 + 0j +assert isinstance(variant_full_holder.variant, complex) diff --git a/unittest/std_unique_ptr.cpp b/unittest/std_unique_ptr.cpp new file mode 100644 index 000000000..a95a5d247 --- /dev/null +++ b/unittest/std_unique_ptr.cpp @@ -0,0 +1,77 @@ +/// @file +/// @copyright Copyright 2023 CNRS INRIA + +#include +#include + +#include +#include +#include + +namespace bp = boost::python; + +struct V1 { + V1() = default; + V1(double p_v) : v(p_v) {} + + double v = 100; +}; + +std::unique_ptr make_unique_int() { return std::make_unique(10); } + +std::unique_ptr make_unique_v1() { return std::make_unique(10); } + +std::unique_ptr make_unique_null() { return nullptr; } + +std::unique_ptr make_unique_str() { + return std::make_unique("str"); +} + +std::unique_ptr > make_unique_complex() { + return std::make_unique >(1., 0.); +} + +struct UniquePtrHolder { + UniquePtrHolder() + : int_ptr(std::make_unique(20)), + v1_ptr(std::make_unique(200)), + str_ptr(std::make_unique("str")), + complex_ptr(std::make_unique >(1., 0.)) {} + + std::unique_ptr int_ptr; + std::unique_ptr v1_ptr; + std::unique_ptr null_ptr; + std::unique_ptr str_ptr; + std::unique_ptr > complex_ptr; +}; + +BOOST_PYTHON_MODULE(std_unique_ptr) { + eigenpy::enableEigenPy(); + + bp::class_("V1", bp::init<>()).def_readwrite("v", &V1::v); + + bp::def("make_unique_int", make_unique_int); + bp::def("make_unique_v1", make_unique_v1); + bp::def("make_unique_null", make_unique_null, + eigenpy::StdUniquePtrCallPolicies()); + bp::def("make_unique_str", make_unique_str); + bp::def("make_unique_complex", make_unique_complex); + + boost::python::class_("UniquePtrHolder", + bp::init<>()) + .add_property("int_ptr", + bp::make_getter(&UniquePtrHolder::int_ptr, + eigenpy::ReturnInternalStdUniquePtr())) + .add_property("v1_ptr", + bp::make_getter(&UniquePtrHolder::v1_ptr, + eigenpy::ReturnInternalStdUniquePtr())) + .add_property("null_ptr", + bp::make_getter(&UniquePtrHolder::null_ptr, + eigenpy::ReturnInternalStdUniquePtr())) + .add_property("str_ptr", + bp::make_getter(&UniquePtrHolder::str_ptr, + eigenpy::ReturnInternalStdUniquePtr())) + .add_property("complex_ptr", + bp::make_getter(&UniquePtrHolder::complex_ptr, + eigenpy::ReturnInternalStdUniquePtr())); +} diff --git a/unittest/variant.cpp.in b/unittest/variant.cpp.in index 12f669937..5d8f75ebd 100644 --- a/unittest/variant.cpp.in +++ b/unittest/variant.cpp.in @@ -4,6 +4,9 @@ #include #include +#include +#include + #cmakedefine TEST_TYPE @TEST_TYPE@ #define VARIANT TEST_TYPE @@ -32,12 +35,18 @@ struct MyVariantNoneHelper > { }; #endif -typedef typename MyVariantNoneHelper >::type +typedef typename MyVariantNoneHelper< + VARIANT > >::type MyVariantFull; MyVariant make_variant() { return V1(); } -MyVariantFull make_variant_full() { return MyVariantFull(); } +MyVariantFull make_variant_full_none() { return MyVariantFull(); } +MyVariantFull make_variant_full_float() { return 3.14; } +MyVariantFull make_variant_full_int() { return 3; } +MyVariantFull make_variant_full_bool() { return false; } +MyVariantFull make_variant_full_str() { return std::string("str"); } +MyVariantFull make_variant_full_complex() { return std::complex(1., 0.); } struct VariantHolder { MyVariant variant; @@ -68,7 +77,12 @@ BOOST_PYTHON_MODULE(@MODNAME@) { typedef eigenpy::VariantConverter ConverterFull; ConverterFull::registration(); - bp::def("make_variant_full", make_variant_full); + bp::def("make_variant_full_none", make_variant_full_none); + bp::def("make_variant_full_float", make_variant_full_float); + bp::def("make_variant_full_int", make_variant_full_int); + bp::def("make_variant_full_bool", make_variant_full_bool); + bp::def("make_variant_full_str", make_variant_full_str); + bp::def("make_variant_full_complex", make_variant_full_complex); boost::python::class_("VariantFullHolder", bp::init<>()) .add_property("variant",