From d50bc6bb200d7d7e4617fd8d2f4a31c1f5a054b8 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 13 Aug 2026 13:58:42 +0200 Subject: [PATCH 1/8] Raise ValueError for nested sequence of repeats in repeat A nested sequence such as `[[4]]` passed as `repeats` to `dpnp.tensor.repeat` previously raised an unclear `TypeError` when comparing the inner list to an int. Convert the sequence to an array up front and reject dimensionality greater than 1, matching NumPy behavior and the existing usm_ndarray path. --- dpnp/tensor/_manipulation_functions.py | 20 +++++++++++-------- .../tensor/test_usm_ndarray_manipulation.py | 6 ++++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/dpnp/tensor/_manipulation_functions.py b/dpnp/tensor/_manipulation_functions.py index 0fdc7209e26..df646f4f72f 100644 --- a/dpnp/tensor/_manipulation_functions.py +++ b/dpnp/tensor/_manipulation_functions.py @@ -663,21 +663,25 @@ def repeat(x, repeats, /, *, axis=None): usm_type = x.usm_type exec_q = x.sycl_queue - len_reps = len(repeats) - if len_reps == 1: - repeats = repeats[0] + repeats = dpt.asarray( + repeats, dtype=dpt.int64, usm_type=usm_type, sycl_queue=exec_q + ) + if repeats.ndim > 1: + raise ValueError( + "`repeats` sequence must be 0- or 1-dimensional, got " + f"{repeats.ndim} dimensions" + ) + if repeats.size == 1: + scalar = True + repeats = int(repeats[0]) if repeats < 0: raise ValueError("`repeats` elements must be positive") - scalar = True else: - if len_reps != axis_size: + if repeats.size != axis_size: raise ValueError( "`repeats` sequence must have the same length as the " "repeated axis" ) - repeats = dpt.asarray( - repeats, dtype=dpt.int64, usm_type=usm_type, sycl_queue=exec_q - ) if not dpt.all(repeats >= 0): raise ValueError("`repeats` elements must be positive") else: diff --git a/dpnp/tests/tensor/test_usm_ndarray_manipulation.py b/dpnp/tests/tensor/test_usm_ndarray_manipulation.py index a97e473d574..ac8a3d35d05 100644 --- a/dpnp/tests/tensor/test_usm_ndarray_manipulation.py +++ b/dpnp/tests/tensor/test_usm_ndarray_manipulation.py @@ -1464,6 +1464,12 @@ def test_repeat_arg_validation(): with pytest.raises(ValueError): dpt.repeat(x, dpt.ones((1, 1), dtype="i8")) + # repeats nested sequence must be 0d or 1d + with pytest.raises(ValueError, match="0- or 1-dimensional"): + dpt.repeat(x, [[4]]) + with pytest.raises(ValueError, match="0- or 1-dimensional"): + dpt.repeat(x, [[1, 2, 3, 4, 5]]) + # repeats must be castable to i8 with pytest.raises(TypeError): dpt.repeat(x, dpt.asarray(2.0, dtype="f4")) From e7cee6d95b8170b392190f7bf543b41a2a0be0c1 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 13 Aug 2026 13:59:42 +0200 Subject: [PATCH 2/8] Un-skip cupy test for nested-list repeats rejection `test_ndim_gt1_list_rejected` now passes since nested sequences raise a `ValueError`; update the expected message match accordingly. --- dpnp/tests/third_party/cupy/manipulation_tests/test_tiling.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dpnp/tests/third_party/cupy/manipulation_tests/test_tiling.py b/dpnp/tests/third_party/cupy/manipulation_tests/test_tiling.py index a0ea1f35865..eecfa2e6ff6 100644 --- a/dpnp/tests/third_party/cupy/manipulation_tests/test_tiling.py +++ b/dpnp/tests/third_party/cupy/manipulation_tests/test_tiling.py @@ -293,9 +293,8 @@ def test_ndim_gt1_matches_numpy(self): with pytest.raises(ValueError): xp.repeat(xp.arange(6), xp.array([[1, 2, 3, 4, 5, 6]])) - @pytest.mark.skip("different message for nested lists") def test_ndim_gt1_list_rejected(self): - with pytest.raises(ValueError, match=r"too deep"): + with pytest.raises(ValueError, match=r"0- or 1-dimensional"): cupy.repeat(cupy.arange(6), [[1, 2, 3, 4, 5, 6]]) def test_bad_axis(self): From 397d873a251397ff349b0146ab6a06754a24d73e Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 13 Aug 2026 14:02:24 +0200 Subject: [PATCH 3/8] Add CHANGELOG entry for repeat nested-sequence fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dee9a8a90e..bc0b0114da7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,7 @@ This release is compatible with NumPy 2.5. * Fixed missing strides validation in `dpnp.tensor.usm_ndarray` constructor when allocating new memory [#2927](https://github.com/IntelPython/dpnp/pull/2927) * Fixed `dpnp.bincount` raising a `ValueError` on an empty input array instead of returning an empty `intp` array [#3018](https://github.com/IntelPython/dpnp/pull/3018) * Fixed `dpnp.tensor.top_k` aborting for `k=0` by returning empty result arrays without launching a zero-sized kernel [#3022](https://github.com/IntelPython/dpnp/pull/3022) +* Fixed `dpnp.repeat` raising an unclear `TypeError` for a nested sequence of `repeats` [#3024](https://github.com/IntelPython/dpnp/issues/3024) ### Security From 745b4d24af276a7cb49d1d50df6875284d04abc1 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 13 Aug 2026 14:10:06 +0200 Subject: [PATCH 4/8] Align test_tiling.py with latest changes --- .../cupy/manipulation_tests/test_tiling.py | 79 +++++++++---------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/dpnp/tests/third_party/cupy/manipulation_tests/test_tiling.py b/dpnp/tests/third_party/cupy/manipulation_tests/test_tiling.py index eecfa2e6ff6..032b0503089 100644 --- a/dpnp/tests/third_party/cupy/manipulation_tests/test_tiling.py +++ b/dpnp/tests/third_party/cupy/manipulation_tests/test_tiling.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import unittest import numpy @@ -29,11 +31,7 @@ def test_array_repeat(self, xp): {"repeats": [2], "axis": None}, {"repeats": [2], "axis": 1}, ) -class TestRepeatListBroadcast(unittest.TestCase): - """Test for `repeats` argument using single element list. - - This feature is only supported in NumPy 1.10 or later. - """ +class TestRepeatListBroadcast: @testing.numpy_cupy_array_equal() def test_array_repeat(self, xp): @@ -48,7 +46,7 @@ def test_array_repeat(self, xp): {"repeats": [1, 2, 3, 4], "axis": None}, {"repeats": [1, 2, 3, 4], "axis": 0}, ) -class TestRepeat1D(unittest.TestCase): +class TestRepeat1D: @testing.numpy_cupy_array_equal() def test_array_repeat(self, xp): @@ -60,8 +58,7 @@ def test_array_repeat(self, xp): {"repeats": [2], "axis": None}, {"repeats": [2], "axis": 0}, ) -class TestRepeat1DListBroadcast(unittest.TestCase): - """See comment in TestRepeatListBroadcast class.""" +class TestRepeat1DListBroadcast: @testing.numpy_cupy_array_equal() def test_array_repeat(self, xp): @@ -77,7 +74,7 @@ def test_array_repeat(self, xp): {"repeats": 2, "axis": -4}, {"repeats": 2, "axis": 3}, ) -class TestRepeatFailure(unittest.TestCase): +class TestRepeatFailure: def test_repeat_failure(self): for xp in (numpy, cupy): @@ -191,38 +188,6 @@ def test_reversed(self, xp): return xp.repeat(x, xp.array([0, 1, 2, 1, 0])) -class TestRepeatNdarrayDtypeEdges: - - @testing.numpy_cupy_array_equal() - def test_bool_perelement(self, xp): - return xp.repeat(xp.arange(3), xp.array([True, False, True])) - - @testing.numpy_cupy_array_equal() - def test_bool_broadcast(self, xp): - return xp.repeat( - testing.shaped_arange((3, 4), xp), xp.array([True]), axis=0 - ) - - @testing.numpy_cupy_array_equal() - def test_uint32_accepted(self, xp): - return xp.repeat( - xp.arange(4), xp.array([1, 2, 3, 4], dtype=numpy.uint32) - ) - - -class TestRepeatNdarrayLarge: - - @testing.numpy_cupy_array_equal() - def test_large_single(self, xp): - return xp.repeat( - testing.shaped_arange((3,), xp), xp.array([0, 100000, 0]) - ) - - @testing.numpy_cupy_array_equal() - def test_large_broadcast(self, xp): - return xp.repeat(testing.shaped_arange((3,), xp), xp.array([50000])) - - class TestRepeatScalarEquivalence: """All scalar-like repeats inputs produce identical results.""" @@ -309,6 +274,38 @@ def test_method_interface(self): testing.assert_array_equal(a.repeat(reps), cupy.repeat(a, reps)) +class TestRepeatNdarrayDtypeEdges: + + @testing.numpy_cupy_array_equal() + def test_bool_perelement(self, xp): + return xp.repeat(xp.arange(3), xp.array([True, False, True])) + + @testing.numpy_cupy_array_equal() + def test_bool_broadcast(self, xp): + return xp.repeat( + testing.shaped_arange((3, 4), xp), xp.array([True]), axis=0 + ) + + @testing.numpy_cupy_array_equal() + def test_uint32_accepted(self, xp): + return xp.repeat( + xp.arange(4), xp.array([1, 2, 3, 4], dtype=numpy.uint32) + ) + + +class TestRepeatNdarrayLarge: + + @testing.numpy_cupy_array_equal() + def test_large_single(self, xp): + return xp.repeat( + testing.shaped_arange((3,), xp), xp.array([0, 100000, 0]) + ) + + @testing.numpy_cupy_array_equal() + def test_large_broadcast(self, xp): + return xp.repeat(testing.shaped_arange((3,), xp), xp.array([50000])) + + @testing.parameterize( {"reps": 0}, {"reps": 1}, From b46548fb165ea97e7dbad9ac29318aed4a4c959d Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Mon, 17 Aug 2026 21:37:27 +0200 Subject: [PATCH 5/8] Point changelog entry to the PR instead of the issue --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc0b0114da7..20ef914f2e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,7 +79,7 @@ This release is compatible with NumPy 2.5. * Fixed missing strides validation in `dpnp.tensor.usm_ndarray` constructor when allocating new memory [#2927](https://github.com/IntelPython/dpnp/pull/2927) * Fixed `dpnp.bincount` raising a `ValueError` on an empty input array instead of returning an empty `intp` array [#3018](https://github.com/IntelPython/dpnp/pull/3018) * Fixed `dpnp.tensor.top_k` aborting for `k=0` by returning empty result arrays without launching a zero-sized kernel [#3022](https://github.com/IntelPython/dpnp/pull/3022) -* Fixed `dpnp.repeat` raising an unclear `TypeError` for a nested sequence of `repeats` [#3024](https://github.com/IntelPython/dpnp/issues/3024) +* Fixed `dpnp.repeat` raising an unclear `TypeError` for a nested sequence of `repeats` [#3024](https://github.com/IntelPython/dpnp/pull/3024) ### Security From f36d4c0f3b1da7279617b840d98d2e2d05fea48f Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Mon, 17 Aug 2026 21:37:51 +0200 Subject: [PATCH 6/8] Add test for multi-element nested sequence of repeats --- dpnp/tests/tensor/test_usm_ndarray_manipulation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dpnp/tests/tensor/test_usm_ndarray_manipulation.py b/dpnp/tests/tensor/test_usm_ndarray_manipulation.py index ac8a3d35d05..bf0bd322630 100644 --- a/dpnp/tests/tensor/test_usm_ndarray_manipulation.py +++ b/dpnp/tests/tensor/test_usm_ndarray_manipulation.py @@ -1469,6 +1469,8 @@ def test_repeat_arg_validation(): dpt.repeat(x, [[4]]) with pytest.raises(ValueError, match="0- or 1-dimensional"): dpt.repeat(x, [[1, 2, 3, 4, 5]]) + with pytest.raises(ValueError, match="0- or 1-dimensional"): + dpt.repeat(x, [[1], [2], [3], [4], [5]]) # repeats must be castable to i8 with pytest.raises(TypeError): From b1998a5dce2bb76dc2786a9de7ffa893c3d0952a Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Mon, 17 Aug 2026 21:42:36 +0200 Subject: [PATCH 7/8] Preserve scalar fast path by checking repeats sequence on the host --- dpnp/tensor/_manipulation_functions.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dpnp/tensor/_manipulation_functions.py b/dpnp/tensor/_manipulation_functions.py index df646f4f72f..7cb00671f9d 100644 --- a/dpnp/tensor/_manipulation_functions.py +++ b/dpnp/tensor/_manipulation_functions.py @@ -663,9 +663,8 @@ def repeat(x, repeats, /, *, axis=None): usm_type = x.usm_type exec_q = x.sycl_queue - repeats = dpt.asarray( - repeats, dtype=dpt.int64, usm_type=usm_type, sycl_queue=exec_q - ) + # inspect the sequence on the host to preserve the scalar fast path + repeats = np.asarray(repeats) if repeats.ndim > 1: raise ValueError( "`repeats` sequence must be 0- or 1-dimensional, got " @@ -682,6 +681,9 @@ def repeat(x, repeats, /, *, axis=None): "`repeats` sequence must have the same length as the " "repeated axis" ) + repeats = dpt.asarray( + repeats, dtype=dpt.int64, usm_type=usm_type, sycl_queue=exec_q + ) if not dpt.all(repeats >= 0): raise ValueError("`repeats` elements must be positive") else: From 0c6fab818488bbee2953087b21f709de739f9aa3 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Mon, 17 Aug 2026 21:46:28 +0200 Subject: [PATCH 8/8] Document 0- or 1-dimensional repeats constraint and fix param name --- dpnp/dpnp_iface_manipulation.py | 5 +++-- dpnp/tensor/_manipulation_functions.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 96d03532cac..f697449dd5b 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -2800,14 +2800,15 @@ def repeat(a, repeats, axis=None): Parameters ---------- - x : {dpnp.ndarray, usm_ndarray} + a : {dpnp.ndarray, usm_ndarray} Input array. repeats : {int, tuple, list, range, dpnp.ndarray, usm_ndarray} The number of repetitions for each element. `repeats` is broadcasted to fit the shape of the given axis. If `repeats` is an array, it must have an integer data type. Otherwise, `repeats` must be a Python integer or sequence of Python - integers (i.e., a tuple, list, or range). + integers (i.e., a tuple, list, or range). A sequence must be 0- or + 1-dimensional. axis : {None, int}, optional The axis along which to repeat values. By default, use the flattened input array, and return a flat output array. diff --git a/dpnp/tensor/_manipulation_functions.py b/dpnp/tensor/_manipulation_functions.py index 7cb00671f9d..080e79aa881 100644 --- a/dpnp/tensor/_manipulation_functions.py +++ b/dpnp/tensor/_manipulation_functions.py @@ -576,7 +576,8 @@ def repeat(x, repeats, /, *, axis=None): If `repeats` is an array, it must have an integer data type. Otherwise, `repeats` must be a Python integer or sequence of - Python integers (i.e., a tuple, list, or range). + Python integers (i.e., a tuple, list, or range). A sequence must + be 0- or 1-dimensional. axis (Optional[int]): The axis along which to repeat values. If `axis` is `None`, the