Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions pandas/core/arraylike.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
from __future__ import annotations

import operator
from typing import Any
from typing import (
TYPE_CHECKING,
Any,
)

import numpy as np

Expand All @@ -21,6 +24,9 @@
from pandas.core.construction import extract_array
from pandas.core.ops.common import unpack_zerodim_and_defer

if TYPE_CHECKING:
from pandas._typing import ArrayLike

REDUCTION_ALIASES = {
"maximum": "max",
"minimum": "min",
Expand All @@ -30,6 +36,41 @@


class OpsMixin:
def _supports_scalar_op(self, other, op_name: str) -> bool:
"""
Return False to have unpack_zerodim_and_defer raise a TypeError with
standardized exception message.

Parameters
----------
other : scalar
The type(other).__name__ will be used for the exception message.
op_name : str

Returns
-------
bool
"""
return True

def _supports_array_op(self, other: ArrayLike, op_name: str) -> bool:
"""
Return False to have unpack_zerodim_and_defer raise a TypeError with
standardized exception message.

Parameters
----------
other : np.ndarray or ExtensionArray
The other.dtype will be used for the exception message.
op_name : str

Returns
-------
bool

"""
return True

# -------------------------------------------------------------
# Comparisons

Expand Down Expand Up @@ -220,7 +261,7 @@ def __rtruediv__(self, other):
def __floordiv__(self, other):
return self._arith_method(other, operator.floordiv)

@unpack_zerodim_and_defer("__rfloordiv")
@unpack_zerodim_and_defer("__rfloordiv__")
def __rfloordiv__(self, other):
return self._arith_method(other, roperator.rfloordiv)

Expand Down
23 changes: 8 additions & 15 deletions pandas/core/arrays/arrow/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -877,16 +877,6 @@ def _cmp_method(self, other, op) -> ArrowExtensionArray:
)
return ArrowExtensionArray(result)

def _op_method_error_message(self, other, op) -> str:
if hasattr(other, "dtype"):
other_type = f"dtype '{other.dtype}'"
else:
other_type = f"object of type {type(other)}"
return (
f"operation '{op.__name__}' not supported for "
f"dtype '{self.dtype}' with {other_type}"
)

def _evaluate_op_method(self, other, op, arrow_funcs) -> Self:
pa_type = self._pa_array.type
other_original = other
Expand All @@ -905,9 +895,10 @@ def _evaluate_op_method(self, other, op, arrow_funcs) -> Self:
elif op is roperator.radd:
result = pc.binary_join_element_wise(other, self._pa_array, sep)
except pa.ArrowNotImplementedError as err:
raise TypeError(
self._op_method_error_message(other_original, op)
) from err
msg = ops.get_op_exception_message(
op.__name__, self, other_original
)
raise TypeError(msg) from err
return self._from_pyarrow_array(result)
elif op in [operator.mul, roperator.rmul]:
binary = self._pa_array
Expand Down Expand Up @@ -940,13 +931,15 @@ def _evaluate_op_method(self, other, op, arrow_funcs) -> Self:
pc_func = arrow_funcs[op.__name__]
if pc_func is NotImplemented:
if pa.types.is_string(pa_type) or pa.types.is_large_string(pa_type):
raise TypeError(self._op_method_error_message(other_original, op))
msg = ops.get_op_exception_message(op.__name__, self, other_original)
raise TypeError(msg)
raise NotImplementedError(f"{op.__name__} not implemented.")

try:
result = pc_func(self._pa_array, other)
except pa.ArrowNotImplementedError as err:
raise TypeError(self._op_method_error_message(other_original, op)) from err
msg = ops.get_op_exception_message(op.__name__, self, other_original)
raise TypeError(msg) from err
return self._from_pyarrow_array(result)

def _logical_method(self, other, op) -> Self:
Expand Down
10 changes: 2 additions & 8 deletions pandas/core/arrays/boolean.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

from pandas._libs import (
lib,
missing as libmissing,
)
from pandas.util._decorators import set_module

Expand Down Expand Up @@ -385,14 +384,9 @@ def _logical_method(self, other, op):
elif isinstance(other, np.bool_):
other = other.item()

if other_is_scalar and other is not libmissing.NA and not lib.is_bool(other):
raise TypeError(
"'other' should be pandas.NA or a bool. "
f"Got {type(other).__name__} instead."
)

if not other_is_scalar and len(self) != len(other):
raise ValueError("Lengths must match")
msg = ops.get_shape_exception_message(self, other)
raise ValueError(msg)

