Skip to content

Commit 36d37dc

Browse files
extract overflow-checking wrapper fcn, add tests
1 parent 51eb151 commit 36d37dc

File tree

5 files changed

+72
-13
lines changed

5 files changed

+72
-13
lines changed

pandas/_libs/ops.pxd

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from numpy cimport int64_t
2+
3+
4+
cpdef int64_t calculate(object op, int64_t a, int64_t b) except? -1

pandas/_libs/ops.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ 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: ...

pandas/_libs/ops.pyx

+10
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import numpy as np
1616

1717
from numpy cimport (
1818
import_array,
19+
int64_t,
1920
ndarray,
2021
uint8_t,
2122
)
@@ -308,3 +309,12 @@ def maybe_convert_bool(ndarray[object] arr,
308309
return (arr, None)
309310
else:
310311
return (result.view(np.bool_), None)
312+
313+
314+
@cython.overflowcheck(True)
315+
cpdef int64_t calculate(object op, int64_t a, int64_t b) except? -1:
316+
"""
317+
Calculate op(a, b) and return the result. Raises OverflowError if converting either
318+
operand or the result to an int64_t would overflow.
319+
"""
320+
return op(a, b)

pandas/_libs/tslibs/timedeltas.pyx

+4-13
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ from cpython.datetime cimport (
2929
import_datetime()
3030

3131

32+
from pandas._libs cimport ops
3233
cimport pandas._libs.tslibs.util as util
3334
from pandas._libs.tslibs.base cimport ABCTimestamp
3435
from pandas._libs.tslibs.conversion cimport (
@@ -684,23 +685,13 @@ def _op_unary_method(func, name):
684685
return f
685686

686687

687-
@cython.overflowcheck(True)
688-
cpdef int64_t _calculate(object op, int64_t a, int64_t b) except? -1:
689-
"""
690-
Calculate op(a, b) and return the result. Raises OverflowError if either operand
691-
or the result would overflow on conversion to int64_t.
692-
"""
693-
return op(a, b)
694-
695-
696688
cpdef int64_t calculate(object op, object a, object b) except? -1:
697689
"""
698-
As above, but raises an OutOfBoundsTimedelta.
690+
Calculate op(a, b), raising if either operand or the resulting value cannot be
691+
safely cast to an int64_t.
699692
"""
700-
cdef int64_t int_a, int_b
701-
702693
try:
703-
return _calculate(op, a, b)
694+
return ops.calculate(op, a, b)
704695
except OverflowError as ex:
705696
msg = f"outside allowed range [{TIMEDELTA_MIN_NS}ns, {TIMEDELTA_MAX_NS}ns]"
706697
raise OutOfBoundsTimedelta(msg) from ex

pandas/tests/libs/test_ops.py

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import operator
2+
3+
import numpy as np
4+
import pytest
5+
6+
from pandas._libs import ops
7+
8+
9+
@pytest.fixture(name="int_max")
10+
def fixture_int_max() -> int:
11+
return np.iinfo(np.int64).max
12+
13+
14+
@pytest.fixture(name="int_min")
15+
def fixture_int_min() -> int:
16+
return np.iinfo(np.int64).min
17+
18+
19+
@pytest.fixture(name="overflow_msg")
20+
def fixture_overflow_msg() -> str:
21+
return "Python int too large to convert to C long"
22+
23+
24+
def test_raises_for_too_large_arg(int_max: int, overflow_msg: str):
25+
with pytest.raises(OverflowError, match=overflow_msg):
26+
ops.calculate(operator.add, int_max + 1, 1)
27+
28+
with pytest.raises(OverflowError, match=overflow_msg):
29+
ops.calculate(operator.add, 1, int_max + 1)
30+
31+
32+
def test_raises_for_too_small_arg(int_min: int, overflow_msg: str):
33+
with pytest.raises(OverflowError, match=overflow_msg):
34+
ops.calculate(operator.add, int_min - 1, 1)
35+
36+
with pytest.raises(OverflowError, match=overflow_msg):
37+
ops.calculate(operator.add, 1, int_min - 1)
38+
39+
40+
def test_raises_for_too_large_result(int_max: int, overflow_msg: str):
41+
with pytest.raises(OverflowError, match=overflow_msg):
42+
ops.calculate(operator.add, int_max, 1)
43+
44+
with pytest.raises(OverflowError, match=overflow_msg):
45+
ops.calculate(operator.add, 1, int_max)
46+
47+
48+
def test_raises_for_too_small_result(int_min: int, overflow_msg: str):
49+
with pytest.raises(OverflowError, match=overflow_msg):
50+
ops.calculate(operator.sub, int_min, 1)
51+
52+
with pytest.raises(OverflowError, match=overflow_msg):
53+
ops.calculate(operator.sub, 1, int_min)

0 commit comments

Comments
 (0)