diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bc5f2d8..1e658fa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Changed - - Remove `accelerate.hpp` header that clash with Accelerate.hpp in non case sensitive OS ([#593](https://github.com/stack-of-tasks/eigenpy/pull/593) - We don't consider it an API break since this header was rarely used. + We don't consider it an API break since this header was rarely used. + +### Added +- Support for Python slice, tuple and list indexing for `std::vector` bindings ([#592](https://github.com/stack-of-tasks/eigenpy/pull/592)) ## [3.12.0] - 2025-08-12 diff --git a/include/eigenpy/std-vector.hpp b/include/eigenpy/std-vector.hpp index f2cf821e..278f942f 100644 --- a/include/eigenpy/std-vector.hpp +++ b/include/eigenpy/std-vector.hpp @@ -1,5 +1,6 @@ /// /// Copyright (c) 2016-2024 CNRS INRIA +/// Copyright (c) 2025-2025 Heriot-Watt University /// This file was taken from Pinocchio (header /// ) /// @@ -84,11 +85,14 @@ struct overload_base_get_item_for_std_vector template void visit(Class &cl) const { - cl.def("__getitem__", &base_get_item); + cl.def("__getitem__", &base_get_item_int) + .def("__getitem__", &base_get_item_slice) + .def("__getitem__", &base_get_item_list) + .def("__getitem__", &base_get_item_tuple); } private: - static boost::python::object base_get_item( + static boost::python::object base_get_item_int( boost::python::back_reference container, PyObject *i_) { index_type idx = convert_index(container.get(), i_); typename Container::iterator i = container.get().begin(); @@ -104,6 +108,83 @@ struct overload_base_get_item_for_std_vector return bp::object(bp::handle<>(convert(*i))); } + static boost::python::object base_get_item_slice( + boost::python::back_reference container, + boost::python::slice slice) { + bp::list out; + try { + auto rng = + slice.get_indices(container.get().begin(), container.get().end()); + // rng.start, rng.stop are iterators; rng.step is int; [start, stop] is + // closed + typename bp::to_python_indirect + convert; + // forward or backward + for (typename Container::iterator it = rng.start;; + std::advance(it, rng.step)) { + out.append(bp::object(bp::handle<>(convert(*it)))); + if (it == rng.stop) break; // closed interval, include stop + } + } catch (const std::invalid_argument &) { + // Boost.Python specifies empty ranges throw invalid_argument. + // Return [] (matches Python's behavior for empty slices). + return bp::list(); + } + return out; + } + + static bp::object base_get_item_list(bp::back_reference c, + bp::list idxs) { + const Py_ssize_t m = bp::len(idxs); + bp::list out; + for (Py_ssize_t k = 0; k < m; ++k) { + bp::object obj = idxs[k]; + bp::extract ei(obj); + if (!ei.check()) { + PyErr_SetString(PyExc_TypeError, "indices must be integers"); + bp::throw_error_already_set(); + } + auto idx = normalize_index(c.get().size(), ei()); + out.append(elem_ref(c.get(), idx)); + } + return out; + } + + static bp::object base_get_item_tuple(bp::back_reference c, + bp::tuple idxs) { + const Py_ssize_t m = bp::len(idxs); + bp::list out; + for (Py_ssize_t k = 0; k < m; ++k) { + bp::object obj = idxs[k]; + bp::extract ei(obj); + if (!ei.check()) { + PyErr_SetString(PyExc_TypeError, "indices must be integers"); + bp::throw_error_already_set(); + } + auto idx = normalize_index(c.get().size(), ei()); + out.append(elem_ref(c.get(), idx)); + } + return out; + } + + static index_type normalize_index(std::size_t n, long i) { + long idx = i; + if (idx < 0) idx += static_cast(n); + if (idx < 0 || idx >= static_cast(n)) { + PyErr_SetString(PyExc_IndexError, "index out of range"); + bp::throw_error_already_set(); + } + return static_cast(idx); + } + + static bp::object elem_ref(Container &c, index_type i) { + typename bp::to_python_indirect + conv; + return bp::object(bp::handle<>(conv(c[i]))); + } + static index_type convert_index(Container &container, PyObject *i_) { bp::extract i(i_); if (i.check()) { diff --git a/unittest/python/test_std_vector.py b/unittest/python/test_std_vector.py index 1b0ace61..3af9723c 100644 --- a/unittest/python/test_std_vector.py +++ b/unittest/python/test_std_vector.py @@ -98,3 +98,20 @@ def checkZero(v): # Test mutable __getitem__ l5[0][:] = 0.0 assert np.allclose(l5[0], 0.0) + +# Test slicing +l6 = eigenpy.StdVec_VectorXd() +for _ in range(4): + l6.append(rng.random(3)) +checkAllValues(l6[:1], l6.tolist()[:1]) +checkAllValues(l6[1:], l6.tolist()[1:]) +checkAllValues(l6[:-1], l6.tolist()[:-1]) +checkAllValues(l6[::2], l6.tolist()[::2]) +L = [0, 2] +L6_copy = l6[L] +for k, i in enumerate(L): + checkAllValues(L6_copy[k], l6[i]) +T = (0, 2) +L6_copy = l6[T] +for k, i in enumerate(L): + checkAllValues(L6_copy[k], l6[i])