diff --git a/pandas/core/arraylike.py b/pandas/core/arraylike.py index 51ddd9e91b227..c3eb16f0ed20e 100644 --- a/pandas/core/arraylike.py +++ b/pandas/core/arraylike.py @@ -8,7 +8,10 @@ from __future__ import annotations import operator -from typing import Any +from typing import ( + TYPE_CHECKING, + Any, +) import numpy as np @@ -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", @@ -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 @@ -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) diff --git a/pandas/core/arrays/arrow/array.py b/pandas/core/arrays/arrow/array.py index 2eed608908440..6374f8a1b9312 100644 --- a/pandas/core/arrays/arrow/array.py +++ b/pandas/core/arrays/arrow/array.py @@ -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 @@ -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 @@ -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: diff --git a/pandas/core/arrays/boolean.py b/pandas/core/arrays/boolean.py index aca2cafe80889..de790bad1aa5d 100644 --- a/pandas/core/arrays/boolean.py +++ b/pandas/core/arrays/boolean.py @@ -12,7 +12,6 @@ from pandas._libs import ( lib, - missing as libmissing, ) from pandas.util._decorators import set_module @@ -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) diff --git a/pandas/core/arrays/categorical.py b/pandas/core/arrays/categorical.py index 4b5d2acf008a8..81c50f1a1f6a3 100644 --- a/pandas/core/arrays/categorical.py +++ b/pandas/core/arrays/categorical.py @@ -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 diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index c68b329b00968..b098b4e4b91e5 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -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: @@ -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 @@ -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) @@ -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 @@ -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]] @@ -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) @@ -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 @@ -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 @@ -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 @@ -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) @@ -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 diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index 8d13e76c57e4f..f29dc3e75c034 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -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, ) @@ -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 diff --git a/pandas/core/arrays/masked.py b/pandas/core/arrays/masked.py index bddca5bed6ff8..e72ad9a972584 100644 --- a/pandas/core/arrays/masked.py +++ b/pandas/core/arrays/masked.py @@ -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 @@ -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: @@ -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 diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 180080da4cd00..cbd939efc0684 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -72,6 +72,7 @@ ) from pandas.core.dtypes.missing import isna +from pandas.core import ops from pandas.core.arrays import datetimelike as dtl import pandas.core.common as com @@ -1070,9 +1071,8 @@ def _add_timedelta_arraylike( """ if not self.dtype._is_tick_like(): # We cannot add timedelta-like to non-tick PeriodArray - raise TypeError( - f"Cannot add or subtract timedelta64[ns] dtype from {self.dtype}" - ) + msg = ops.get_op_exception_message("__add__", self, other) + raise TypeError(msg) dtype = np.dtype(f"m8[{self.dtype._td64_unit}]") diff --git a/pandas/core/arrays/sparse/array.py b/pandas/core/arrays/sparse/array.py index c04f3716f4739..5e4ed02efab7a 100644 --- a/pandas/core/arrays/sparse/array.py +++ b/pandas/core/arrays/sparse/array.py @@ -66,7 +66,10 @@ notna, ) -from pandas.core import arraylike +from pandas.core import ( + arraylike, + ops, +) import pandas.core.algorithms as algos from pandas.core.arraylike import OpsMixin from pandas.core.arrays import ExtensionArray @@ -1806,11 +1809,10 @@ def _arith_method(self, other, op): else: other = np.asarray(other) + if len(self) != len(other): + msg = ops.get_shape_exception_message(self, other) + raise ValueError(msg) with np.errstate(all="ignore"): - if len(self) != len(other): - raise AssertionError( - f"length mismatch: {len(self)} vs. {len(other)}" - ) if not isinstance(other, SparseArray): dtype = getattr(other, "dtype", None) other = SparseArray(other, fill_value=self.fill_value, dtype=dtype) @@ -1827,9 +1829,8 @@ def _cmp_method(self, other, op) -> SparseArray: if isinstance(other, SparseArray): if len(self) != len(other): - raise ValueError( - f"operands have mismatched length {len(self)} and {len(other)}" - ) + msg = ops.get_shape_exception_message(self, other) + raise ValueError(msg) op_name = op.__name__.strip("_") return _sparse_array_op(self, other, op, op_name) diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index 64c2e1779aba7..f65eb4f4e41f7 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -56,6 +56,7 @@ from pandas.core import ( nanops, + ops, roperator, ) from pandas.core.array_algos import datetimelike_accumulations @@ -465,9 +466,8 @@ def _format_native_types( def _add_offset(self, other): assert not isinstance(other, (Tick, Day)) - raise TypeError( - f"cannot add the type {type(other).__name__} to a {type(self).__name__}" - ) + msg = ops.get_op_exception_message(self, other) + raise TypeError(msg) @unpack_zerodim_and_defer("__mul__") def __mul__(self, other) -> Self: @@ -482,7 +482,8 @@ def __mul__(self, other) -> Self: if result.dtype.kind != "m": # numpy >= 2.1 may not raise a TypeError # and seems to dispatch to others.__rmul__? - raise TypeError(f"Cannot multiply with {type(other).__name__}") + msg = ops.get_op_exception_message(self, other) + raise TypeError(msg) freq = None if self.freq is not None and not isna(other): freq = self.freq * other @@ -504,7 +505,8 @@ def __mul__(self, other) -> Self: if len(other) != len(self) and not lib.is_np_dtype(other.dtype, "m"): # Exclude timedelta64 here so we correctly raise TypeError # for that instead of ValueError - raise ValueError("Cannot multiply with unequal lengths") + msg = ops.get_shape_exception_message(self, other) + raise ValueError(msg) if is_object_dtype(other.dtype): # this multiplication will succeed only if all elements of other @@ -520,7 +522,8 @@ def __mul__(self, other) -> Self: if result.dtype.kind != "m": # numpy >= 2.1 may not raise a TypeError # and seems to dispatch to others.__rmul__? - raise TypeError(f"Cannot multiply with {type(other).__name__}") + msg = ops.get_op_exception_message(self, other) + raise TypeError(msg) return type(self)._simple_new(result, dtype=result.dtype) __rmul__ = __mul__ @@ -578,7 +581,8 @@ def _cast_divlike_op(self, other): other = np.array(other) if len(other) != len(self): - raise ValueError("Cannot divide vectors with unequal lengths") + msg = ops.get_shape_exception_message(self, other) + raise ValueError(msg) return other def _vector_divlike_op(self, other, op) -> np.ndarray | Self: diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index d75479da70d11..7a32a61f10711 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -7151,11 +7151,6 @@ def _cmp_method(self, other, op): arr[self.isna()] = True return arr - if isinstance(other, (np.ndarray, Index, ABCSeries, ExtensionArray)) and len( - self - ) != len(other): - raise ValueError("Lengths must match to compare") - if not isinstance(other, ABCMultiIndex): other = extract_array(other, extract_numpy=True) else: diff --git a/pandas/core/ops/__init__.py b/pandas/core/ops/__init__.py index 9f9d69a182f72..b35a959dbed40 100644 --- a/pandas/core/ops/__init__.py +++ b/pandas/core/ops/__init__.py @@ -16,7 +16,9 @@ maybe_prepare_scalar_for_op, ) from pandas.core.ops.common import ( + get_op_exception_message, get_op_result_name, + get_shape_exception_message, unpack_zerodim_and_defer, ) from pandas.core.ops.docstrings import make_flex_doc @@ -70,7 +72,9 @@ "comparison_op", "fill_binop", "get_array_op", + "get_op_exception_message", "get_op_result_name", + "get_shape_exception_message", "invalid_comparison", "kleene_and", "kleene_or", diff --git a/pandas/core/ops/array_ops.py b/pandas/core/ops/array_ops.py index ecd2e2e4963d3..594abe82861f8 100644 --- a/pandas/core/ops/array_ops.py +++ b/pandas/core/ops/array_ops.py @@ -55,6 +55,10 @@ from pandas.core.computation import expressions from pandas.core.construction import ensure_wrapped_if_datetimelike from pandas.core.ops import missing +from pandas.core.ops.common import ( + get_op_exception_message, + get_shape_exception_message, +) from pandas.core.ops.dispatch import should_extension_dispatch from pandas.core.ops.invalid import invalid_comparison @@ -122,7 +126,8 @@ def comp_method_OBJECT_ARRAY(op, x, y): y = y._values if x.shape != y.shape: - raise ValueError("Shapes must match", x.shape, y.shape) + msg = get_shape_exception_message(x, y) + raise ValueError(msg) result = libops.vec_compare(x.ravel(), y.ravel(), op) else: result = libops.scalar_compare(x.ravel(), y, op) @@ -149,7 +154,8 @@ def _masked_arith_op(x: np.ndarray, y, op) -> np.ndarray: result = np.empty(x.size, dtype=dtype) if len(x) != len(y): - raise ValueError(x.shape, y.shape) + msg = get_shape_exception_message(x, y) + raise ValueError(msg) ymask = notna(y) # NB: ravel() is only safe since y is ndarray; for e.g. PeriodIndex @@ -163,9 +169,8 @@ def _masked_arith_op(x: np.ndarray, y, op) -> np.ndarray: else: if not is_scalar(y): - raise TypeError( - f"Cannot broadcast np.ndarray with operand of type {type(y)}" - ) + msg = get_op_exception_message(op.__name__, x, y) + raise TypeError(msg) # mask is only meaningful for x result = np.empty(x.size, dtype=x.dtype) @@ -317,9 +322,8 @@ def comparison_op(left: ArrayLike, right: Any, op) -> ArrayLike: # We are not catching all listlikes here (e.g. frozenset, tuple) # The ambiguous case is object-dtype. See GH#27803 if len(lvalues) != len(rvalues): - raise ValueError( - "Lengths must match to compare", lvalues.shape, rvalues.shape - ) + msg = get_shape_exception_message(lvalues, rvalues) + raise ValueError(msg) if should_extension_dispatch(lvalues, rvalues) or ( (isinstance(rvalues, (Timedelta, BaseOffset, Timestamp)) or right is NaT) @@ -380,11 +384,8 @@ def na_logical_op(x: np.ndarray, y, op): OverflowError, NotImplementedError, ) as err: - typ = type(y).__name__ - raise TypeError( - f"Cannot perform '{op.__name__}' with a dtyped [{x.dtype}] array " - f"and scalar of type [{typ}]" - ) from err + msg = get_op_exception_message(op.__name__, x, y) + raise TypeError(msg) from err return result.reshape(x.shape) @@ -597,7 +598,5 @@ def _bool_arith_check(op, a: np.ndarray, b) -> None: """ if op in _BOOL_OP_NOT_ALLOWED: if a.dtype.kind == "b" and (is_bool_dtype(b) or lib.is_bool(b)): - op_name = op.__name__.strip("_").lstrip("r") - raise NotImplementedError( - f"operator '{op_name}' not implemented for bool dtypes" - ) + msg = get_op_exception_message(op.__name__, a, b) + raise TypeError(msg) diff --git a/pandas/core/ops/common.py b/pandas/core/ops/common.py index e0aa4f44fe2be..3de82ca7cde21 100644 --- a/pandas/core/ops/common.py +++ b/pandas/core/ops/common.py @@ -5,12 +5,18 @@ from __future__ import annotations from functools import wraps -from typing import TYPE_CHECKING +from typing import ( + TYPE_CHECKING, + Any, +) + +import numpy as np from pandas._libs.lib import item_from_zerodim from pandas._libs.missing import is_matching_na from pandas.core.dtypes.generic import ( + ABCExtensionArray, ABCIndex, ABCSeries, ) @@ -18,7 +24,40 @@ if TYPE_CHECKING: from collections.abc import Callable - from pandas._typing import F + from pandas._typing import ( + ArrayLike, + F, + ) + + +def get_shape_exception_message(left: ArrayLike, right: ArrayLike) -> str: + """ + Find the standardized exception message to give for operations between + arrays of mismatched length or shape. + """ + if left.ndim == right.ndim == 1: + return "Lengths must match" + else: + return "Shapes must match" + + +def get_op_exception_message(op_name: str, left: ArrayLike, right: Any) -> str: + """ + Find the standardized exception message to give for op(left, right). + """ + if isinstance(right, (np.ndarray, ABCExtensionArray)): + msg = ( + f"Cannot perform operation '{op_name}' between object " + f"with dtype '{left.dtype}' and " + f"dtype '{right.dtype}'" + ) + else: + msg = ( + f"Cannot perform operation '{op_name}' between object " + f"with dtype '{left.dtype}' and " + f"type '{type(right).__name__}'" + ) + return msg def unpack_zerodim_and_defer(name: str) -> Callable[[F], F]: @@ -67,6 +106,20 @@ def new_method(self, other): other = item_from_zerodim(other) + if isinstance(self, ABCExtensionArray): + if isinstance(other, (np.ndarray, ABCExtensionArray)): + if not self._supports_array_op(other, name): + msg = get_op_exception_message(name, self, other) + raise TypeError(msg) + + if other.shape != self.shape: + msg = get_shape_exception_message(self, other) + raise ValueError(msg) + else: + if not self._supports_scalar_op(other, name): + msg = get_op_exception_message(name, self, other) + raise TypeError(msg) + return method(self, other) # error: Incompatible return value type (got "Callable[[Any, Any], Any]",