From 3e1b9eda5eb928cd393e423ac43e9a636b1b0761 Mon Sep 17 00:00:00 2001 From: AryanRG Date: Thu, 29 Oct 2026 16:03:07 +0530 Subject: [PATCH] Fix NumPy clip with read-only integer bounds --- src/array_api_compat/numpy/_aliases.py | 5 ++-- tests/test_numpy.py | 33 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/array_api_compat/numpy/_aliases.py b/src/array_api_compat/numpy/_aliases.py index f20102ee..90adda3a 100644 --- a/src/array_api_compat/numpy/_aliases.py +++ b/src/array_api_compat/numpy/_aliases.py @@ -160,12 +160,13 @@ def _bound_shape(a: object) -> tuple[int, ...]: # this covers integer arrays for float and integer bounds # Also handle cases where the min/max are arrays/lists (replace values below min with iinfo.min and above max with iinfo.max) if np.issubdtype(dtype, np.integer): + # Bounds with the same dtype are already in range and may be read-only. if np.issubdtype(type(min), np.integer) and min <= np.iinfo(dtype).min: min = None elif np.issubdtype(type(min), np.floating) and min < np.iinfo(dtype).min: min = np.iinfo(dtype).min - elif isinstance(min, Array): + elif isinstance(min, Array) and min.dtype != dtype: min[min < np.iinfo(dtype).min] = np.iinfo(dtype).min elif isinstance(min, (list,tuple)): min = np.asarray(min) @@ -176,7 +177,7 @@ def _bound_shape(a: object) -> tuple[int, ...]: max = None elif np.issubdtype(type(max), np.floating) and max > np.iinfo(dtype).max: max = np.iinfo(dtype).max - elif isinstance(max, Array): + elif isinstance(max, Array) and max.dtype != dtype: max[max > np.iinfo(dtype).max] = np.iinfo(dtype).max elif isinstance(max, (list,tuple)): max = np.asarray(max) diff --git a/tests/test_numpy.py b/tests/test_numpy.py index f8d45e2d..751ff2d9 100644 --- a/tests/test_numpy.py +++ b/tests/test_numpy.py @@ -132,6 +132,39 @@ def test_numpy_clip_returns_copy_when_unbounded(): np.testing.assert_array_equal(y, x) +@pytest.mark.parametrize("dtype", [np.int8, np.int64, np.uint8, np.uint64]) +@pytest.mark.parametrize("bound_name", ["min", "max"]) +@pytest.mark.parametrize("broadcast", [False, True]) +@pytest.mark.parametrize("use_out", [False, True]) +def test_numpy_clip_readonly_integer_bounds(dtype, bound_name, broadcast, use_out): + x = xp.asarray([[0, 3, 9], [4, 8, 1]], dtype=dtype) + if broadcast: + bound = xp.broadcast_to(xp.asarray(2 if bound_name == "min" else 5, dtype=dtype), (3,)) + expected = [[2, 3, 9], [4, 8, 2]] if bound_name == "min" else [[0, 3, 5], [4, 5, 1]] + else: + bound = xp.asarray([2, 3, 4] if bound_name == "min" else [5, 6, 7], dtype=dtype) + bound.flags.writeable = False + expected = [[2, 3, 9], [4, 8, 4]] if bound_name == "min" else [[0, 3, 7], [4, 6, 1]] + + original_bound = bound.copy() + assert not bound.flags.writeable + kwargs = {bound_name: bound} + if use_out: + kwargs["out"] = xp.empty_like(x) + result = xp.clip(x, **kwargs) + + np.testing.assert_array_equal(result, xp.asarray(expected, dtype=dtype)) + assert result.dtype == x.dtype + if use_out: + np.testing.assert_array_equal(kwargs["out"], result) + assert np.shares_memory(kwargs["out"], result) + else: + assert not np.shares_memory(x, result) + np.testing.assert_array_equal(x, xp.asarray([[0, 3, 9], [4, 8, 1]], dtype=dtype)) + np.testing.assert_array_equal(bound, original_bound) + assert not bound.flags.writeable + + def test_matrix_is_not_array_api_obj(): assert is_array_api_obj(np.asarray(3)) assert is_array_api_obj(np.float64(3))