diff --git a/python/pyarrow/array.pxi b/python/pyarrow/array.pxi index 3060c533255..ce248f4793e 100644 --- a/python/pyarrow/array.pxi +++ b/python/pyarrow/array.pxi @@ -1123,6 +1123,19 @@ cdef PandasOptions _convert_pandas_options(dict options): return result +def _compute_binary_op(func_name, left, right): + """ + Helper for arithmetic/bitwise dunder methods. + + Only use for ops that can't raise ArrowTypeError as it + subclasses TypeError, so will get swallowed. + """ + try: + return _pc().call_function(func_name, [left, right]) + except TypeError: + return NotImplemented + + cdef class Array(_PandasConvertible): """ The base class for all Arrow arrays. @@ -2440,15 +2453,15 @@ cdef class Array(_PandasConvertible): def __add__(self, object other): self._assert_cpu() - return _pc().call_function('add_checked', [self, other]) + return _compute_binary_op('add_checked', self, other) def __truediv__(self, object other): self._assert_cpu() - return _pc().call_function('divide_checked', [self, other]) + return _compute_binary_op('divide_checked', self, other) def __mul__(self, object other): self._assert_cpu() - return _pc().call_function('multiply_checked', [self, other]) + return _compute_binary_op('multiply_checked', self, other) def __neg__(self): self._assert_cpu() @@ -2456,31 +2469,31 @@ cdef class Array(_PandasConvertible): def __pow__(self, object other): self._assert_cpu() - return _pc().call_function('power_checked', [self, other]) + return _compute_binary_op('power_checked', self, other) def __sub__(self, object other): self._assert_cpu() - return _pc().call_function('subtract_checked', [self, other]) + return _compute_binary_op('subtract_checked', self, other) def __and__(self, object other): self._assert_cpu() - return _pc().call_function('bit_wise_and', [self, other]) + return _compute_binary_op('bit_wise_and', self, other) def __or__(self, object other): self._assert_cpu() - return _pc().call_function('bit_wise_or', [self, other]) + return _compute_binary_op('bit_wise_or', self, other) def __xor__(self, object other): self._assert_cpu() - return _pc().call_function('bit_wise_xor', [self, other]) + return _compute_binary_op('bit_wise_xor', self, other) def __lshift__(self, object other): self._assert_cpu() - return _pc().call_function('shift_left_checked', [self, other]) + return _compute_binary_op('shift_left_checked', self, other) def __rshift__(self, object other): self._assert_cpu() - return _pc().call_function('shift_right_checked', [self, other]) + return _compute_binary_op('shift_right_checked', self, other) cdef _array_like_to_pandas(obj, options, types_mapper): diff --git a/python/pyarrow/scalar.pxi b/python/pyarrow/scalar.pxi index 863ab2b66a8..f96fb40572b 100644 --- a/python/pyarrow/scalar.pxi +++ b/python/pyarrow/scalar.pxi @@ -199,37 +199,37 @@ cdef class Scalar(_Weakrefable): return _pc().call_function('abs_checked', [self]) def __add__(self, object other): - return _pc().call_function('add_checked', [self, other]) + return _compute_binary_op('add_checked', self, other) def __truediv__(self, object other): - return _pc().call_function('divide_checked', [self, other]) + return _compute_binary_op('divide_checked', self, other) def __mul__(self, object other): - return _pc().call_function('multiply_checked', [self, other]) + return _compute_binary_op('multiply_checked', self, other) def __neg__(self): return _pc().call_function('negate_checked', [self]) def __pow__(self, object other): - return _pc().call_function('power_checked', [self, other]) + return _compute_binary_op('power_checked', self, other) def __sub__(self, object other): - return _pc().call_function('subtract_checked', [self, other]) + return _compute_binary_op('subtract_checked', self, other) def __and__(self, object other): - return _pc().call_function('bit_wise_and', [self, other]) + return _compute_binary_op('bit_wise_and', self, other) def __or__(self, object other): - return _pc().call_function('bit_wise_or', [self, other]) + return _compute_binary_op('bit_wise_or', self, other) def __xor__(self, object other): - return _pc().call_function('bit_wise_xor', [self, other]) + return _compute_binary_op('bit_wise_xor', self, other) def __lshift__(self, object other): - return _pc().call_function('shift_left_checked', [self, other]) + return _compute_binary_op('shift_left_checked', self, other) def __rshift__(self, object other): - return _pc().call_function('shift_right_checked', [self, other]) + return _compute_binary_op('shift_right_checked', self, other) _NULL = NA = None diff --git a/python/pyarrow/tests/test_array.py b/python/pyarrow/tests/test_array.py index a1e3616c9ce..9d3c6014a81 100644 --- a/python/pyarrow/tests/test_array.py +++ b/python/pyarrow/tests/test_array.py @@ -21,6 +21,7 @@ import hypothesis as h import hypothesis.strategies as st import itertools +import operator import pytest import struct import subprocess @@ -4612,3 +4613,36 @@ def test_dictionary_uint64_index_to_pandas(): result = arr.to_pandas() assert list(result.cat.categories) == ["a", "b"] assert result.cat.codes.tolist() == [0, 1, -1, 0] + + +@pytest.mark.parametrize("op", [ + operator.add, + operator.sub, + operator.mul, + operator.truediv, + operator.pow, + operator.and_, + operator.or_, + operator.xor, + operator.lshift, + operator.rshift, +]) +def test_arithmetic_dunders_unknown_types(op): + # GH-49826 + class MyObj: + def __radd__(self, other): + return "reflected" + + __rsub__ = __rmul__ = __rtruediv__ = __rpow__ = __radd__ + __rand__ = __ror__ = __rxor__ = __rlshift__ = __rrshift__ = __radd__ + + assert op(pa.array([1, 2, 3]), MyObj()) == "reflected" + + with pytest.raises(TypeError, match="unsupported operand type"): + op(pa.array([1, 2, 3]), object()) + + +def test_arithmetic_dunder_raises_arrow_invalid(): + # GH-49826 + with pytest.raises(pa.ArrowInvalid, match="divide by zero"): + pa.array([1, 2, 3]) / pa.scalar(0) diff --git a/python/pyarrow/tests/test_scalars.py b/python/pyarrow/tests/test_scalars.py index 08f9fcd55ce..a71915b73aa 100644 --- a/python/pyarrow/tests/test_scalars.py +++ b/python/pyarrow/tests/test_scalars.py @@ -17,6 +17,7 @@ import datetime import decimal +import operator import pytest import weakref from collections.abc import Sequence, Mapping @@ -1051,3 +1052,36 @@ def test_dunders_checked_overflow(): scl ** scl with pytest.raises(pa.ArrowInvalid, match=error_match): scl * scl + + +@pytest.mark.parametrize("op", [ + operator.add, + operator.sub, + operator.mul, + operator.truediv, + operator.pow, + operator.and_, + operator.or_, + operator.xor, + operator.lshift, + operator.rshift, +]) +def test_arithmetic_dunders_unknown_types(op): + # GH-49826 + class MyObj: + def __radd__(self, other): + return "reflected" + + __rsub__ = __rmul__ = __rtruediv__ = __rpow__ = __radd__ + __rand__ = __ror__ = __rxor__ = __rlshift__ = __rrshift__ = __radd__ + + assert op(pa.scalar(5), MyObj()) == "reflected" + + with pytest.raises(TypeError, match="unsupported operand type"): + op(pa.scalar(1), object()) + + +def test_arithmetic_dunder_raises_arrow_invalid(): + # GH-49826 + with pytest.raises(pa.ArrowInvalid, match="divide by zero"): + pa.scalar(1) / pa.scalar(0)