Skip to content

Commit 4247286

Browse files
add wrapper for overflow-checked int/float ops
1 parent 43291f8 commit 4247286

File tree

4 files changed

+122
-27
lines changed

4 files changed

+122
-27
lines changed

pandas/_libs/ops.pxd

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from numpy cimport int64_t
22

33

4-
cpdef int64_t calculate(object op, int64_t a, int64_t b) except? -1
4+
cpdef int64_t calc_int_int(object op, int64_t a, int64_t b) except? -1
5+
cpdef int64_t calc_int_float(object op, int64_t a, double b) except? -1

pandas/_libs/ops.pyi

+2-1
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ def maybe_convert_bool(
4848
*,
4949
convert_to_masked_nullable: Literal[True],
5050
) -> tuple[np.ndarray, np.ndarray]: ...
51-
def calculate(op, left: int, right: int) -> int: ...
51+
def calc_int_int(op, left: int, right: int) -> int: ...
52+
def calc_int_float(op, left: int, right: float) -> int: ...

pandas/_libs/ops.pyx

+9-1
Original file line numberDiff line numberDiff line change
@@ -312,9 +312,17 @@ def maybe_convert_bool(ndarray[object] arr,
312312

313313

314314
@cython.overflowcheck(True)
315-
cpdef int64_t calculate(object op, int64_t a, int64_t b) except? -1:
315+
cpdef int64_t calc_int_int(object op, int64_t a, int64_t b) except? -1:
316316
"""
317317
Calculate op(a, b) and return the result. Raises OverflowError if converting either
318318
operand or the result to an int64_t would overflow.
319319
"""
320320
return op(a, b)
321+
322+
@cython.overflowcheck(True)
323+
cpdef int64_t calc_int_float(object op, int64_t a, double b) except? -1:
324+
"""
325+
Calculate op(a, b) and return the result. Raises OverflowError if converting either
326+
operand or the result would overflow.
327+
"""
328+
return op(a, b)

pandas/tests/libs/test_ops.py

+109-24
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,27 @@
66
from pandas._libs import ops
77

88

9-
@pytest.fixture(name="int_max")
9+
@pytest.fixture(name="int_max", scope="module")
1010
def fixture_int_max() -> int:
1111
return np.iinfo(np.int64).max
1212

1313

14-
@pytest.fixture(name="int_min")
14+
@pytest.fixture(name="int_min", scope="module")
1515
def fixture_int_min() -> int:
1616
return np.iinfo(np.int64).min
1717

1818

19-
@pytest.fixture(name="overflow_msg")
19+
@pytest.fixture(name="float_max", scope="module")
20+
def fixture_float_max() -> int:
21+
return np.finfo(np.float64).max
22+
23+
24+
@pytest.fixture(name="float_min", scope="module")
25+
def fixture_float_min() -> int:
26+
return np.finfo(np.float64).min
27+
28+
29+
@pytest.fixture(name="overflow_msg", scope="module")
2030
def fixture_overflow_msg() -> str:
2131
return "|".join(
2232
(
@@ -26,33 +36,108 @@ def fixture_overflow_msg() -> str:
2636
)
2737

2838

29-
def test_raises_for_too_large_arg(int_max: int, overflow_msg: str):
30-
with pytest.raises(OverflowError, match=overflow_msg):
31-
ops.calculate(operator.add, int_max + 1, 1)
32-
33-
with pytest.raises(OverflowError, match=overflow_msg):
34-
ops.calculate(operator.add, 1, int_max + 1)
39+
class TestCalcIntInt:
40+
def test_raises_for_too_large_arg(self, int_max: int, overflow_msg: str):
41+
with pytest.raises(OverflowError, match=overflow_msg):
42+
ops.calc_int_int(operator.add, int_max + 1, 1)
3543

44+
with pytest.raises(OverflowError, match=overflow_msg):
45+
ops.calc_int_int(operator.add, 1, int_max + 1)
3646

37-
def test_raises_for_too_small_arg(int_min: int, overflow_msg: str):
38-
with pytest.raises(OverflowError, match=overflow_msg):
39-
ops.calculate(operator.add, int_min - 1, 1)
47+
def test_raises_for_too_small_arg(self, int_min: int, overflow_msg: str):
48+
with pytest.raises(OverflowError, match=overflow_msg):
49+
ops.calc_int_int(operator.add, int_min - 1, 1)
4050

41-
with pytest.raises(OverflowError, match=overflow_msg):
42-
ops.calculate(operator.add, 1, int_min - 1)
51+
with pytest.raises(OverflowError, match=overflow_msg):
52+
ops.calc_int_int(operator.add, 1, int_min - 1)
4353

54+
def test_raises_for_too_large_result(self, int_max: int, overflow_msg: str):
55+
with pytest.raises(OverflowError, match=overflow_msg):
56+
ops.calc_int_int(operator.add, int_max, 1)
4457

45-
def test_raises_for_too_large_result(int_max: int, overflow_msg: str):
46-
with pytest.raises(OverflowError, match=overflow_msg):
47-
ops.calculate(operator.add, int_max, 1)
58+
with pytest.raises(OverflowError, match=overflow_msg):
59+
ops.calc_int_int(operator.add, 1, int_max)
4860

49-
with pytest.raises(OverflowError, match=overflow_msg):
50-
ops.calculate(operator.add, 1, int_max)
61+
def test_raises_for_too_small_result(self, int_min: int, overflow_msg: str):
62+
with pytest.raises(OverflowError, match=overflow_msg):
63+
ops.calc_int_int(operator.sub, int_min, 1)
5164

65+
with pytest.raises(OverflowError, match=overflow_msg):
66+
ops.calc_int_int(operator.sub, 1, int_min)
5267

53-
def test_raises_for_too_small_result(int_min: int, overflow_msg: str):
54-
with pytest.raises(OverflowError, match=overflow_msg):
55-
ops.calculate(operator.sub, int_min, 1)
5668

57-
with pytest.raises(OverflowError, match=overflow_msg):
58-
ops.calculate(operator.sub, 1, int_min)
69+
class TestCalcIntFloat:
70+
@pytest.mark.parametrize(
71+
"op,lval,rval,expected",
72+
(
73+
(operator.add, 1, 1.0, 2),
74+
(operator.sub, 2, 1.0, 1),
75+
(operator.mul, 1, 2.0, 2),
76+
(operator.truediv, 1, 0.5, 2),
77+
),
78+
ids=("+", "-", "*", "/"),
79+
)
80+
def test_arithmetic_ops(self, op, lval: int, rval: float, expected: int):
81+
result = ops.calc_int_float(op, lval, rval)
82+
83+
assert result == expected
84+
assert isinstance(result, int)
85+
86+
def test_raises_for_too_large_arg(
87+
self,
88+
int_max: int,
89+
float_max: float,
90+
overflow_msg: str,
91+
):
92+
with pytest.raises(OverflowError, match=overflow_msg):
93+
ops.calc_int_float(operator.add, int_max + 1, 1)
94+
95+
with pytest.raises(OverflowError, match=overflow_msg):
96+
ops.calc_int_float(operator.add, 1, float_max + 1)
97+
98+
def test_raises_for_too_small_arg(
99+
self,
100+
int_min: int,
101+
float_min: float,
102+
overflow_msg: str,
103+
):
104+
with pytest.raises(OverflowError, match=overflow_msg):
105+
ops.calc_int_float(operator.add, int_min - 1, 1)
106+
107+
with pytest.raises(OverflowError, match=overflow_msg):
108+
ops.calc_int_float(operator.add, 1, float_min - 1)
109+
110+
def test_raises_for_too_large_result(
111+
self,
112+
int_max: int,
113+
float_max: float,
114+
overflow_msg: str,
115+
):
116+
with pytest.raises(OverflowError, match=overflow_msg):
117+
ops.calc_int_float(operator.add, int_max, 1)
118+
119+
with pytest.raises(OverflowError, match=overflow_msg):
120+
ops.calc_int_float(operator.add, 1, float_max)
121+
122+
@pytest.mark.parametrize(
123+
"value",
124+
(
125+
pytest.param(
126+
1024,
127+
marks=pytest.mark.xfail(
128+
reason="TBD",
129+
raises=pytest.fail.Exception,
130+
strict=True,
131+
),
132+
),
133+
1024.1,
134+
),
135+
)
136+
def test_raises_for_most_too_small_results(
137+
self,
138+
value: float,
139+
int_min: int,
140+
overflow_msg: str,
141+
):
142+
with pytest.raises(OverflowError, match=overflow_msg):
143+
ops.calc_int_float(operator.sub, int_min, value)

0 commit comments

Comments
 (0)