Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,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
42 changes: 42 additions & 0 deletions dpnp/dpnp_iface_manipulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,39 @@ 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)
if host_obj.dtype == dpnp.bool:
# a boolean mask selects positions, which (for an oversized mask)
# can fall out of bounds, so validate the flatnonzero result
host_obj = numpy.flatnonzero(host_obj)
min_idx, max_idx = host_obj.min(), host_obj.max()
Comment thread
antonwolfy marked this conversation as resolved.

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 +2468,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 +2562,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
41 changes: 38 additions & 3 deletions dpnp/tests/test_manipulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -815,11 +815,46 @@ 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):
@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"):
xp.insert(a, [5, 0], 9, axis=axis)

@pytest.mark.parametrize(
"obj",
[
[True, False, False, False, True],
numpy.array([True, False, False, False, True]),
dpnp.array([True, False, False, False, True]),
],
ids=["list", "numpy", "dpnp"],
)
def test_bool_mask_out_of_bounds(self, obj):
a = dpnp.array([0, 1, 2])
with pytest.raises(IndexError, match="out of bounds"):
dpnp.insert(a, [idx], [3, 4])
dpnp.insert(a, obj, 9)


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