diff --git a/pandas/core/arrays/base.py b/pandas/core/arrays/base.py index 9f515c38c14ce..f7c4ee35adfe4 100644 --- a/pandas/core/arrays/base.py +++ b/pandas/core/arrays/base.py @@ -739,14 +739,22 @@ def _create_method(cls, op, coerce_to_dtype=True): ---------- op : function An operator that takes arguments op(a, b) - coerce_to_dtype : bool + coerce_to_dtype : bool, default True boolean indicating whether to attempt to convert - the result to the underlying ExtensionArray dtype - (default True) + the result to the underlying ExtensionArray dtype. + If it's not possible to create a new ExtensionArray with the + values, an ndarray is returned instead. Returns ------- - A method that can be bound to a method of a class + Callable[[Any, Any], Union[ndarray, ExtensionArray]] + A method that can be bound to a class. When used, the method + receives the two arguments, one of which is the instance of + this class, and should return an ExtensionArray or an ndarray. + + Returning an ndarray may be necessary when the result of the + `op` cannot be stored in the ExtensionArray. The dtype of the + ndarray uses NumPy's normal inference rules. Example ------- @@ -757,7 +765,6 @@ def _create_method(cls, op, coerce_to_dtype=True): in the class definition of MyExtensionArray to create the operator for addition, that will be based on the operator implementation of the underlying elements of the ExtensionArray - """ def _binop(self, other): @@ -778,7 +785,12 @@ def convert_values(param): try: res = self._from_sequence(res) except Exception: - res = np.asarray(res, dtype=object) + # https://github.com/pandas-dev/pandas/issues/22850 + # We catch all regular exceptions here, and fall back + # to an ndarray. + res = np.asarray(res) + else: + res = np.asarray(res) return res diff --git a/pandas/core/series.py b/pandas/core/series.py index 6eb0ce362c497..2e22e4e6e1bfc 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -2323,12 +2323,14 @@ def combine(self, other, func, fill_value=None): pass elif is_extension_array_dtype(self.values): # The function can return something of any type, so check - # if the type is compatible with the calling EA - # ExtensionArray._from_sequence can raise anything, so we - # have to catch everything. + # if the type is compatible with the calling EA. try: new_values = self._values._from_sequence(new_values) except Exception: + # https://github.com/pandas-dev/pandas/issues/22850 + # pandas has no control over what 3rd-party ExtensionArrays + # do in _values_from_sequence. We still want ops to work + # though, so we catch any regular Exception. pass return self._constructor(new_values, index=new_index, name=new_name) diff --git a/pandas/tests/extension/decimal/test_decimal.py b/pandas/tests/extension/decimal/test_decimal.py index 64872eba6f8f0..dd625d6e1eb3c 100644 --- a/pandas/tests/extension/decimal/test_decimal.py +++ b/pandas/tests/extension/decimal/test_decimal.py @@ -279,6 +279,15 @@ def _from_sequence(cls, scalars, dtype=None, copy=False): raise KeyError("For the test") +class DecimalArrayWithoutCoercion(DecimalArrayWithoutFromSequence): + @classmethod + def _create_arithmetic_method(cls, op): + return cls._create_method(op, coerce_to_dtype=False) + + +DecimalArrayWithoutCoercion._add_arithmetic_ops() + + def test_combine_from_sequence_raises(): # https://github.com/pandas-dev/pandas/issues/22850 ser = pd.Series(DecimalArrayWithoutFromSequence([ @@ -293,8 +302,12 @@ def test_combine_from_sequence_raises(): tm.assert_series_equal(result, expected) -def test_scalar_ops_from_sequence_raises(): - arr = DecimalArrayWithoutFromSequence([ +@pytest.mark.parametrize("class_", [DecimalArrayWithoutFromSequence, + DecimalArrayWithoutCoercion]) +def test_scalar_ops_from_sequence_raises(class_): + # op(EA, EA) should return an EA, or an ndarray if it's not possible + # to return an EA with the return values. + arr = class_([ decimal.Decimal("1.0"), decimal.Decimal("2.0") ])