Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ This release is compatible with NumPy 2.5.
* Fixed `dpnp.all` and `dpnp.any` aborting when reducing over an empty axis (e.g. an array with a zero-length dimension) [#3021](https://github.com/IntelPython/dpnp/pull/3021)
* Released the GIL before the blocking OneMKL DFT calls in the FFT extension [#3040](https://github.com/IntelPython/dpnp/pull/3040)
* Fixed `astype` casting an out-of-range floating point value to a signed narrow integer type saturating to the destination min/max instead of wrapping like NumPy, generalizing the earlier unsigned-only fix [#3033](https://github.com/IntelPython/dpnp/pull/3033)
* Fixed `dpnp.insert` silently ignoring out-of-bounds negative indices in a multi-element `obj`, so a mix of in-bounds and out-of-bounds indices now consistently raises `IndexError` [#3041](https://github.com/IntelPython/dpnp/pull/3041)

### Security

Expand Down
38 changes: 38 additions & 0 deletions dpnp/dpnp_iface_manipulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,35 @@ def _calc_parameters(a, axis, obj, values=None):
)


def _check_index_bounds(obj, indices, n, axis):
"""
Raise ``IndexError`` if any index in `obj` is out of bounds for `axis`.

Mirrors the size-1 path in ``_insert_singleton_index``: when `obj` lives on
the host (a Python sequence/scalar or NumPy array) the bounds are validated
with NumPy, avoiding a device sync entirely. A slice cannot be out of bounds
(``obj.indices(n)`` is clamped to ``[0, n]``), so it is skipped. Only a
device array `obj` needs a single host transfer to read its extremes.

"""

if isinstance(obj, slice) or indices.size == 0:
return

if dpnp.is_supported_array_type(obj):
min_idx, max_idx = dpnp.stack([indices.min(), indices.max()]).asnumpy()
else:
host_obj = numpy.asarray(obj)
min_idx, max_idx = host_obj.min(), host_obj.max()

min_idx, max_idx = int(min_idx), int(max_idx)
if min_idx < -n or max_idx > n:
oob = min_idx if min_idx < -n else max_idx
raise IndexError(
f"index {oob} is out of bounds for axis {axis} with size {n}"
)


def _insert_array_indices(parameters, indices, values, obj):
"""
Utility function for ``dpnp.insert`` when indices is an array with
Expand Down Expand Up @@ -2435,6 +2464,13 @@ def insert(arr, obj, values, axis=None):
does not occur in-place: a new array is returned. If
`axis` is ``None``, `out` is a flattened array.

Warnings
--------
This function might synchronize in order to validate that the indices are
within bounds. This may harm performance in some applications. To avoid
synchronization, pass `obj` as a Python scalar or sequence, or as a NumPy
array.

See Also
--------
:obj:`dpnp.append` : Append elements at the end of an array.
Expand Down Expand Up @@ -2522,8 +2558,10 @@ def insert(arr, obj, values, axis=None):
)

if indices.size == 1:
# the size-1 path validates the bounds itself while reading the index
return _insert_singleton_index(params, indices, values, obj)

_check_index_bounds(obj, indices, params.n, params.axis)
return _insert_array_indices(params, indices, values, obj)


Expand Down
29 changes: 25 additions & 4 deletions dpnp/tests/test_manipulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -815,11 +815,32 @@ def test_error(self):
with pytest.raises(TypeError):
dpnp.insert(a, [], 2, axis="nonsense")

@pytest.mark.parametrize("idx", [4, -4])
def test_index_out_of_bounds(self, idx):
a = dpnp.array([0, 1, 2])
@testing.with_requires("numpy>=2.6")
@pytest.mark.parametrize("xp", [numpy, dpnp])
@pytest.mark.parametrize(
"idx, values",
[
# single-element obj -> singleton path
([4], [3, 4]),
([-4], [3, 4]),
# multi-element obj -> array path
([-6, 0], [9, 8]),
([0, 6], [9, 8]),
([4, 4], [3, 4]),
([-4, -5], [3, 4]),
],
)
def test_index_out_of_bounds(self, xp, idx, values):
a = xp.array([0, 1, 2])
with pytest.raises(IndexError, match="out of bounds"):
xp.insert(a, idx, values)

@pytest.mark.parametrize("xp", [numpy, dpnp])
@pytest.mark.parametrize("axis", [0, 1])
def test_index_out_of_bounds_ndim(self, xp, axis):
a = xp.ones((3, 3))
with pytest.raises(IndexError, match="out of bounds"):
dpnp.insert(a, [idx], [3, 4])
xp.insert(a, [5, 0], 9, axis=axis)


# array_split has more comprehensive test of splitting.
Expand Down
Loading