diff --git a/numpy/_core/_methods.py b/numpy/_core/_methods.py index 374fad91d90c..9d5914968388 100644 --- a/numpy/_core/_methods.py +++ b/numpy/_core/_methods.py @@ -93,12 +93,39 @@ def _clip(a, min=None, max=None, out=None, **kwargs): if min is None and max is None: raise ValueError("One of max or min must be given") - if min is None: - return um.minimum(a, max, out=out, **kwargs) - elif max is None: - return um.maximum(a, min, out=out, **kwargs) - else: - return um.clip(a, min, max, out=out, **kwargs) + try: + if min is None: + return um.minimum(a, max, out=out, **kwargs) + elif max is None: + return um.maximum(a, min, out=out, **kwargs) + else: + return um.clip(a, min, max, out=out, **kwargs) + except OverflowError: + # If there were python ints, an OverflowError due NEP-50 casting + # might be responsible. In that case, we try again. + # (We do not do this up front since this is a rare case, and + # we do not want to slow down the common path.) + if a.dtype.kind not in "iu": + raise + + # Circular import if done on top. + from numpy._core.getlimits import iinfo + a_info = iinfo(a.dtype) + changed = False + if isinstance(min, int) and min < a_info.min: + min = a_info.min + changed = True + if isinstance(max, int) and max > a_info.max: + max = a_info.max + changed = True + if not changed: + raise + # try/except just in case the error was unrelated somehow. + try: + return _clip(a, min, max, out=out, **kwargs) + except Exception: + pass + raise def _mean(a, axis=None, dtype=None, out=None, keepdims=False, *, where=True): arr = asanyarray(a) diff --git a/numpy/_core/tests/test_multiarray.py b/numpy/_core/tests/test_multiarray.py index 918d6b24eac3..9f2ac8234ff8 100644 --- a/numpy/_core/tests/test_multiarray.py +++ b/numpy/_core/tests/test_multiarray.py @@ -5015,17 +5015,28 @@ def test_basic(self): self._clip_type( 'uint', 1024, 10, 100, inplace=inplace) - @pytest.mark.parametrize("inplace", [False, True]) - def test_int_range_error(self, inplace): - # E.g. clipping uint with negative integers fails to promote - # (changed with NEP 50 and may be adaptable) - # Similar to last check in `test_basic` - x = (np.random.random(1000) * 255).astype("uint8") - with pytest.raises(OverflowError): - x.clip(-1, 10, out=x if inplace else None) - - with pytest.raises(OverflowError): - x.clip(0, 256, out=x if inplace else None) + @pytest.mark.parametrize("do_inplace, min, max, emin, emax", [ + (True, -1, 10, 0, 10), + (True, -1, None, 0, 255), + (False, np.int8(-1), None, 0, 255), + (True, 10, 400, 10, 255), + (True, None, 400, 0, 255), + (True, -10, 256, 0, 255), + (False, np.int8(-10), 256, 0, 255), + (False, -10, np.uint16(256), 0, 255), + ]) + def test_int_out_of_range(self, do_inplace, min, max, emin, emax): + # E.g. clipping uint with negative python integers fails to promote + # for ufunc, but we special-case it in _methods._clip. + # For numpy integers, promotion works, but then the output dtype + # changes, so we cannot do this in-place. + x = np.arange(0, 256, dtype="uint8") + result = x.clip(min, max) + self._check_range(result, emin, emax) + if do_inplace: + result2 = x.clip(min, max, out=x) + assert result2 is x + assert_array_equal(result2, result) def test_record_array(self): rec = np.array([(-5, 2.0, 3.0), (5.0, 4.0, 3.0)],