diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h index a26ac2b4cd..fd960f1dfa 100644 --- a/include/pybind11/numpy.h +++ b/include/pybind11/numpy.h @@ -1156,9 +1156,9 @@ class array : public buffer { } #endif - /// Dimension along a given axis + /// Dimension along a given axis. Throws if dim is negative or >= ndim() (no wrapping). ssize_t shape(ssize_t dim) const { - if (dim >= ndim()) { + if (dim < 0 || dim >= ndim()) { fail_dim_check(dim, "invalid axis"); } return shape()[dim]; @@ -1174,9 +1174,9 @@ class array : public buffer { } #endif - /// Stride along a given axis + /// Stride along a given axis. Throws if dim is negative or >= ndim() (no wrapping). ssize_t strides(ssize_t dim) const { - if (dim >= ndim()) { + if (dim < 0 || dim >= ndim()) { fail_dim_check(dim, "invalid axis"); } return strides()[dim]; @@ -1196,15 +1196,16 @@ class array : public buffer { } /// Pointer to the contained data. If index is not provided, points to the - /// beginning of the buffer. May throw if the index would lead to out of bounds access. + /// beginning of the buffer. Throws if an index is out of bounds; negative indices are not + /// wrapped. template const void *data(Ix... index) const { return static_cast(detail::array_proxy(m_ptr)->data + offset_at(index...)); } /// Mutable pointer to the contained data. If index is not provided, points to the - /// beginning of the buffer. May throw if the index would lead to out of bounds access. - /// May throw if the array is not writeable. + /// beginning of the buffer. Throws if an index is out of bounds; negative indices are not + /// wrapped. May throw if the array is not writeable. template void *mutable_data(Ix... index) { check_writeable(); @@ -1212,7 +1213,7 @@ class array : public buffer { } /// Byte offset from beginning of the array to a given index (full or partial). - /// May throw if the index would lead to out of bounds access. + /// Throws if an index is out of bounds; negative indices are not wrapped. template ssize_t offset_at(Ix... index) const { if ((ssize_t) sizeof...(index) > ndim()) { @@ -1224,7 +1225,7 @@ class array : public buffer { ssize_t offset_at() const { return 0; } /// Item count from beginning of the array to a given index (full or partial). - /// May throw if the index would lead to out of bounds access. + /// Throws if an index is out of bounds; negative indices are not wrapped. template ssize_t index_at(Ix... index) const { return offset_at(index...) / itemsize(); @@ -1356,10 +1357,12 @@ class array : public buffer { template void check_dimensions_impl(ssize_t axis, const ssize_t *shape, ssize_t i, Ix... index) const { - if (i >= *shape) { + // Unsigned compare also rejects negative indices + if (static_cast(i) >= static_cast(*shape)) { throw index_error(std::string("index ") + std::to_string(i) + " is out of bounds for axis " + std::to_string(axis) - + " with size " + std::to_string(*shape)); + + " with size " + std::to_string(*shape) + + (i < 0 ? " (negative indices are not supported)" : "")); } check_dimensions_impl(axis + 1, shape + 1, index...); } @@ -1449,7 +1452,8 @@ class array_t : public array { return static_cast(array::mutable_data(index...)); } - // Reference to element at a given index + // Reference to element at a given index. Throws if out of bounds; negative indices are not + // wrapped. template const T &at(Ix... index) const { if ((ssize_t) sizeof...(index) != ndim()) { @@ -1459,7 +1463,8 @@ class array_t : public array { + byte_offset(ssize_t(index)...) / itemsize()); } - // Mutable reference to element at a given index + // Mutable reference to element at a given index. Throws if out of bounds; negative indices + // are not wrapped. template T &mutable_at(Ix... index) { if ((ssize_t) sizeof...(index) != ndim()) { diff --git a/tests/test_numpy_array.py b/tests/test_numpy_array.py index e3f29e92a6..2110c7c939 100644 --- a/tests/test_numpy_array.py +++ b/tests/test_numpy_array.py @@ -69,6 +69,12 @@ def test_array_attributes(): with pytest.raises(IndexError) as excinfo: m.strides(a, 2) assert str(excinfo.value) == "invalid axis: 2 (ndim = 2)" + with pytest.raises(IndexError) as excinfo: + m.shape(a, -1) + assert str(excinfo.value) == "invalid axis: -1 (ndim = 2)" + with pytest.raises(IndexError) as excinfo: + m.strides(a, -1) + assert str(excinfo.value) == "invalid axis: -1 (ndim = 2)" assert not m.writeable(a) assert m.size(a) == 6 assert m.itemsize(a) == 2 @@ -204,6 +210,8 @@ def test_bounds_check(arr): for func in ( m.index_at, m.index_at_t, + m.offset_at, + m.offset_at_t, m.data, m.data_t, m.mutate_data, @@ -217,6 +225,19 @@ def test_bounds_check(arr): with pytest.raises(IndexError) as excinfo: func(arr, 0, 4) assert str(excinfo.value) == "index 4 is out of bounds for axis 1 with size 3" + # Negative indices are not wrapped like in NumPy + with pytest.raises(IndexError) as excinfo: + func(arr, -1, 0) + assert ( + str(excinfo.value) + == "index -1 is out of bounds for axis 0 with size 2 (negative indices are not supported)" + ) + with pytest.raises(IndexError) as excinfo: + func(arr, 0, -1) + assert ( + str(excinfo.value) + == "index -1 is out of bounds for axis 1 with size 3 (negative indices are not supported)" + ) def test_make_c_f_array():