From 8d0149e4ad2b629df8e869ab103eed5a2ec91d5b Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Fri, 24 Apr 2026 05:17:26 -0700 Subject: [PATCH 01/11] Remove __bool__ fallback from _is_boolean in _slicing.pxi --- dpnp/tensor/_slicing.pxi | 6 ------ 1 file changed, 6 deletions(-) diff --git a/dpnp/tensor/_slicing.pxi b/dpnp/tensor/_slicing.pxi index f387aef8afd..2f22894c4b1 100644 --- a/dpnp/tensor/_slicing.pxi +++ b/dpnp/tensor/_slicing.pxi @@ -104,12 +104,6 @@ cdef bint _is_boolean(object x) except *: return f in "?" else: return False - if callable(getattr(x, "__bool__", None)): - try: - x.__bool__() - except (TypeError, ValueError): - return False - return True return False From 672a45c7230b39085183fb2d372d835e642b4404 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Fri, 24 Apr 2026 05:18:53 -0700 Subject: [PATCH 02/11] Support range/list as advanced index keys in dpnp_array --- dpnp/dpnp_array.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index 00a1b2d00e5..1e680ac706c 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -38,6 +38,8 @@ import warnings +import numpy + import dpnp import dpnp.tensor as dpt import dpnp.tensor._type_utils as dtu @@ -46,23 +48,39 @@ from .exceptions import AxisError +def _unwrap_index_element(x): + """ + Unwrap a single index element for the tensor indexing layer. + + Converts dpnp arrays to usm_ndarray and array-like objects (range, list) + to numpy arrays with intp dtype for NumPy-compatible advanced indexing. + + """ + + if isinstance(x, dpnp_array): + return x.get_array() + if isinstance(x, (range, list)): + return numpy.asarray(x, dtype=numpy.intp) + return x + + def _get_unwrapped_index_key(key): """ Get an unwrapped index key. Return a key where each nested instance of DPNP array is unwrapped into - USM ndarray for further processing in DPCTL advanced indexing functions. + USM ndarray, and array-like objects (range, list) are converted to numpy + arrays for further processing in advanced indexing functions. """ if isinstance(key, tuple): - if any(isinstance(x, dpnp_array) for x in key): - # create a new tuple from the input key with unwrapped DPNP arrays - return tuple( - x.get_array() if isinstance(x, dpnp_array) else x for x in key - ) + if any(isinstance(x, (dpnp_array, range, list)) for x in key): + return tuple(_unwrap_index_element(x) for x in key) elif isinstance(key, dpnp_array): return key.get_array() + elif isinstance(key, (range, list)): + return numpy.asarray(key, dtype=numpy.intp) return key From 7ef44b727ac22d22636b14842b0a3b76c3d1a458 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Fri, 24 Apr 2026 05:21:58 -0700 Subject: [PATCH 03/11] Add tests for range/list advanced indexing --- dpnp/tests/test_indexing.py | 53 +++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/dpnp/tests/test_indexing.py b/dpnp/tests/test_indexing.py index bfdcf0ed30a..2edc8214f3e 100644 --- a/dpnp/tests/test_indexing.py +++ b/dpnp/tests/test_indexing.py @@ -353,6 +353,59 @@ def test_indexing_array_negative_strides(self): arr[slices] = 10 assert_equal(arr, 10.0, strict=False) + @pytest.mark.parametrize( + "idx", + [ + (range(2), range(2)), + ([0, 1], [0, 1]), + ], + ids=["range", "list"], + ) + def test_array_like_index_getitem(self, idx): + np_a = numpy.arange(36).reshape(2, 2, 3, 3) + dp_a = dpnp.arange(36).reshape(2, 2, 3, 3) + assert_array_equal(dp_a[idx], np_a[idx]) + + @pytest.mark.parametrize( + "idx", + [ + (range(2), range(2)), + ([0, 1], [0, 1]), + ], + ids=["range", "list"], + ) + def test_array_like_index_setitem(self, idx): + np_a = numpy.arange(36).reshape(2, 2, 3, 3) + dp_a = dpnp.arange(36).reshape(2, 2, 3, 3) + np_a[idx] = 0 + dp_a[idx] = 0 + assert_array_equal(dp_a, np_a) + + def test_array_like_index_inplace_add(self): + np_a = numpy.arange(36).reshape(2, 2, 3, 3) + dp_a = dpnp.arange(36).reshape(2, 2, 3, 3) + np_tmp = -numpy.ones((2, 3, 3), dtype=numpy.intp) + dp_tmp = -dpnp.ones((2, 3, 3), dtype=numpy.intp) + + np_a[range(2), range(2)] += 2 * np_tmp + dp_a[range(2), range(2)] += 2 * dp_tmp + assert_array_equal(dp_a, np_a) + + @pytest.mark.parametrize( + "idx", + [ + range(2), + [0, 1], + range(0), + [], + ], + ids=["range", "list", "empty_range", "empty_list"], + ) + def test_array_like_single_index(self, idx): + np_a = numpy.arange(24).reshape(2, 3, 4) + dp_a = dpnp.arange(24).reshape(2, 3, 4) + assert_array_equal(dp_a[idx], np_a[idx]) + class TestIx: @pytest.mark.parametrize( From 5f5b0b173d5701f66fcdc33a72ee989ae6f688c5 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Fri, 24 Apr 2026 07:21:04 -0700 Subject: [PATCH 04/11] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6239fd9c1f..81b7578044f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* Fixed incorrect in-place advanced indexing for 4D arrays when using `range` or `list` as index keys [#2872](https://github.com/IntelPython/dpnp/pull/2872) + ### Security From 830ef88b5bc8f02e1b1f08eea046d291f3a139d0 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Fri, 24 Apr 2026 10:13:02 -0700 Subject: [PATCH 05/11] Handle list/empty list advanced indexing correctly --- dpnp/dpnp_array.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index 1e680ac706c..7b4391c6f82 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -59,8 +59,16 @@ def _unwrap_index_element(x): if isinstance(x, dpnp_array): return x.get_array() - if isinstance(x, (range, list)): + if isinstance(x, range): return numpy.asarray(x, dtype=numpy.intp) + if isinstance(x, list): + # keep boolean lists as boolean + arr = numpy.asarray(x) + # cast empty lists (float64 in NumPy) to intp + # for correct tensor indexing + if arr.size == 0: + arr = arr.astype(numpy.intp) + return arr return x @@ -77,10 +85,8 @@ def _get_unwrapped_index_key(key): if isinstance(key, tuple): if any(isinstance(x, (dpnp_array, range, list)) for x in key): return tuple(_unwrap_index_element(x) for x in key) - elif isinstance(key, dpnp_array): - return key.get_array() - elif isinstance(key, (range, list)): - return numpy.asarray(key, dtype=numpy.intp) + elif isinstance(key, (dpnp_array, range, list)): + return _unwrap_index_element(key) return key From 18bb3586e20de1d9654689c809910f592f0c6c5e Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Tue, 28 Apr 2026 10:55:33 -0700 Subject: [PATCH 06/11] Apply remark --- dpnp/dpnp_array.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index 7b4391c6f82..02cd655fcef 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -57,6 +57,8 @@ def _unwrap_index_element(x): """ + if isinstance(x, dpt.usm_ndarray): + return x if isinstance(x, dpnp_array): return x.get_array() if isinstance(x, range): @@ -83,11 +85,8 @@ def _get_unwrapped_index_key(key): """ if isinstance(key, tuple): - if any(isinstance(x, (dpnp_array, range, list)) for x in key): - return tuple(_unwrap_index_element(x) for x in key) - elif isinstance(key, (dpnp_array, range, list)): - return _unwrap_index_element(key) - return key + return tuple(_unwrap_index_element(x) for x in key) + return _unwrap_index_element(key) # pylint: disable=too-many-public-methods From 4523353e0df1e01f338755c5df0289b9c1194a6d Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Mon, 4 May 2026 05:07:27 -0700 Subject: [PATCH 07/11] Support buffer protocol objects as index keys --- .github/workflows/conda-package.yml | 4 ++-- dpnp/dpnp_array.py | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/conda-package.yml b/.github/workflows/conda-package.yml index f0c85d8c3f0..6d1f2e5d00e 100644 --- a/.github/workflows/conda-package.yml +++ b/.github/workflows/conda-package.yml @@ -91,13 +91,13 @@ jobs: - name: Build conda package id: build_conda_pkg continue-on-error: true - run: conda build --no-test --python ${{ matrix.python }} --numpy 2.0 ${{ env.channels-list }} conda-recipe + run: conda-build --no-test --python ${{ matrix.python }} --numpy 2.0 ${{ env.channels-list }} conda-recipe env: MAX_BUILD_CMPL_MKL_VERSION: '2026.0a0' - name: ReBuild conda package if: steps.build_conda_pkg.outcome == 'failure' - run: conda build --no-test --python ${{ matrix.python }} --numpy 2.0 ${{ env.channels-list }} conda-recipe + run: conda-build --no-test --python ${{ matrix.python }} --numpy 2.0 ${{ env.channels-list }} conda-recipe env: MAX_BUILD_CMPL_MKL_VERSION: '2026.0a0' diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index 02cd655fcef..5c5c28a7002 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -52,8 +52,9 @@ def _unwrap_index_element(x): """ Unwrap a single index element for the tensor indexing layer. - Converts dpnp arrays to usm_ndarray and array-like objects (range, list) - to numpy arrays with intp dtype for NumPy-compatible advanced indexing. + Converts dpnp arrays to usm_ndarray and array-like objects (range, list, + buffer protocol objects) to numpy arrays for NumPy-compatible advanced + indexing. """ @@ -71,6 +72,16 @@ def _unwrap_index_element(x): if arr.size == 0: arr = arr.astype(numpy.intp) return arr + if isinstance(x, numpy.ndarray): + return x + # convert buffer protocol objects (array.array, memoryview, etc.) + try: + mv = memoryview(x) + except TypeError: + return x + # 0-d buffers are handled by the tensor layer + if mv.ndim > 0: + return numpy.asarray(x) return x From fc5b54fe65564dbb3809d1da3040842679604f16 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Mon, 4 May 2026 05:10:23 -0700 Subject: [PATCH 08/11] Add tests for buffer protocol advanced indexing --- dpnp/tests/test_indexing.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/dpnp/tests/test_indexing.py b/dpnp/tests/test_indexing.py index 2edc8214f3e..aaa9f03e7ae 100644 --- a/dpnp/tests/test_indexing.py +++ b/dpnp/tests/test_indexing.py @@ -1,3 +1,4 @@ +import array import functools import dpctl @@ -406,6 +407,41 @@ def test_array_like_single_index(self, idx): dp_a = dpnp.arange(24).reshape(2, 3, 4) assert_array_equal(dp_a[idx], np_a[idx]) + def test_buffer_protocol_getitem(self): + inds = array.array("l") + inds.frombytes(numpy.arange(3).tobytes()) + np_a = numpy.arange(12).reshape(3, 4) + dp_a = dpnp.arange(12).reshape(3, 4) + assert_array_equal(dp_a[inds], np_a[inds]) + + def test_buffer_protocol_paired_index(self): + inds = array.array("l") + inds.frombytes(numpy.arange(3).tobytes()) + np_a = numpy.arange(12).reshape(3, 4) + dp_a = dpnp.arange(12).reshape(3, 4) + assert_array_equal(dp_a[inds, inds], np_a[inds, inds]) + + def test_buffer_protocol_setitem(self): + inds = array.array("l") + inds.frombytes(numpy.arange(3).tobytes()) + np_a = numpy.arange(12).reshape(3, 4) + dp_a = dpnp.arange(12).reshape(3, 4) + np_a[inds, inds] = 0 + dp_a[inds, inds] = 0 + assert_array_equal(dp_a, np_a) + + def test_memoryview_getitem(self): + inds = memoryview(array.array("l", [0, 1, 2])) + np_a = numpy.arange(12).reshape(3, 4) + dp_a = dpnp.arange(12).reshape(3, 4) + assert_array_equal(dp_a[inds], np_a[inds]) + + def test_bytearray_getitem(self): + inds = bytearray(b"\x00\x01\x02") + np_a = numpy.arange(10) + dp_a = dpnp.arange(10) + assert_array_equal(dp_a[inds], np_a[inds]) + class TestIx: @pytest.mark.parametrize( From c883880a8c2db866ad9e134e61e4470828e78133 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Mon, 4 May 2026 05:25:14 -0700 Subject: [PATCH 09/11] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81b7578044f..debbacf480d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Added support for buffer protocol objects as advanced index keys in `dpnp.ndarray` [#2889](https://github.com/IntelPython/dpnp/pull/2889) + ### Changed ### Deprecated From dc77d6e5a796d9caf0d9b374903fd2a84a72cf3d Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Wed, 6 May 2026 03:36:22 -0700 Subject: [PATCH 10/11] Apply remarks --- dpnp/dpnp_array.py | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index 5c5c28a7002..365f2759d2c 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -54,35 +54,29 @@ def _unwrap_index_element(x): Converts dpnp arrays to usm_ndarray and array-like objects (range, list, buffer protocol objects) to numpy arrays for NumPy-compatible advanced - indexing. + indexing. Scalars and slices pass through to the tensor layer. """ - if isinstance(x, dpt.usm_ndarray): + if ( + x is None + or x is Ellipsis + or isinstance(x, (dpt.usm_ndarray, slice, numpy.ndarray)) + ): return x if isinstance(x, dpnp_array): return x.get_array() - if isinstance(x, range): - return numpy.asarray(x, dtype=numpy.intp) - if isinstance(x, list): - # keep boolean lists as boolean - arr = numpy.asarray(x) - # cast empty lists (float64 in NumPy) to intp - # for correct tensor indexing - if arr.size == 0: - arr = arr.astype(numpy.intp) - return arr - if isinstance(x, numpy.ndarray): - return x - # convert buffer protocol objects (array.array, memoryview, etc.) - try: - mv = memoryview(x) - except TypeError: + # scalars (int, bool, numpy scalars) pass through to the tensor layer + if isinstance(x, (int, numpy.generic)): return x - # 0-d buffers are handled by the tensor layer - if mv.ndim > 0: - return numpy.asarray(x) - return x + + # convert array-like objects (range, list, buffer protocol) to numpy + arr = numpy.asarray(x) + # cast empty arrays (float64 in NumPy) to intp + # for correct tensor indexing + if arr.size == 0 and arr.dtype.kind == "f": + arr = arr.astype(numpy.intp) + return arr def _get_unwrapped_index_key(key): From f00b22e8d19661c40320f4533b0457a492d10271 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Wed, 6 May 2026 03:38:17 -0700 Subject: [PATCH 11/11] Add test_invalid_index to TestIndexing --- dpnp/tests/test_indexing.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/dpnp/tests/test_indexing.py b/dpnp/tests/test_indexing.py index aaa9f03e7ae..19273fef36c 100644 --- a/dpnp/tests/test_indexing.py +++ b/dpnp/tests/test_indexing.py @@ -442,6 +442,30 @@ def test_bytearray_getitem(self): dp_a = dpnp.arange(10) assert_array_equal(dp_a[inds], np_a[inds]) + @pytest.mark.parametrize( + "idx", + [ + 1.0, + 1 + 0j, + numpy.float64(1.0), + numpy.complex128(1.0), + "a", + [0.5, 1.5], + ], + ids=[ + "float", + "complex", + "np.float64", + "np.complex128", + "str", + "float_list", + ], + ) + def test_invalid_index(self, idx): + dp_a = dpnp.arange(12).reshape(3, 4) + with pytest.raises((IndexError, TypeError)): + dp_a[idx] + class TestIx: @pytest.mark.parametrize(