From 7809332e862cb797d2c7c79164a2d54c29f2ac94 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 27 Aug 2026 11:10:06 +0200 Subject: [PATCH 01/20] Align dpnp.flatiter indexing edge cases with NumPy Support slices, ellipsis, empty tuple, and integer/boolean array indices in flatiter __getitem__/__setitem__, reusing regular array indexing for validation. Reject numpy.newaxis (None), raise IndexError for out-of-bounds integer array indices, and reject assignment with a 0-D index, matching NumPy (gh-28590). Return copies from __getitem__ rather than views. SAT-8204 --- dpnp/dpnp_flatiter.py | 112 +++++++++++++++++++++++++++--------------- 1 file changed, 72 insertions(+), 40 deletions(-) diff --git a/dpnp/dpnp_flatiter.py b/dpnp/dpnp_flatiter.py index 7375e03d8020..e06b9d79ca2c 100644 --- a/dpnp/dpnp_flatiter.py +++ b/dpnp/dpnp_flatiter.py @@ -34,57 +34,89 @@ class flatiter: """Flat iterator object to iterate over arrays.""" - def __init__(self, X): - if type(X) is not dpnp.ndarray: + def __init__(self, a): + if type(a) is not dpnp.ndarray: raise TypeError( - "Argument must be of type dpnp.ndarray, got {}".format(type(X)) + "Argument must be of type dpnp.ndarray, got {}".format(type(a)) ) - self.arr_ = X - self.size_ = X.size - self.i_ = 0 - - def _multiindex(self, i): - nd = self.arr_.ndim - if nd == 0: - if i == 0: - return () - raise KeyError - elif nd == 1: - return (i,) - sh = self.arr_.shape - i_ = i - multi_index = [0] * nd - for k in reversed(range(1, nd)): - si = sh[k] - q = i_ // si - multi_index[k] = i_ - q * si - i_ = q - multi_index[0] = i_ - return tuple(multi_index) + self._arr = a + self._size = a.size + self._i = 0 + + @staticmethod + def _reject_newaxis(key): + # newaxis (None) is valid for array indexing but not for flat indexing + if key is None or ( + isinstance(key, tuple) and any(k is None for k in key) + ): + raise IndexError( + "only integers, slices (`:`), ellipsis (`...`) and integer " + "or boolean arrays are valid indices" + ) + + def _check_bounds(self, key): + # fancy int indices wrap instead of raising, so check them vs NumPy + if key is Ellipsis or isinstance(key, (slice, bool, tuple)): + return + + if isinstance(key, int) or ( + callable(getattr(key, "__index__", None)) + and not hasattr(key, "ndim") + ): + return # scalar int: regular indexing checks it + + try: + idx = dpnp.asarray(key, sycl_queue=self._arr.sycl_queue) + except Exception: + return # let regular indexing raise + + if idx.dtype.kind not in "iu" or idx.size == 0: + return + + size = self._size + hi, lo = int(dpnp.max(idx)), int(dpnp.min(idx)) + if hi >= size: + raise IndexError(f"index {hi} is out of bounds for size {size}") + if lo < -size: + raise IndexError(f"index {lo} is out of bounds for size {size}") + + def _flatten(self): + # C-order flat view (copy if non-contiguous) + return dpnp.reshape(self._arr, -1) def __getitem__(self, key): - idx = getattr(key, "__index__", None) - if not callable(idx): - raise TypeError(key) - i = idx() - mi = self._multiindex(i) - return self.arr_.__getitem__(mi) + self._reject_newaxis(key) + self._check_bounds(key) + + # flat always yields a copy, never a view + return self._flatten()[key].copy() def __setitem__(self, key, val): - idx = getattr(key, "__index__", None) - if not callable(idx): - raise TypeError(key) - i = idx() - mi = self._multiindex(i) - return self.arr_.__setitem__(mi, val) + self._reject_newaxis(key) + self._check_bounds(key) + + if isinstance(key, tuple) and len(key) == 0: + # NumPy rejects arr.flat[()] = val + raise IndexError( + "Assigning to a flat iterator with a 0-D index is not " + "supported" + ) + + # resolve key to flat positions, reusing regular indexing to validate + arr = self._arr + flat_index = dpnp.arange( + arr.size, sycl_queue=arr.sycl_queue, usm_type=arr.usm_type + ) + positions = dpnp.reshape(flat_index[key], -1) + dpnp.put(arr, positions, val) def __iter__(self): return self def __next__(self): - if self.i_ < self.size_: - val = self.__getitem__(self.i_) - self.i_ = self.i_ + 1 + if self._i < self._size: + val = self.__getitem__(self._i) + self._i = self._i + 1 return val else: raise StopIteration From 0bf9d53a63d5c4764c048f77fb7d210c92c07c38 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 27 Aug 2026 11:18:44 +0200 Subject: [PATCH 02/20] Accept dpnp.ndarray subclasses in flatiter via isinstance check --- dpnp/dpnp_flatiter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dpnp/dpnp_flatiter.py b/dpnp/dpnp_flatiter.py index e06b9d79ca2c..27cc07ca07d5 100644 --- a/dpnp/dpnp_flatiter.py +++ b/dpnp/dpnp_flatiter.py @@ -35,9 +35,9 @@ class flatiter: """Flat iterator object to iterate over arrays.""" def __init__(self, a): - if type(a) is not dpnp.ndarray: + if not isinstance(a, dpnp.ndarray): raise TypeError( - "Argument must be of type dpnp.ndarray, got {}".format(type(a)) + f"An array must be of type dpnp.ndarray, but got {type(a)}" ) self._arr = a self._size = a.size From 6c1e5b2e1a52fbc6b11590edff521db3e9aba4d1 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 27 Aug 2026 11:46:17 +0200 Subject: [PATCH 03/20] Add flatiter indexing tests aligned with NumPy Cover slices, ellipsis, empty tuple, integer/boolean array indices, newaxis rejection, out-of-bounds, non-contiguous write-back, and copy-not-view semantics. Cross-check against NumPy where behavior is shared, and gate NumPy 2.4-only cases (numpy-gh-28590) with testing.with_requires. --- dpnp/tests/test_flat.py | 147 ++++++++++++++++++++++++++++++++++------ 1 file changed, 128 insertions(+), 19 deletions(-) diff --git a/dpnp/tests/test_flat.py b/dpnp/tests/test_flat.py index c40e95d3ee84..70bbd1ef3358 100644 --- a/dpnp/tests/test_flat.py +++ b/dpnp/tests/test_flat.py @@ -1,9 +1,11 @@ import numpy as np import pytest -from numpy.testing import assert_array_equal, assert_raises +from numpy.testing import assert_array_equal import dpnp +from .third_party.cupy import testing + class TestFlatiter: @pytest.mark.parametrize( @@ -16,37 +18,144 @@ class TestFlatiter: ids=["1D array", "2D array", "2D.T array"], ) def test_flat_getitem(self, a, index): - a_dp = dpnp.array(a) - result = a_dp.flat[index] + ia = dpnp.array(a) + result = ia.flat[index] expected = a.flat[index] assert_array_equal(expected, result) def test_flat_iteration(self): a = np.array([[1, 2], [3, 4]]) - a_dp = dpnp.array(a) - for dp_val, np_val in zip(a_dp.flat, a.flat): - assert dp_val == np_val + ia = dpnp.array(a) + for ival, val in zip(ia.flat, a.flat): + assert ival == val def test_init_error(self): - assert_raises(TypeError, dpnp.flatiter, [1, 2, 3]) + with pytest.raises(TypeError, match="must be of type dpnp.ndarray"): + dpnp.flatiter([1, 2, 3]) + + @pytest.mark.parametrize("xp", [dpnp, np]) + def test_flat_key_error(self, xp): + a = xp.array(42) + with pytest.raises(IndexError): + _ = a.flat[1] - def test_flat_key_error(self): - a_dp = dpnp.array(42) - with pytest.raises(KeyError): - _ = a_dp.flat[1] + @pytest.mark.parametrize("xp", [dpnp, np]) + def test_flat_invalid_key(self, xp): + flat = xp.array([1, 2, 3]).flat - def test_flat_invalid_key(self): - a_dp = dpnp.array([1, 2, 3]) - flat = dpnp.flatiter(a_dp) # check __getitem__ - with pytest.raises(TypeError): + with pytest.raises(IndexError): _ = flat["invalid"] + # check __setitem__ - with pytest.raises(TypeError): + with pytest.raises(IndexError): flat["invalid"] = 42 - def test_flat_out_of_bounds(self): - a_dp = dpnp.array([1, 2, 3]) - flat = dpnp.flatiter(a_dp) + @pytest.mark.parametrize("xp", [dpnp, np]) + def test_flat_out_of_bounds(self, xp): + flat = xp.array([1, 2, 3]).flat with pytest.raises(IndexError): _ = flat[10] + + @pytest.mark.parametrize( + "key", + [ + slice(1, 4), + slice(None), + slice(None, None, 2), + [0, 2, 4], + [-1, -2], + Ellipsis, + ], + ids=["slice", "full_slice", "step_slice", "list", "neg_list", "..."], + ) + def test_flat_getitem_index_types(self, key): + a = np.arange(1, 7).reshape(2, 3) + ia = dpnp.array(a) + assert_array_equal(ia.flat[key], a.flat[key]) + + @pytest.mark.parametrize( + "key", + [slice(1, 4), slice(None), [0, 2, 4], [-1, -2], Ellipsis], + ids=["slice", "full_slice", "list", "neg_list", "..."], + ) + def test_flat_setitem_index_types(self, key): + a = np.arange(1, 7).reshape(2, 3) + ia = dpnp.array(a) + a.flat[key] = 0 + ia.flat[key] = 0 + assert_array_equal(ia, a) + + def test_flat_index_array(self): + a = np.arange(1, 7).reshape(2, 3) + ia = dpnp.array(a) + + # int array index + assert_array_equal(ia.flat[dpnp.array([0, 3, 5])], a.flat[[0, 3, 5]]) + + @testing.with_requires("numpy>=2.4") + def test_flat_bool_mask(self): + a = np.arange(1, 7).reshape(2, 3) + ia = dpnp.array(a) + mask = np.array([True, False] * 3) + + # getitem via bool array + assert_array_equal(ia.flat[dpnp.array(mask)], a.flat[mask]) + + # setitem via bool array + a.flat[mask] = -1 + ia.flat[dpnp.array(mask)] = -1 + assert_array_equal(ia, a) + + def test_flat_non_contiguous(self): + # C-order traversal + write-back for non-contiguous arrays + a = np.arange(1, 7).reshape(2, 3).T + ia = dpnp.array(np.arange(1, 7).reshape(2, 3)).T + assert_array_equal(ia.flat[1:5], a.flat[1:5]) + a.flat[1:5] = 0 + ia.flat[1:5] = 0 + assert_array_equal(ia, a) + + @pytest.mark.parametrize("xp", [dpnp, np]) + def test_flat_getitem_returns_copy(self, xp): + # flat yields copies, not views + a = xp.arange(10) + s = a.flat[1:4] + s[0] = 999 + assert a[1] != 999 + + def test_flat_scalar_getitem_returns_copy(self): + # dpnp returns a 0-d array copy (NumPy returns an immutable scalar) + ia = dpnp.arange(10) + x = ia.flat[3] + x[...] = 777 + assert ia[3] != 777 + + @testing.with_requires("numpy>=2.4") + @pytest.mark.parametrize("xp", [dpnp, np]) + def test_flat_newaxis(self, xp): + a = xp.array([1, 2, 3]) + with pytest.raises(IndexError, match="are valid indices"): + _ = a.flat[None] + with pytest.raises(IndexError, match="are valid indices"): + a.flat[None] = 0 + + @testing.with_requires("numpy>=2.4") + @pytest.mark.parametrize("xp", [dpnp, np]) + def test_flat_empty_tuple(self, xp): + a = xp.arange(1, 7).reshape(2, 3) + # getitem with () returns the whole flattened array + assert_array_equal(a.flat[()], xp.arange(1, 7)) + # setitem with a 0-d index is unsupported + with pytest.raises(IndexError, match="0-D index is not supported"): + a.flat[()] = 0 + + @testing.with_requires("numpy>=2.4") + @pytest.mark.parametrize("xp", [dpnp, np]) + @pytest.mark.parametrize("key", [[100], [-100]], ids=["oob", "neg_oob"]) + def test_flat_array_out_of_bounds(self, xp, key): + a = xp.array([1, 2, 3]) + with pytest.raises(IndexError, match="out of bounds for size"): + _ = a.flat[key] + with pytest.raises(IndexError, match="out of bounds for size"): + a.flat[key] = 0 From be8f6a0df31404d5c8258b01571ac798ab063213 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 27 Aug 2026 15:22:22 +0200 Subject: [PATCH 04/20] Cycle values in flatiter setitem and enable cupy iterate tests Match numpy's np.put-style cycling when a flat assignment value is shorter than the selection (dpnp.put broadcasts instead). Enable the previously-disabled slice/ellipsis/empty-tuple parametrizations in the cupy flatiter iterate tests, and drop the IndexError cases that became valid indices in numpy 2.4 (numpy-gh-28590). --- dpnp/dpnp_flatiter.py | 26 ++++++++++++---- .../cupy/indexing_tests/test_iterate.py | 31 ++++++++++--------- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/dpnp/dpnp_flatiter.py b/dpnp/dpnp_flatiter.py index 27cc07ca07d5..b24eb29c6655 100644 --- a/dpnp/dpnp_flatiter.py +++ b/dpnp/dpnp_flatiter.py @@ -102,13 +102,27 @@ def __setitem__(self, key, val): "supported" ) + a = self._arr + exec_q = a.sycl_queue + usm_type = a.usm_type + # resolve key to flat positions, reusing regular indexing to validate - arr = self._arr - flat_index = dpnp.arange( - arr.size, sycl_queue=arr.sycl_queue, usm_type=arr.usm_type - ) - positions = dpnp.reshape(flat_index[key], -1) - dpnp.put(arr, positions, val) + flat_index = dpnp.arange(a.size, sycl_queue=exec_q, usm_type=usm_type) + idx = flat_index[key] + + if not dpnp.isscalar(val): + val = dpnp.asarray( + val, sycl_queue=exec_q, usm_type=usm_type + ).ravel() + n = idx.size + if 0 < val.size != n: + # cycles the values over the selection + val = val[ + dpnp.arange(n, sycl_queue=exec_q, usm_type=usm_type) + % val.size + ] + + dpnp.put(a, idx, val) def __iter__(self): return self diff --git a/dpnp/tests/third_party/cupy/indexing_tests/test_iterate.py b/dpnp/tests/third_party/cupy/indexing_tests/test_iterate.py index f68af146dd64..7cf8d995b4b2 100644 --- a/dpnp/tests/third_party/cupy/indexing_tests/test_iterate.py +++ b/dpnp/tests/third_party/cupy/indexing_tests/test_iterate.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import unittest import warnings @@ -58,17 +60,17 @@ def test_copy_next(self, xp): @testing.parameterize( - # {"shape": (2, 3, 4), "index": Ellipsis}, + {"shape": (2, 3, 4), "index": Ellipsis}, {"shape": (2, 3, 4), "index": 0}, {"shape": (2, 3, 4), "index": 10}, - # {"shape": (2, 3, 4), "index": slice(None)}, - # {"shape": (2, 3, 4), "index": slice(None, 10)}, - # {"shape": (2, 3, 4), "index": slice(None, None, 2)}, - # {"shape": (2, 3, 4), "index": slice(None, None, -1)}, - # {"shape": (2, 3, 4), "index": slice(10, None, -1)}, - # {"shape": (2, 3, 4), "index": slice(10, None, -2)}, - # {"shape": (), "index": slice(None)}, - # {"shape": (10,), "index": slice(None)}, + {"shape": (2, 3, 4), "index": slice(None)}, + {"shape": (2, 3, 4), "index": slice(None, 10)}, + {"shape": (2, 3, 4), "index": slice(None, None, 2)}, + {"shape": (2, 3, 4), "index": slice(None, None, -1)}, + {"shape": (2, 3, 4), "index": slice(10, None, -1)}, + {"shape": (2, 3, 4), "index": slice(10, None, -2)}, + {"shape": (), "index": slice(None)}, + {"shape": (10,), "index": slice(None)}, ) class TestFlatiterSubscript(unittest.TestCase): @@ -125,12 +127,13 @@ def test_setitem_ndarray_different_types(self, xp, a_dtype, v_dtype, order): @testing.parameterize( {"shape": (2, 3, 4), "index": None}, - {"shape": (2, 3, 4), "index": (0,)}, - {"shape": (2, 3, 4), "index": True}, - {"shape": (2, 3, 4), "index": cupy.array([0])}, - {"shape": (2, 3, 4), "index": [0]}, + # the indices below are valid for flat iterators since NumPy 2.4 + # (numpy-gh-28590) and no longer raise an IndexError: + # {"shape": (2, 3, 4), "index": (0,)}, + # {"shape": (2, 3, 4), "index": True}, + # {"shape": (2, 3, 4), "index": cupy.array([0])}, + # {"shape": (2, 3, 4), "index": [0]}, ) -@pytest.mark.skip("no exception raised") class TestFlatiterSubscriptIndexError(unittest.TestCase): @testing.for_all_dtypes() From fe66c93c659bc518b175f4378a4ec27fb6bb0e18 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 27 Aug 2026 15:26:25 +0200 Subject: [PATCH 05/20] Add CHANGELOG entry for flatiter indexing fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e37ae7055273..0f1d88596082 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,7 @@ This release is compatible with NumPy 2.5. * 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) * Fixed a per-call `sycl::queue` leak in `usm_ndarray::get_queue()`/`get_device()` [#3042](https://github.com/IntelPython/dpnp/pull/3042) +* Fixed `dpnp.ndarray.flat` indexing edge cases, adding support for slices, ellipsis, and integer/boolean array indices [#3045](https://github.com/IntelPython/dpnp/pull/3045) ### Security From 575c6b1531f8829c41a9748057410196da31be8e Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Fri, 28 Aug 2026 14:45:22 +0200 Subject: [PATCH 06/20] Improve dpnp.flatiter and dpnp.ndarray.flat docstrings Expand the flatiter and ndarray.flat docstrings to align with NumPy, documenting supported basic and advanced indexing and adding See Also and Examples sections. Fix a broken dpnp.flat cross-reference in the ndarray.flatten docstring. --- dpnp/dpnp_array.py | 32 +++++++++++++++++++++++++++++--- dpnp/dpnp_flatiter.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index 86055a4828fd..8ae492206a77 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -1332,9 +1332,35 @@ def flags(self): @property def flat(self): """ - Return a flat iterator, or set a flattened version of self to value. + A 1-D iterator over the array. - """ # noqa: D200 + This is a :obj:`dpnp.flatiter` instance, which acts similarly to, but + is not a subclass of, Python's built-in iterator object. + + See Also + -------- + :obj:`dpnp.flatiter` : Flat iterator object to iterate over arrays. + :obj:`dpnp.ndarray.flatten` : Return a flattened copy of the array. + + Examples + -------- + >>> import dpnp as np + >>> x = np.arange(1, 7).reshape(2, 3) + >>> x + array([[1, 2, 3], + [4, 5, 6]]) + >>> x.flat[3] + array(4) + >>> x.T.flat[3] + array(5) + + An assignment example: + + >>> x.flat[[1, 4]] = 1; x + array([[1, 1, 3], + [4, 1, 6]]) + + """ return dpnp.flatiter(self) @@ -1367,7 +1393,7 @@ def flatten(self, /, order="C"): See Also -------- :obj:`dpnp.ravel` : Return a flattened array. - :obj:`dpnp.flat` : A 1-D flat iterator over the array. + :obj:`dpnp.ndarray.flat` : A 1-D flat iterator over the array. Examples -------- diff --git a/dpnp/dpnp_flatiter.py b/dpnp/dpnp_flatiter.py index b24eb29c6655..9651057bd903 100644 --- a/dpnp/dpnp_flatiter.py +++ b/dpnp/dpnp_flatiter.py @@ -32,7 +32,41 @@ class flatiter: - """Flat iterator object to iterate over arrays.""" + """ + Flat iterator object to iterate over arrays. + + A flat iterator is returned by :obj:`dpnp.ndarray.flat` for any array. It + allows iterating over the array as if it were a 1-D array, either in a + for-loop or by calling its ``next`` method. + + Iteration is done in row-major, C-style order (the last index varying the + fastest). The iterator can also be indexed using basic slicing or advanced + indexing. + + For full documentation refer to :obj:`numpy.flatiter`. + + See Also + -------- + :obj:`dpnp.ndarray.flat` : Return a flat iterator over an array. + :obj:`dpnp.ndarray.flatten` : Return a flattened copy of an array. + + Examples + -------- + >>> import dpnp as np + >>> x = np.arange(6).reshape(2, 3) + >>> for item in x.flat: + ... print(item) + 0 + 1 + 2 + 3 + 4 + 5 + + >>> x.flat[2:4] + array([2, 3]) + + """ def __init__(self, a): if not isinstance(a, dpnp.ndarray): From ee7488cf97188c375bf2405fab9e4d9a6e87f0f4 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Fri, 28 Aug 2026 17:36:52 +0200 Subject: [PATCH 07/20] Add NumPy reference to dpnp.ndarray.flat docstring --- dpnp/dpnp_array.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index 8ae492206a77..ea981c6f2c48 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -1337,6 +1337,8 @@ def flat(self): This is a :obj:`dpnp.flatiter` instance, which acts similarly to, but is not a subclass of, Python's built-in iterator object. + For full documentation refer to :obj:`numpy.ndarray.flat`. + See Also -------- :obj:`dpnp.flatiter` : Flat iterator object to iterate over arrays. From 1dbcb7f32e0863508e4662700003fb71556f736b Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Mon, 31 Aug 2026 16:42:31 +0200 Subject: [PATCH 08/20] Avoid full index array for scalar and slice flatiter assignment Resolve a scalar integer or slice flat index to positions directly instead of allocating arange(size) and indexing it, so a single-element or slice assignment no longer materializes a full index array. --- dpnp/dpnp_flatiter.py | 21 +++++++++++++++++++-- dpnp/tests/test_flat.py | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/dpnp/dpnp_flatiter.py b/dpnp/dpnp_flatiter.py index 9651057bd903..af5e7bbe8dd5 100644 --- a/dpnp/dpnp_flatiter.py +++ b/dpnp/dpnp_flatiter.py @@ -141,8 +141,25 @@ def __setitem__(self, key, val): usm_type = a.usm_type # resolve key to flat positions, reusing regular indexing to validate - flat_index = dpnp.arange(a.size, sycl_queue=exec_q, usm_type=usm_type) - idx = flat_index[key] + if isinstance(key, int) and not isinstance(key, bool): + # fast path for a scalar index: avoid building a full index array + pos = key + a.size if key < 0 else key + if not 0 <= pos < a.size: + raise IndexError( + f"index {key} is out of bounds for size {a.size}" + ) + idx = dpnp.asarray(pos, sycl_queue=exec_q, usm_type=usm_type) + elif isinstance(key, slice): + # slice fast path: build only the selected positions + start, stop, step = key.indices(a.size) + idx = dpnp.arange( + start, stop, step, sycl_queue=exec_q, usm_type=usm_type + ) + else: + flat_index = dpnp.arange( + a.size, sycl_queue=exec_q, usm_type=usm_type + ) + idx = flat_index[key] if not dpnp.isscalar(val): val = dpnp.asarray( diff --git a/dpnp/tests/test_flat.py b/dpnp/tests/test_flat.py index 70bbd1ef3358..61e9bde362a8 100644 --- a/dpnp/tests/test_flat.py +++ b/dpnp/tests/test_flat.py @@ -76,8 +76,24 @@ def test_flat_getitem_index_types(self, key): @pytest.mark.parametrize( "key", - [slice(1, 4), slice(None), [0, 2, 4], [-1, -2], Ellipsis], - ids=["slice", "full_slice", "list", "neg_list", "..."], + [ + slice(1, 4), + slice(None), + slice(None, None, 2), + slice(None, None, -1), + [0, 2, 4], + [-1, -2], + Ellipsis, + ], + ids=[ + "slice", + "full_slice", + "step_slice", + "neg_step_slice", + "list", + "neg_list", + "...", + ], ) def test_flat_setitem_index_types(self, key): a = np.arange(1, 7).reshape(2, 3) @@ -86,6 +102,21 @@ def test_flat_setitem_index_types(self, key): ia.flat[key] = 0 assert_array_equal(ia, a) + @pytest.mark.parametrize("index", [0, 5, -1, -6]) + def test_flat_setitem_scalar(self, index): + a = np.arange(1, 7) + ia = dpnp.array(a) + a.flat[index] = 99 + ia.flat[index] = 99 + assert_array_equal(ia, a) + + @pytest.mark.parametrize("xp", [dpnp, np]) + @pytest.mark.parametrize("index", [6, -7], ids=["oob", "neg_oob"]) + def test_flat_setitem_scalar_out_of_bounds(self, xp, index): + a = xp.arange(1, 7) + with pytest.raises(IndexError, match="out of bounds"): + a.flat[index] = 0 + def test_flat_index_array(self): a = np.arange(1, 7).reshape(2, 3) ia = dpnp.array(a) From 6bc35e60d157e4b3ae1004e1df60524c8af12e31 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Mon, 31 Aug 2026 16:58:34 +0200 Subject: [PATCH 09/20] Catch out-of-bounds index wrapped in a tuple in flatiter A 1-D flat iterator takes a single index, so unwrap a 1-element index tuple to its element before validation. This makes an out-of-bounds array index wrapped in a tuple (e.g. arr.flat[(array([5]),)]) raise IndexError as in NumPy, instead of silently wrapping. --- dpnp/dpnp_flatiter.py | 10 ++++++++++ dpnp/tests/test_flat.py | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/dpnp/dpnp_flatiter.py b/dpnp/dpnp_flatiter.py index af5e7bbe8dd5..b8da1cb5f989 100644 --- a/dpnp/dpnp_flatiter.py +++ b/dpnp/dpnp_flatiter.py @@ -77,6 +77,14 @@ def __init__(self, a): self._size = a.size self._i = 0 + @staticmethod + def _unwrap_tuple(key): + # a flat iterator is 1-D, so a single-element index tuple is equivalent + # to its element (e.g. `flat[(idx,)]` behaves like `flat[idx]`) + if isinstance(key, tuple) and len(key) == 1: + return key[0] + return key + @staticmethod def _reject_newaxis(key): # newaxis (None) is valid for array indexing but not for flat indexing @@ -119,6 +127,7 @@ def _flatten(self): return dpnp.reshape(self._arr, -1) def __getitem__(self, key): + key = self._unwrap_tuple(key) self._reject_newaxis(key) self._check_bounds(key) @@ -126,6 +135,7 @@ def __getitem__(self, key): return self._flatten()[key].copy() def __setitem__(self, key, val): + key = self._unwrap_tuple(key) self._reject_newaxis(key) self._check_bounds(key) diff --git a/dpnp/tests/test_flat.py b/dpnp/tests/test_flat.py index 61e9bde362a8..873d766cb963 100644 --- a/dpnp/tests/test_flat.py +++ b/dpnp/tests/test_flat.py @@ -124,6 +124,26 @@ def test_flat_index_array(self): # int array index assert_array_equal(ia.flat[dpnp.array([0, 3, 5])], a.flat[[0, 3, 5]]) + def test_flat_single_element_tuple(self): + a = np.arange(1, 7) + ia = dpnp.array(a) + + # a 1-element index tuple is equivalent to the bare index + assert_array_equal(ia.flat[(0,)], a.flat[(0,)]) + assert_array_equal(ia.flat[(slice(1, 4),)], a.flat[(slice(1, 4),)]) + assert_array_equal( + ia.flat[(dpnp.array([0, 2]),)], a.flat[(np.array([0, 2]),)] + ) + + @pytest.mark.parametrize("xp", [dpnp, np]) + def test_flat_tuple_array_out_of_bounds(self, xp): + a = xp.array([1, 2, 3]) + idx = (xp.array([5]),) + with pytest.raises(IndexError, match="out of bounds"): + _ = a.flat[idx] + with pytest.raises(IndexError, match="out of bounds"): + a.flat[idx] = 0 + @testing.with_requires("numpy>=2.4") def test_flat_bool_mask(self): a = np.arange(1, 7).reshape(2, 3) From 3a9333a9345b1427e048b50bd4eb3924fb1de378 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Mon, 31 Aug 2026 17:31:22 +0200 Subject: [PATCH 10/20] Bounds-check flat index on the host to avoid device transfers Validate an out-of-bounds flat index by inspecting the raw index on the host (numpy) when it is not already a device array, instead of always uploading it via dpnp.asarray and reducing on device. Add tests for usm_ndarray and empty index keys. --- dpnp/dpnp_flatiter.py | 22 ++++++++++++++++------ dpnp/tests/test_flat.py | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/dpnp/dpnp_flatiter.py b/dpnp/dpnp_flatiter.py index b8da1cb5f989..7eb881226684 100644 --- a/dpnp/dpnp_flatiter.py +++ b/dpnp/dpnp_flatiter.py @@ -28,7 +28,12 @@ """Implementation of flatiter.""" +import numpy + import dpnp +import dpnp.tensor as dpt + +from .dpnp_array import dpnp_array class flatiter: @@ -107,16 +112,21 @@ def _check_bounds(self, key): ): return # scalar int: regular indexing checks it - try: - idx = dpnp.asarray(key, sycl_queue=self._arr.sycl_queue) - except Exception: - return # let regular indexing raise + if isinstance(key, dpnp_array): + idx = key + elif isinstance(key, dpt.usm_ndarray): + idx = dpnp_array._create_from_usm_ndarray(key) + else: + try: + idx = numpy.asarray(key) + except Exception: + return # let regular indexing raise - if idx.dtype.kind not in "iu" or idx.size == 0: + if not dpnp.issubdtype(idx.dtype, dpnp.integer) or idx.size == 0: return size = self._size - hi, lo = int(dpnp.max(idx)), int(dpnp.min(idx)) + hi, lo = int(idx.max()), int(idx.min()) if hi >= size: raise IndexError(f"index {hi} is out of bounds for size {size}") if lo < -size: diff --git a/dpnp/tests/test_flat.py b/dpnp/tests/test_flat.py index 873d766cb963..dd21479e69f1 100644 --- a/dpnp/tests/test_flat.py +++ b/dpnp/tests/test_flat.py @@ -124,6 +124,21 @@ def test_flat_index_array(self): # int array index assert_array_equal(ia.flat[dpnp.array([0, 3, 5])], a.flat[[0, 3, 5]]) + def test_flat_usm_ndarray_index(self): + a = np.arange(1, 7) + ia = dpnp.array(a) + + # a usm_ndarray index is validated and used like a dpnp array + usm_key = dpnp.array([0, 2, 4]).get_array() + assert_array_equal(ia.flat[usm_key], a.flat[[0, 2, 4]]) + with pytest.raises(IndexError, match="out of bounds"): + _ = ia.flat[dpnp.array([100]).get_array()] + + @pytest.mark.parametrize("xp", [dpnp, np]) + def test_flat_empty_index(self, xp): + a = xp.arange(1, 7) + assert_array_equal(a.flat[xp.array([], dtype=xp.intp)], a.flat[[]]) + def test_flat_single_element_tuple(self): a = np.arange(1, 7) ia = dpnp.array(a) From f70a68aa3b385229e6e70987d1475edefd876346 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 3 Sep 2026 14:56:05 +0200 Subject: [PATCH 11/20] Reject an array value assigned to a single flatiter item A scalar flat index targets a single element, so assigning an array (ndim >= 1) value now raises ValueError to match NumPy and dpnp's own scalar element assignment, instead of silently taking the first value. --- dpnp/dpnp_flatiter.py | 9 ++++++--- dpnp/tests/test_flat.py | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/dpnp/dpnp_flatiter.py b/dpnp/dpnp_flatiter.py index 7eb881226684..12a947d64874 100644 --- a/dpnp/dpnp_flatiter.py +++ b/dpnp/dpnp_flatiter.py @@ -182,9 +182,12 @@ def __setitem__(self, key, val): idx = flat_index[key] if not dpnp.isscalar(val): - val = dpnp.asarray( - val, sycl_queue=exec_q, usm_type=usm_type - ).ravel() + val = dpnp.asarray(val, sycl_queue=exec_q, usm_type=usm_type) + if idx.ndim == 0 and val.ndim != 0: + # a scalar index targets a single item, reject an array value + raise ValueError("Error setting single item of array.") + + val = val.ravel() n = idx.size if 0 < val.size != n: # cycles the values over the selection diff --git a/dpnp/tests/test_flat.py b/dpnp/tests/test_flat.py index dd21479e69f1..3dd082011e30 100644 --- a/dpnp/tests/test_flat.py +++ b/dpnp/tests/test_flat.py @@ -117,6 +117,32 @@ def test_flat_setitem_scalar_out_of_bounds(self, xp, index): with pytest.raises(IndexError, match="out of bounds"): a.flat[index] = 0 + @pytest.mark.parametrize("xp", [dpnp, np]) + def test_flat_setitem_single_item_array_value(self, xp): + for index in (0, xp.array(0), np.int64(0)): + a = xp.arange(1, 7) + with pytest.raises(ValueError, match="single item"): + a.flat[index] = [1, 2, 3] + + def test_flat_setitem_single_item_scalar_value(self): + a = np.arange(1, 7) + ia = dpnp.array(a) + + a.flat[0] = 9 + a.flat[np.array(1)] = np.asarray(8) + + ia.flat[0] = 9 + ia.flat[dpnp.array(1)] = dpnp.asarray(8) + assert_array_equal(ia, a) + + def test_flat_setitem_length_one_slice_cycles(self): + a = np.arange(1, 7) + ia = dpnp.array(a) + + a.flat[0:1] = [10, 20, 30] + ia.flat[0:1] = [10, 20, 30] + assert_array_equal(ia, a) + def test_flat_index_array(self): a = np.arange(1, 7).reshape(2, 3) ia = dpnp.array(a) From eb5804e7447d2ba8f7ff275e06ec8746d62b5d14 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 3 Sep 2026 15:30:22 +0200 Subject: [PATCH 12/20] Correct NumPy version gating of flatiter tests Gate the single-item array-assignment test on numpy>=2.4 (the 0-d array index only raises there), and remove the numpy>=2.4 gate from the boolean-mask and array-out-of-bounds tests, which already behave identically on older NumPy. --- dpnp/tests/test_flat.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dpnp/tests/test_flat.py b/dpnp/tests/test_flat.py index 3dd082011e30..e915414fb688 100644 --- a/dpnp/tests/test_flat.py +++ b/dpnp/tests/test_flat.py @@ -117,6 +117,7 @@ def test_flat_setitem_scalar_out_of_bounds(self, xp, index): with pytest.raises(IndexError, match="out of bounds"): a.flat[index] = 0 + @testing.with_requires("numpy>=2.4") @pytest.mark.parametrize("xp", [dpnp, np]) def test_flat_setitem_single_item_array_value(self, xp): for index in (0, xp.array(0), np.int64(0)): @@ -185,7 +186,6 @@ def test_flat_tuple_array_out_of_bounds(self, xp): with pytest.raises(IndexError, match="out of bounds"): a.flat[idx] = 0 - @testing.with_requires("numpy>=2.4") def test_flat_bool_mask(self): a = np.arange(1, 7).reshape(2, 3) ia = dpnp.array(a) @@ -242,7 +242,6 @@ def test_flat_empty_tuple(self, xp): with pytest.raises(IndexError, match="0-D index is not supported"): a.flat[()] = 0 - @testing.with_requires("numpy>=2.4") @pytest.mark.parametrize("xp", [dpnp, np]) @pytest.mark.parametrize("key", [[100], [-100]], ids=["oob", "neg_oob"]) def test_flat_array_out_of_bounds(self, xp, key): From 63b0fb7e7cb87f4b797ea7c03f97786b32c5f814 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 3 Sep 2026 15:32:09 +0200 Subject: [PATCH 13/20] Use dpnp_array consistently in flatiter type checks --- dpnp/dpnp_flatiter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dpnp/dpnp_flatiter.py b/dpnp/dpnp_flatiter.py index 12a947d64874..7f5dea453b23 100644 --- a/dpnp/dpnp_flatiter.py +++ b/dpnp/dpnp_flatiter.py @@ -74,7 +74,7 @@ class flatiter: """ def __init__(self, a): - if not isinstance(a, dpnp.ndarray): + if not isinstance(a, dpnp_array): raise TypeError( f"An array must be of type dpnp.ndarray, but got {type(a)}" ) From 5c430b1e3caed67df9f53e0ced5860f831b5e3ff Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 3 Sep 2026 15:32:57 +0200 Subject: [PATCH 14/20] Cover negative-step slice in flatiter getitem test --- dpnp/tests/test_flat.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dpnp/tests/test_flat.py b/dpnp/tests/test_flat.py index e915414fb688..ea633d2392d9 100644 --- a/dpnp/tests/test_flat.py +++ b/dpnp/tests/test_flat.py @@ -63,11 +63,20 @@ def test_flat_out_of_bounds(self, xp): slice(1, 4), slice(None), slice(None, None, 2), + slice(None, None, -1), [0, 2, 4], [-1, -2], Ellipsis, ], - ids=["slice", "full_slice", "step_slice", "list", "neg_list", "..."], + ids=[ + "slice", + "full_slice", + "step_slice", + "neg_step_slice", + "list", + "neg_list", + "...", + ], ) def test_flat_getitem_index_types(self, key): a = np.arange(1, 7).reshape(2, 3) From 7c87de9b4c249127944689c3c99960466ce2e28c Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 3 Sep 2026 15:39:27 +0200 Subject: [PATCH 15/20] Reject a multi-element index tuple in flatiter A flat iterator is 1-D and takes a single index, so a tuple with more than one element (e.g. arr.flat[..., 2]) now raises IndexError as in NumPy, instead of silently absorbing the extra dimensions and, for setitem, mutating the array. --- dpnp/dpnp_flatiter.py | 13 ++++++------- dpnp/tests/test_flat.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/dpnp/dpnp_flatiter.py b/dpnp/dpnp_flatiter.py index 7f5dea453b23..13b3b1f474d9 100644 --- a/dpnp/dpnp_flatiter.py +++ b/dpnp/dpnp_flatiter.py @@ -91,11 +91,10 @@ def _unwrap_tuple(key): return key @staticmethod - def _reject_newaxis(key): - # newaxis (None) is valid for array indexing but not for flat indexing - if key is None or ( - isinstance(key, tuple) and any(k is None for k in key) - ): + def _reject_invalid_key(key): + # newaxis (None) and multi-element tuples (a flat iterator is 1-D and + # takes a single index) are not valid, unlike regular array indexing + if key is None or (isinstance(key, tuple) and len(key) > 1): raise IndexError( "only integers, slices (`:`), ellipsis (`...`) and integer " "or boolean arrays are valid indices" @@ -138,7 +137,7 @@ def _flatten(self): def __getitem__(self, key): key = self._unwrap_tuple(key) - self._reject_newaxis(key) + self._reject_invalid_key(key) self._check_bounds(key) # flat always yields a copy, never a view @@ -146,7 +145,7 @@ def __getitem__(self, key): def __setitem__(self, key, val): key = self._unwrap_tuple(key) - self._reject_newaxis(key) + self._reject_invalid_key(key) self._check_bounds(key) if isinstance(key, tuple) and len(key) == 0: diff --git a/dpnp/tests/test_flat.py b/dpnp/tests/test_flat.py index ea633d2392d9..d2fab67fbf9f 100644 --- a/dpnp/tests/test_flat.py +++ b/dpnp/tests/test_flat.py @@ -186,6 +186,19 @@ def test_flat_single_element_tuple(self): ia.flat[(dpnp.array([0, 2]),)], a.flat[(np.array([0, 2]),)] ) + @pytest.mark.parametrize("xp", [dpnp, np]) + @pytest.mark.parametrize( + "key", + [(Ellipsis, 2), (2, Ellipsis), (Ellipsis, slice(1, 3)), (1, 2)], + ids=["ell_int", "int_ell", "ell_slice", "int_int"], + ) + def test_flat_multi_element_tuple(self, xp, key): + a = xp.arange(6) + with pytest.raises(IndexError): + _ = a.flat[key] + with pytest.raises(IndexError): + a.flat[key] = 0 + @pytest.mark.parametrize("xp", [dpnp, np]) def test_flat_tuple_array_out_of_bounds(self, xp): a = xp.array([1, 2, 3]) From 338b6df56639937e90b9f294efc7f80ca5c58454 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 3 Sep 2026 15:42:55 +0200 Subject: [PATCH 16/20] Narrow index-conversion exception handling in flatiter Catch only TypeError/ValueError from numpy.asarray when resolving a flat index for bounds-checking, so unexpected errors propagate. Add a ragged-index test covering the fallback. --- dpnp/dpnp_flatiter.py | 2 +- dpnp/tests/test_flat.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/dpnp/dpnp_flatiter.py b/dpnp/dpnp_flatiter.py index 13b3b1f474d9..0f4d25dab074 100644 --- a/dpnp/dpnp_flatiter.py +++ b/dpnp/dpnp_flatiter.py @@ -118,7 +118,7 @@ def _check_bounds(self, key): else: try: idx = numpy.asarray(key) - except Exception: + except (TypeError, ValueError): return # let regular indexing raise if not dpnp.issubdtype(idx.dtype, dpnp.integer) or idx.size == 0: diff --git a/dpnp/tests/test_flat.py b/dpnp/tests/test_flat.py index d2fab67fbf9f..7e6f568adea5 100644 --- a/dpnp/tests/test_flat.py +++ b/dpnp/tests/test_flat.py @@ -175,6 +175,12 @@ def test_flat_empty_index(self, xp): a = xp.arange(1, 7) assert_array_equal(a.flat[xp.array([], dtype=xp.intp)], a.flat[[]]) + @pytest.mark.parametrize("xp", [dpnp, np]) + def test_flat_ragged_index(self, xp): + a = xp.arange(6) + with pytest.raises(ValueError): + _ = a.flat[[[1, 2], [3]]] + def test_flat_single_element_tuple(self): a = np.arange(1, 7) ia = dpnp.array(a) From 6c446ec818b02f8d51d3057a838025a45502feaf Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 3 Sep 2026 15:58:37 +0200 Subject: [PATCH 17/20] Consolidate flatiter key normalization into a single helper Fold tuple-unwrap and invalid-key rejection into _normalize_key (called by getitem/setitem), inline the single-use flat view, and abbreviate a comment. --- dpnp/dpnp_flatiter.py | 42 ++++++++++++++---------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/dpnp/dpnp_flatiter.py b/dpnp/dpnp_flatiter.py index 0f4d25dab074..6ed97921dfcf 100644 --- a/dpnp/dpnp_flatiter.py +++ b/dpnp/dpnp_flatiter.py @@ -82,24 +82,6 @@ def __init__(self, a): self._size = a.size self._i = 0 - @staticmethod - def _unwrap_tuple(key): - # a flat iterator is 1-D, so a single-element index tuple is equivalent - # to its element (e.g. `flat[(idx,)]` behaves like `flat[idx]`) - if isinstance(key, tuple) and len(key) == 1: - return key[0] - return key - - @staticmethod - def _reject_invalid_key(key): - # newaxis (None) and multi-element tuples (a flat iterator is 1-D and - # takes a single index) are not valid, unlike regular array indexing - if key is None or (isinstance(key, tuple) and len(key) > 1): - raise IndexError( - "only integers, slices (`:`), ellipsis (`...`) and integer " - "or boolean arrays are valid indices" - ) - def _check_bounds(self, key): # fancy int indices wrap instead of raising, so check them vs NumPy if key is Ellipsis or isinstance(key, (slice, bool, tuple)): @@ -131,22 +113,26 @@ def _check_bounds(self, key): if lo < -size: raise IndexError(f"index {lo} is out of bounds for size {size}") - def _flatten(self): - # C-order flat view (copy if non-contiguous) - return dpnp.reshape(self._arr, -1) + def _normalize_key(self, key): + # 1-D iterator: unwrap a 1-elem tuple; reject None and longer tuples + if isinstance(key, tuple) and len(key) == 1: + key = key[0] + if key is None or (isinstance(key, tuple) and len(key) > 1): + raise IndexError( + "only integers, slices (`:`), ellipsis (`...`) and integer " + "or boolean arrays are valid indices" + ) + self._check_bounds(key) + return key def __getitem__(self, key): - key = self._unwrap_tuple(key) - self._reject_invalid_key(key) - self._check_bounds(key) + key = self._normalize_key(key) # flat always yields a copy, never a view - return self._flatten()[key].copy() + return dpnp.reshape(self._arr, -1)[key].copy() def __setitem__(self, key, val): - key = self._unwrap_tuple(key) - self._reject_invalid_key(key) - self._check_bounds(key) + key = self._normalize_key(key) if isinstance(key, tuple) and len(key) == 0: # NumPy rejects arr.flat[()] = val From 9d48d8298020916e2110ce2756a470b2b888e426 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 3 Sep 2026 17:22:37 +0200 Subject: [PATCH 18/20] Reject non-mask boolean indices in flatiter A boolean flat index is only valid as an ndarray mask; a boolean scalar (True/False or 0-d bool array) or a boolean list now raises IndexError, matching NumPy (gh-28590) and avoiding a whole-array overwrite on assignment. Re-enable the boolean-scalar case in the cupy iterate IndexError tests and add dpnp tests for the rejected forms. --- dpnp/dpnp_flatiter.py | 29 +++++++++++++------ dpnp/tests/test_flat.py | 20 +++++++++++++ .../cupy/indexing_tests/test_iterate.py | 2 +- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/dpnp/dpnp_flatiter.py b/dpnp/dpnp_flatiter.py index 6ed97921dfcf..b8ad22088817 100644 --- a/dpnp/dpnp_flatiter.py +++ b/dpnp/dpnp_flatiter.py @@ -82,16 +82,20 @@ def __init__(self, a): self._size = a.size self._i = 0 - def _check_bounds(self, key): - # fancy int indices wrap instead of raising, so check them vs NumPy - if key is Ellipsis or isinstance(key, (slice, bool, tuple)): + def _validate_key(self, key): + # Ellipsis/slice/tuple need no validation here + if key is Ellipsis or isinstance(key, (slice, tuple)): return - if isinstance(key, int) or ( - callable(getattr(key, "__index__", None)) - and not hasattr(key, "ndim") + # a genuine scalar int (not bool): regular indexing checks it + if not isinstance(key, bool) and ( + isinstance(key, int) + or ( + callable(getattr(key, "__index__", None)) + and not hasattr(key, "ndim") + ) ): - return # scalar int: regular indexing checks it + return if isinstance(key, dpnp_array): idx = key @@ -103,9 +107,16 @@ def _check_bounds(self, key): except (TypeError, ValueError): return # let regular indexing raise + if dpnp.issubdtype(idx.dtype, dpnp.bool): + # only a boolean ndarray mask is valid; reject bool scalars/lists + if idx.ndim > 0 and not isinstance(key, (bool, list, tuple)): + return + raise IndexError("boolean indices for iterators are not supported") + if not dpnp.issubdtype(idx.dtype, dpnp.integer) or idx.size == 0: return + # fancy int indices wrap instead of raising, so bounds-check size = self._size hi, lo = int(idx.max()), int(idx.min()) if hi >= size: @@ -122,7 +133,7 @@ def _normalize_key(self, key): "only integers, slices (`:`), ellipsis (`...`) and integer " "or boolean arrays are valid indices" ) - self._check_bounds(key) + self._validate_key(key) return key def __getitem__(self, key): @@ -145,7 +156,7 @@ def __setitem__(self, key, val): exec_q = a.sycl_queue usm_type = a.usm_type - # resolve key to flat positions, reusing regular indexing to validate + # resolve key to flat positions if isinstance(key, int) and not isinstance(key, bool): # fast path for a scalar index: avoid building a full index array pos = key + a.size if key < 0 else key diff --git a/dpnp/tests/test_flat.py b/dpnp/tests/test_flat.py index 7e6f568adea5..fef5312fd752 100644 --- a/dpnp/tests/test_flat.py +++ b/dpnp/tests/test_flat.py @@ -227,6 +227,26 @@ def test_flat_bool_mask(self): ia.flat[dpnp.array(mask)] = -1 assert_array_equal(ia, a) + @testing.with_requires("numpy>=2.4") + @pytest.mark.parametrize("xp", [dpnp, np]) + def test_flat_boolean_list_index(self, xp): + a = xp.arange(6) + mask = [True, False, True, False, True, False] + with pytest.raises(IndexError): + _ = a.flat[mask] + with pytest.raises(IndexError): + a.flat[mask] = 0 + + @pytest.mark.parametrize("index", [True, False]) + def test_flat_boolean_scalar_index(self, index): + a = dpnp.arange(6) + with pytest.raises(IndexError): + _ = a.flat[index] + with pytest.raises(IndexError): + a.flat[index] = 9 + with pytest.raises(IndexError): + _ = a.flat[dpnp.array(index)] + def test_flat_non_contiguous(self): # C-order traversal + write-back for non-contiguous arrays a = np.arange(1, 7).reshape(2, 3).T diff --git a/dpnp/tests/third_party/cupy/indexing_tests/test_iterate.py b/dpnp/tests/third_party/cupy/indexing_tests/test_iterate.py index 7cf8d995b4b2..2030104a443f 100644 --- a/dpnp/tests/third_party/cupy/indexing_tests/test_iterate.py +++ b/dpnp/tests/third_party/cupy/indexing_tests/test_iterate.py @@ -130,7 +130,7 @@ def test_setitem_ndarray_different_types(self, xp, a_dtype, v_dtype, order): # the indices below are valid for flat iterators since NumPy 2.4 # (numpy-gh-28590) and no longer raise an IndexError: # {"shape": (2, 3, 4), "index": (0,)}, - # {"shape": (2, 3, 4), "index": True}, + {"shape": (2, 3, 4), "index": True}, # {"shape": (2, 3, 4), "index": cupy.array([0])}, # {"shape": (2, 3, 4), "index": [0]}, ) From e31432115a33940d58bfe1e3f428c54ab76512ea Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 3 Sep 2026 17:40:58 +0200 Subject: [PATCH 19/20] Gate tuple-index flatiter tests and harden the iteration test Gate the single-element-tuple and tuple-wrapped out-of-bounds tests on numpy>=2.4, since tuple indexing of a flat iterator became valid only in NumPy 2.4 (numpy-gh-28590). Assert the iterated element count so an early-stop __next__ regression is caught despite zip truncation. --- dpnp/tests/test_flat.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dpnp/tests/test_flat.py b/dpnp/tests/test_flat.py index fef5312fd752..e9e0afcb556e 100644 --- a/dpnp/tests/test_flat.py +++ b/dpnp/tests/test_flat.py @@ -26,7 +26,9 @@ def test_flat_getitem(self, a, index): def test_flat_iteration(self): a = np.array([[1, 2], [3, 4]]) ia = dpnp.array(a) - for ival, val in zip(ia.flat, a.flat): + result = list(ia.flat) + assert len(result) == a.size + for ival, val in zip(result, a.flat): assert ival == val def test_init_error(self): @@ -181,6 +183,7 @@ def test_flat_ragged_index(self, xp): with pytest.raises(ValueError): _ = a.flat[[[1, 2], [3]]] + @testing.with_requires("numpy>=2.4") def test_flat_single_element_tuple(self): a = np.arange(1, 7) ia = dpnp.array(a) @@ -205,6 +208,7 @@ def test_flat_multi_element_tuple(self, xp, key): with pytest.raises(IndexError): a.flat[key] = 0 + @testing.with_requires("numpy>=2.4") @pytest.mark.parametrize("xp", [dpnp, np]) def test_flat_tuple_array_out_of_bounds(self, xp): a = xp.array([1, 2, 3]) From 2f7e6e7e5e57805613a87440e26b6a11020b5af4 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 3 Sep 2026 18:20:54 +0200 Subject: [PATCH 20/20] Un-gate tuple-index flatiter tests (valid since NumPy 2.0) Single-element tuple indexing and tuple-wrapped out-of-bounds already behave the same on NumPy 2.0, so drop the unnecessary numpy>=2.4 gate to keep the tests running on the older-NumPy CI leg. --- dpnp/tests/test_flat.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dpnp/tests/test_flat.py b/dpnp/tests/test_flat.py index e9e0afcb556e..bfe88ab6b92a 100644 --- a/dpnp/tests/test_flat.py +++ b/dpnp/tests/test_flat.py @@ -183,7 +183,6 @@ def test_flat_ragged_index(self, xp): with pytest.raises(ValueError): _ = a.flat[[[1, 2], [3]]] - @testing.with_requires("numpy>=2.4") def test_flat_single_element_tuple(self): a = np.arange(1, 7) ia = dpnp.array(a) @@ -208,7 +207,6 @@ def test_flat_multi_element_tuple(self, xp, key): with pytest.raises(IndexError): a.flat[key] = 0 - @testing.with_requires("numpy>=2.4") @pytest.mark.parametrize("xp", [dpnp, np]) def test_flat_tuple_array_out_of_bounds(self, xp): a = xp.array([1, 2, 3])