Skip to content

Commit 40836b6

Browse files
committed
ENH: Allow use of clip with Python integers to always succeed
This helps with NEP 50 behavior since the type is unchanged in that case, but the result is still clearly defined for clipping. This part could be pushed into the loops, but is more tricky to do there. It may make sense if it comes up as a problem with minimum/maximum. However, I think the situation there is slightly less surprising. (Does not preserve NEP 50 warnings/old behavior.) Closes numpygh-26759
1 parent 735a477 commit 40836b6

File tree

3 files changed

+45
-10
lines changed

3 files changed

+45
-10
lines changed

numpy/_core/_methods.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import warnings
99
from contextlib import nullcontext
1010

11+
import numpy as np
1112
from numpy._core import multiarray as mu
1213
from numpy._core import umath as um
1314
from numpy._core.multiarray import asanyarray
@@ -97,10 +98,18 @@ def _count_reduce_items(arr, axis, keepdims=False, where=True):
9798
return items
9899

99100
def _clip(a, min=None, max=None, out=None, **kwargs):
101+
if a.dtype.kind in "iu":
102+
# If min/max is a Python integer, deal with out-of-bound values here.
103+
# (This enforces NEP 50 rules as no value based promotion is done.)
104+
if type(min) is int and min <= np.iinfo(a.dtype).min:
105+
min = None
106+
if type(max) is int and max >= np.iinfo(a.dtype).max:
107+
max = None
108+
100109
if min is None and max is None:
101110
# return identity
102111
return um.positive(a, out=out, **kwargs)
103-
if min is None:
112+
elif min is None:
104113
return um.minimum(a, max, out=out, **kwargs)
105114
elif max is None:
106115
return um.maximum(a, min, out=out, **kwargs)

numpy/_core/tests/test_multiarray.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5091,16 +5091,24 @@ def test_basic(self):
50915091
'uint', 1024, 10, 100, inplace=inplace)
50925092

50935093
@pytest.mark.parametrize("inplace", [False, True])
5094-
def test_int_range_error(self, inplace):
5095-
# E.g. clipping uint with negative integers fails to promote
5096-
# (changed with NEP 50 and may be adaptable)
5097-
# Similar to last check in `test_basic`
5094+
def test_int_out_of_range(self, inplace):
5095+
# Simple check for out-of-bound integers, also testing the in-place
5096+
# path.
50985097
x = (np.random.random(1000) * 255).astype("uint8")
5099-
with pytest.raises(OverflowError):
5100-
x.clip(-1, 10, out=x if inplace else None)
5101-
5102-
with pytest.raises(OverflowError):
5103-
x.clip(0, 256, out=x if inplace else None)
5098+
out = np.empty_like(x)
5099+
res = x.clip(-1, 300, out=out if inplace else None)
5100+
assert res is out or not inplace
5101+
assert (res == x).all()
5102+
5103+
res = x.clip(-1, 50, out=out if inplace else None)
5104+
assert res is out or not inplace
5105+
assert (res <= 50).all()
5106+
assert (res[x <= 50] == x[x <= 50]).all()
5107+
5108+
res = x.clip(100, 1000, out=out if inplace else None)
5109+
assert res is out or not inplace
5110+
assert (res >= 100).all()
5111+
assert (res[x >= 100] == x[x >= 100]).all()
51045112

51055113
def test_record_array(self):
51065114
rec = np.array([(-5, 2.0, 3.0), (5.0, 4.0, 3.0)],

numpy/_core/tests/test_numeric.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2880,6 +2880,24 @@ def test_clip_min_max_args(self):
28802880
with assert_raises_regex(ValueError, msg):
28812881
np.clip(arr, 2, 3, min=2)
28822882

2883+
@pytest.mark.parametrize("dtype,min,max", [
2884+
("int32", -2**32-1, 2**32),
2885+
("int32", -2**320, None),
2886+
("int32", None, 2**300),
2887+
("int32", -1000, 2**32),
2888+
("int32", -2**32-1, 1000),
2889+
("uint8", -1, 129),
2890+
])
2891+
def test_out_of_bound_pyints(self, dtype, min, max):
2892+
a = np.arange(10000).astype(dtype)
2893+
# Check min only
2894+
c = np.clip(a, min=min, max=max)
2895+
assert not np.may_share_memory(a, c)
2896+
assert c.dtype == a.dtype
2897+
if min is not None:
2898+
assert (c >= min).all()
2899+
if max is not None:
2900+
assert (c <= max).all()
28832901

28842902
class TestAllclose:
28852903
rtol = 1e-5

0 commit comments

Comments
 (0)