Skip to content

Commit bd0da1f

Browse files
authored
Add proper exception handling for nested lists in dpnp.repeat (#3024)
This PR fixes #2972. Passing a nested sequence as `repeats` to `dpnp.repeat` (e.g. `np.repeat(x, [[4]])`) raised an unclear `TypeError` instead of a meaningful error: ```python >>> import dpnp as np >>> x = np.array([3]) >>> np.repeat(x, [[4]]) TypeError: '<' not supported between instances of 'list' and 'int' ``` The sequence branch of `dpnp.tensor.repeat` special-cased a length-1 sequence by taking `repeats[0]` and comparing it to `0`, which fails when that element is itself a list. It also had no dimensionality guard, so a multi-element nested sequence could slip through as a 2-D array — unlike the `usm_ndarray` branch, which already rejects `ndim > 1`. The fix converts the sequence to an array up front and rejects a dimensionality greater than 1, aligning with NumPy, which raises a `ValueError` in this case: ```python >>> np.repeat(x, [[4]]) ValueError: `repeats` sequence must be 0- or 1-dimensional, got 2 dimensions ``` The scalar fast path (`_repeat_by_scalar`) is preserved for the size-1 case.
1 parent 7a2714b commit bd0da1f

5 files changed

Lines changed: 64 additions & 52 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ This release is compatible with NumPy 2.5.
8383
* Fixed comparison functions (`dpnp.equal`, `dpnp.not_equal`, `dpnp.less`, `dpnp.less_equal`, `dpnp.greater`, `dpnp.greater_equal`) and `dpnp.divide` raising `OverflowError` when comparing an integer array against a Python integer scalar outside the array dtype's range [#3017](https://github.com/IntelPython/dpnp/pull/3017)
8484
* Fixed a crash in boolean-mask advanced indexing (`dpnp.ndarray` get/set item) when the selection is empty (e.g. a scalar `False` index that injects a length-0 axis) [#3019](https://github.com/IntelPython/dpnp/pull/3019)
8585
* Released the GIL before the remaining blocking OneMKL BLAS and LAPACK calls to prevent host tasks contention, completing the work started in [#2850](https://github.com/IntelPython/dpnp/pull/2850) [#3027](https://github.com/IntelPython/dpnp/pull/3027)
86-
86+
* Fixed `dpnp.repeat` raising an unclear `TypeError` for a nested sequence of `repeats` [#3024](https://github.com/IntelPython/dpnp/pull/3024)
8787

8888
### Security
8989

dpnp/dpnp_iface_manipulation.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2800,14 +2800,15 @@ def repeat(a, repeats, axis=None):
28002800
28012801
Parameters
28022802
----------
2803-
x : {dpnp.ndarray, usm_ndarray}
2803+
a : {dpnp.ndarray, usm_ndarray}
28042804
Input array.
28052805
repeats : {int, tuple, list, range, dpnp.ndarray, usm_ndarray}
28062806
The number of repetitions for each element. `repeats` is broadcasted to
28072807
fit the shape of the given axis.
28082808
If `repeats` is an array, it must have an integer data type.
28092809
Otherwise, `repeats` must be a Python integer or sequence of Python
2810-
integers (i.e., a tuple, list, or range).
2810+
integers (i.e., a tuple, list, or range). A sequence must be 0- or
2811+
1-dimensional.
28112812
axis : {None, int}, optional
28122813
The axis along which to repeat values. By default, use the flattened
28132814
input array, and return a flat output array.

dpnp/tensor/_manipulation_functions.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,8 @@ def repeat(x, repeats, /, *, axis=None):
576576
577577
If `repeats` is an array, it must have an integer data type.
578578
Otherwise, `repeats` must be a Python integer or sequence of
579-
Python integers (i.e., a tuple, list, or range).
579+
Python integers (i.e., a tuple, list, or range). A sequence must
580+
be 0- or 1-dimensional.
580581
581582
axis (Optional[int]):
582583
The axis along which to repeat values. If `axis` is `None`, the
@@ -663,14 +664,20 @@ def repeat(x, repeats, /, *, axis=None):
663664
usm_type = x.usm_type
664665
exec_q = x.sycl_queue
665666

666-
len_reps = len(repeats)
667-
if len_reps == 1:
668-
repeats = repeats[0]
667+
# inspect the sequence on the host to preserve the scalar fast path
668+
repeats = np.asarray(repeats)
669+
if repeats.ndim > 1:
670+
raise ValueError(
671+
"`repeats` sequence must be 0- or 1-dimensional, got "
672+
f"{repeats.ndim} dimensions"
673+
)
674+
if repeats.size == 1:
675+
scalar = True
676+
repeats = int(repeats[0])
669677
if repeats < 0:
670678
raise ValueError("`repeats` elements must be positive")
671-
scalar = True
672679
else:
673-
if len_reps != axis_size:
680+
if repeats.size != axis_size:
674681
raise ValueError(
675682
"`repeats` sequence must have the same length as the "
676683
"repeated axis"

dpnp/tests/tensor/test_usm_ndarray_manipulation.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1464,6 +1464,14 @@ def test_repeat_arg_validation():
14641464
with pytest.raises(ValueError):
14651465
dpt.repeat(x, dpt.ones((1, 1), dtype="i8"))
14661466

1467+
# repeats nested sequence must be 0d or 1d
1468+
with pytest.raises(ValueError, match="0- or 1-dimensional"):
1469+
dpt.repeat(x, [[4]])
1470+
with pytest.raises(ValueError, match="0- or 1-dimensional"):
1471+
dpt.repeat(x, [[1, 2, 3, 4, 5]])
1472+
with pytest.raises(ValueError, match="0- or 1-dimensional"):
1473+
dpt.repeat(x, [[1], [2], [3], [4], [5]])
1474+
14671475
# repeats must be castable to i8
14681476
with pytest.raises(TypeError):
14691477
dpt.repeat(x, dpt.asarray(2.0, dtype="f4"))

dpnp/tests/third_party/cupy/manipulation_tests/test_tiling.py

Lines changed: 39 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import unittest
24

35
import numpy
@@ -29,11 +31,7 @@ def test_array_repeat(self, xp):
2931
{"repeats": [2], "axis": None},
3032
{"repeats": [2], "axis": 1},
3133
)
32-
class TestRepeatListBroadcast(unittest.TestCase):
33-
"""Test for `repeats` argument using single element list.
34-
35-
This feature is only supported in NumPy 1.10 or later.
36-
"""
34+
class TestRepeatListBroadcast:
3735

3836
@testing.numpy_cupy_array_equal()
3937
def test_array_repeat(self, xp):
@@ -48,7 +46,7 @@ def test_array_repeat(self, xp):
4846
{"repeats": [1, 2, 3, 4], "axis": None},
4947
{"repeats": [1, 2, 3, 4], "axis": 0},
5048
)
51-
class TestRepeat1D(unittest.TestCase):
49+
class TestRepeat1D:
5250

5351
@testing.numpy_cupy_array_equal()
5452
def test_array_repeat(self, xp):
@@ -60,8 +58,7 @@ def test_array_repeat(self, xp):
6058
{"repeats": [2], "axis": None},
6159
{"repeats": [2], "axis": 0},
6260
)
63-
class TestRepeat1DListBroadcast(unittest.TestCase):
64-
"""See comment in TestRepeatListBroadcast class."""
61+
class TestRepeat1DListBroadcast:
6562

6663
@testing.numpy_cupy_array_equal()
6764
def test_array_repeat(self, xp):
@@ -77,7 +74,7 @@ def test_array_repeat(self, xp):
7774
{"repeats": 2, "axis": -4},
7875
{"repeats": 2, "axis": 3},
7976
)
80-
class TestRepeatFailure(unittest.TestCase):
77+
class TestRepeatFailure:
8178

8279
def test_repeat_failure(self):
8380
for xp in (numpy, cupy):
@@ -191,38 +188,6 @@ def test_reversed(self, xp):
191188
return xp.repeat(x, xp.array([0, 1, 2, 1, 0]))
192189

193190

194-
class TestRepeatNdarrayDtypeEdges:
195-
196-
@testing.numpy_cupy_array_equal()
197-
def test_bool_perelement(self, xp):
198-
return xp.repeat(xp.arange(3), xp.array([True, False, True]))
199-
200-
@testing.numpy_cupy_array_equal()
201-
def test_bool_broadcast(self, xp):
202-
return xp.repeat(
203-
testing.shaped_arange((3, 4), xp), xp.array([True]), axis=0
204-
)
205-
206-
@testing.numpy_cupy_array_equal()
207-
def test_uint32_accepted(self, xp):
208-
return xp.repeat(
209-
xp.arange(4), xp.array([1, 2, 3, 4], dtype=numpy.uint32)
210-
)
211-
212-
213-
class TestRepeatNdarrayLarge:
214-
215-
@testing.numpy_cupy_array_equal()
216-
def test_large_single(self, xp):
217-
return xp.repeat(
218-
testing.shaped_arange((3,), xp), xp.array([0, 100000, 0])
219-
)
220-
221-
@testing.numpy_cupy_array_equal()
222-
def test_large_broadcast(self, xp):
223-
return xp.repeat(testing.shaped_arange((3,), xp), xp.array([50000]))
224-
225-
226191
class TestRepeatScalarEquivalence:
227192
"""All scalar-like repeats inputs produce identical results."""
228193

@@ -293,9 +258,8 @@ def test_ndim_gt1_matches_numpy(self):
293258
with pytest.raises(ValueError):
294259
xp.repeat(xp.arange(6), xp.array([[1, 2, 3, 4, 5, 6]]))
295260

296-
@pytest.mark.skip("different message for nested lists")
297261
def test_ndim_gt1_list_rejected(self):
298-
with pytest.raises(ValueError, match=r"too deep"):
262+
with pytest.raises(ValueError, match=r"0- or 1-dimensional"):
299263
cupy.repeat(cupy.arange(6), [[1, 2, 3, 4, 5, 6]])
300264

301265
def test_bad_axis(self):
@@ -310,6 +274,38 @@ def test_method_interface(self):
310274
testing.assert_array_equal(a.repeat(reps), cupy.repeat(a, reps))
311275

312276

277+
class TestRepeatNdarrayDtypeEdges:
278+
279+
@testing.numpy_cupy_array_equal()
280+
def test_bool_perelement(self, xp):
281+
return xp.repeat(xp.arange(3), xp.array([True, False, True]))
282+
283+
@testing.numpy_cupy_array_equal()
284+
def test_bool_broadcast(self, xp):
285+
return xp.repeat(
286+
testing.shaped_arange((3, 4), xp), xp.array([True]), axis=0
287+
)
288+
289+
@testing.numpy_cupy_array_equal()
290+
def test_uint32_accepted(self, xp):
291+
return xp.repeat(
292+
xp.arange(4), xp.array([1, 2, 3, 4], dtype=numpy.uint32)
293+
)
294+
295+
296+
class TestRepeatNdarrayLarge:
297+
298+
@testing.numpy_cupy_array_equal()
299+
def test_large_single(self, xp):
300+
return xp.repeat(
301+
testing.shaped_arange((3,), xp), xp.array([0, 100000, 0])
302+
)
303+
304+
@testing.numpy_cupy_array_equal()
305+
def test_large_broadcast(self, xp):
306+
return xp.repeat(testing.shaped_arange((3,), xp), xp.array([50000]))
307+
308+
313309
@testing.parameterize(
314310
{"reps": 0},
315311
{"reps": 1},

0 commit comments

Comments
 (0)