if op.__name__ in {"or_", "ror_"}:
result, mask = ops.kleene_or(self._data, other, self._mask, mask)
Expand Down
6 changes: 6 additions & 0 deletions pandas/core/arrays/categorical.py
Original file line number Diff line number Diff line change
Expand Up @@ -1612,6 +1612,12 @@ def map(
__le__ = _cat_compare_op(operator.le)
__ge__ = _cat_compare_op(operator.ge)

def _supports_scalar_op(self, other, op_name: str) -> bool:
return True

def _supports_array_op(self, other: ArrayLike, op_name: str) -> bool:
return True

# -------------------------------------------------------------
# Validators; ideally these can be de-duplicated

Expand Down
54 changes: 29 additions & 25 deletions pandas/core/arrays/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,8 @@ def _validate_comparison_value(self, other):
raise InvalidComparison(other)

elif len(other) != len(self):
raise ValueError("Lengths must match")
msg = ops.get_shape_exception_message(self, other)
raise ValueError(msg)

else:
try:
Expand Down Expand Up @@ -963,6 +964,12 @@ def _is_unique(self) -> bool:
# ------------------------------------------------------------------
# Arithmetic Methods

def _supports_scalar_op(self, other, op_name):
return True

def _supports_array_op(self, other, op_name):
return True

def _cmp_method(self, other, op):
if self.ndim > 1 and getattr(other, "shape", None) == self.shape:
# TODO: handle 2D-like listlikes
Expand Down Expand Up @@ -1099,9 +1106,8 @@ def _get_arithmetic_result_freq(self, other) -> BaseOffset | None:
@final
def _add_datetimelike_scalar(self, other) -> DatetimeArray:
if not lib.is_np_dtype(self.dtype, "m"):
raise TypeError(
f"cannot add {type(self).__name__} and {type(other).__name__}"
)
msg = ops.get_op_exception_message(self, other)
raise TypeError(msg)

self = cast("TimedeltaArray", self)

Expand Down Expand Up @@ -1133,9 +1139,8 @@ def _add_datetimelike_scalar(self, other) -> DatetimeArray:
@final
def _add_datetime_arraylike(self, other: DatetimeArray) -> DatetimeArray:
if not lib.is_np_dtype(self.dtype, "m"):
raise TypeError(
f"cannot add {type(self).__name__} and {type(other).__name__}"
)
msg = ops.get_op_exception_message(self, other)
raise TypeError(msg)

# defer to DatetimeArray.__add__
return other + self
Expand All @@ -1145,7 +1150,8 @@ def _sub_datetimelike_scalar(
self, other: datetime | np.datetime64
) -> TimedeltaArray:
if self.dtype.kind != "M":
raise TypeError(f"cannot subtract a datelike from a {type(self).__name__}")
msg = ops.get_op_exception_message(self, other)
raise TypeError(msg)

self = cast("DatetimeArray", self)
# subtract a datetime from myself, yielding a ndarray[timedelta64[ns]]
Expand All @@ -1162,10 +1168,8 @@ def _sub_datetimelike_scalar(
@final
def _sub_datetime_arraylike(self, other: DatetimeArray) -> TimedeltaArray:
if self.dtype.kind != "M":
raise TypeError(f"cannot subtract a datelike from a {type(self).__name__}")

if len(self) != len(other):
raise ValueError("cannot add indices of unequal length")
msg = ops.get_op_exception_message(self, other)
raise TypeError(msg)

self = cast("DatetimeArray", self)

Expand Down Expand Up @@ -1195,7 +1199,8 @@ def _sub_datetimelike(self, other: Timestamp | DatetimeArray) -> TimedeltaArray:
@final
def _add_period(self, other: Period) -> PeriodArray:
if not lib.is_np_dtype(self.dtype, "m"):
raise TypeError(f"cannot add Period to a {type(self).__name__}")
msg = ops.get_op_exception_message(self, other)
raise TypeError(msg)

# We will wrap in a PeriodArray and defer to the reversed operation
from pandas.core.arrays.period import PeriodArray
Expand Down Expand Up @@ -1239,7 +1244,8 @@ def _add_timedelta_arraylike(self, other: TimedeltaArray) -> Self:
# overridden by PeriodArray

if len(self) != len(other):
raise ValueError("cannot add indices of unequal length")
msg = ops.get_shape_exception_message(self, other)
raise ValueError(msg)

self, other = cast(
"DatetimeArray | TimedeltaArray", self
Expand Down Expand Up @@ -1267,9 +1273,8 @@ def _add_nat(self) -> Self:
Add pd.NaT to self
"""
if isinstance(self.dtype, PeriodDtype):
raise TypeError(
f"Cannot add {type(self).__name__} and {type(NaT).__name__}"
)
msg = ops.get_op_exception_message(self, NaT)
raise TypeError(msg)

# GH#19124 pd.NaT is treated like a timedelta for both timedelta
# and datetime dtypes
Expand Down Expand Up @@ -1308,9 +1313,8 @@ def _sub_periodlike(self, other: Period | PeriodArray) -> npt.NDArray[np.object_
# If the operation is well-defined, we return an object-dtype ndarray
# of DateOffsets. Null entries are filled with pd.NaT
if not isinstance(self.dtype, PeriodDtype):
raise TypeError(
f"cannot subtract {type(other).__name__} from {type(self).__name__}"
)
msg = ops.get_op_exception_message(self, other)
raise TypeError(msg)

self = cast("PeriodArray", self)
self._check_compatible_with(other)
Expand Down Expand Up @@ -1519,13 +1523,13 @@ def __rsub__(self, other):
elif self.dtype.kind == "M" and hasattr(other, "dtype") and not other_is_dt64:
# GH#19959 datetime - datetime is well-defined as timedelta,
# but any other type - datetime is not well-defined.
raise TypeError(
f"cannot subtract {type(self).__name__} from "
f"{type(other).__name__}[{other.dtype}]"
)
msg = ops.get_op_exception_message(self, other)
raise TypeError(msg)

elif isinstance(self.dtype, PeriodDtype) and lib.is_np_dtype(other_dtype, "m"):
# TODO: Can we simplify/generalize these cases at all?
raise TypeError(f"cannot subtract {type(self).__name__} from {other.dtype}")
msg = ops.get_op_exception_message(self, other)
raise TypeError(msg)
elif lib.is_np_dtype(self.dtype, "m"):
self = cast("TimedeltaArray", self)
return (-self) + other
Expand Down
10 changes: 9 additions & 1 deletion pandas/core/arrays/interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
)
from pandas.core.indexers import check_array_indexer
from pandas.core.ops import (
get_shape_exception_message,
invalid_comparison,
unpack_zerodim_and_defer,
)
Expand Down Expand Up @@ -733,11 +734,18 @@ def __setitem__(self, key, value) -> None:
self._left[key] = value_left
self._right[key] = value_right

def _supports_scalar_op(self, other, op_name: str) -> bool:
return True

def _supports_array_op(self, other: ArrayLike, op_name: str) -> bool:
return True

def _cmp_method(self, other, op):
# ensure pandas array for list-like and eliminate non-interval scalars
if is_list_like(other):
if len(self) != len(other):
raise ValueError("Lengths must match to compare")
msg = get_shape_exception_message(self, other)
raise ValueError(msg)
other = pd_array(other)
elif not isinstance(other, Interval):
# non-interval scalar -> no matches
Expand Down
53 changes: 39 additions & 14 deletions pandas/core/arrays/masked.py
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,43 @@ def _propagate_mask(
mask = self._mask | mask
return mask

def _supports_scalar_op(self, other, op_name: str) -> bool:
if self.dtype.kind == "b":
if op_name.strip("_") in {"or", "ror", "and", "rand", "xor", "rxor"}:
if other is not libmissing.NA and not lib.is_bool(other):
return False

if other is libmissing.NA and op_name.strip("_") in {
"floordiv",
"rfloordiv",
"pow",
"rpow",
"truediv",
"rtruediv",
}:
# GH#41165 Try to match non-masked Series behavior
# This is still imperfect GH#46043
return False

return True

def _supports_array_op(self, other, op_name: str) -> bool:
if self.dtype.kind == "b":
if op_name.strip("_") in {
"floordiv",
"rfloordiv",
"pow",
"rpow",
"truediv",
"rtruediv",
"sub",
"rsub",
}:
# GH#41165 Try to match non-masked Series behavior
# This is still imperfect GH#46043
return False
return True

def _arith_method(self, other, op):
op_name = op.__name__
omask = None
Expand Down Expand Up @@ -770,19 +807,6 @@ def _arith_method(self, other, op):
if other is libmissing.NA:
result = np.ones_like(self._data)
if self.dtype.kind == "b":
if op_name in {
"floordiv",
"rfloordiv",
"pow",
"rpow",
"truediv",
"rtruediv",
}:
# GH#41165 Try to match non-masked Series behavior
# This is still imperfect GH#46043
raise NotImplementedError(
f"operator '{op_name}' not implemented for bool dtypes"
)
if op_name in {"mod", "rmod"}:
dtype = "int8"
else:
Expand Down Expand Up @@ -843,7 +867,8 @@ def _cmp_method(self, other, op) -> BooleanArray:
if other.ndim > 1:
raise NotImplementedError("can only perform ops with 1-d structures")
if len(self) != len(other):
raise ValueError("Lengths must match to compare")
msg = ops.get_shape_exception_message(self, other)
raise ValueError(msg)

if other is libmissing.NA:
# numpy does not handle pd.NA well as "other" scalar (it returns
Expand Down
Loading
Loading