Skip to content

Commit c19c805

Browse files
TomAugspurgerjreback
authored andcommitted
Catch Exception in combine (pandas-dev#22936)
1 parent d553ab3 commit c19c805

File tree

8 files changed

+97
-25
lines changed

8 files changed

+97
-25
lines changed

pandas/core/arrays/base.py

+19-7
Original file line numberDiff line numberDiff line change
@@ -739,14 +739,22 @@ def _create_method(cls, op, coerce_to_dtype=True):
739739
----------
740740
op : function
741741
An operator that takes arguments op(a, b)
742-
coerce_to_dtype : bool
742+
coerce_to_dtype : bool, default True
743743
boolean indicating whether to attempt to convert
744-
the result to the underlying ExtensionArray dtype
745-
(default True)
744+
the result to the underlying ExtensionArray dtype.
745+
If it's not possible to create a new ExtensionArray with the
746+
values, an ndarray is returned instead.
746747
747748
Returns
748749
-------
749-
A method that can be bound to a method of a class
750+
Callable[[Any, Any], Union[ndarray, ExtensionArray]]
751+
A method that can be bound to a class. When used, the method
752+
receives the two arguments, one of which is the instance of
753+
this class, and should return an ExtensionArray or an ndarray.
754+
755+
Returning an ndarray may be necessary when the result of the
756+
`op` cannot be stored in the ExtensionArray. The dtype of the
757+
ndarray uses NumPy's normal inference rules.
750758
751759
Example
752760
-------
@@ -757,7 +765,6 @@ def _create_method(cls, op, coerce_to_dtype=True):
757765
in the class definition of MyExtensionArray to create the operator
758766
for addition, that will be based on the operator implementation
759767
of the underlying elements of the ExtensionArray
760-
761768
"""
762769

763770
def _binop(self, other):
@@ -777,8 +784,13 @@ def convert_values(param):
777784
if coerce_to_dtype:
778785
try:
779786
res = self._from_sequence(res)
780-
except TypeError:
781-
pass
787+
except Exception:
788+
# https://github.com/pandas-dev/pandas/issues/22850
789+
# We catch all regular exceptions here, and fall back
790+
# to an ndarray.
791+
res = np.asarray(res)
792+
else:
793+
res = np.asarray(res)
782794

783795
return res
784796

pandas/core/series.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -2323,10 +2323,14 @@ def combine(self, other, func, fill_value=None):
23232323
pass
23242324
elif is_extension_array_dtype(self.values):
23252325
# The function can return something of any type, so check
2326-
# if the type is compatible with the calling EA
2326+
# if the type is compatible with the calling EA.
23272327
try:
23282328
new_values = self._values._from_sequence(new_values)
2329-
except TypeError:
2329+
except Exception:
2330+
# https://github.com/pandas-dev/pandas/issues/22850
2331+
# pandas has no control over what 3rd-party ExtensionArrays
2332+
# do in _values_from_sequence. We still want ops to work
2333+
# though, so we catch any regular Exception.
23302334
pass
23312335

23322336
return self._constructor(new_values, index=new_index, name=new_name)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .array import DecimalArray, DecimalDtype, to_decimal, make_data
2+
3+
4+
__all__ = ['DecimalArray', 'DecimalDtype', 'to_decimal', 'make_data']

pandas/tests/extension/decimal/array.py

+9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import decimal
22
import numbers
3+
import random
34
import sys
45

56
import numpy as np
@@ -138,5 +139,13 @@ def _concat_same_type(cls, to_concat):
138139
return cls(np.concatenate([x._data for x in to_concat]))
139140

140141

142+
def to_decimal(values, context=None):
143+
return DecimalArray([decimal.Decimal(x) for x in values], context=context)
144+
145+
146+
def make_data():
147+
return [decimal.Decimal(random.random()) for _ in range(100)]
148+
149+
141150
DecimalArray._add_arithmetic_ops()
142151
DecimalArray._add_comparison_ops()

pandas/tests/extension/decimal/test_decimal.py

+46-6
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
1+
import operator
12
import decimal
23

3-
import random
44
import numpy as np
55
import pandas as pd
66
import pandas.util.testing as tm
77
import pytest
88

99
from pandas.tests.extension import base
1010

11-
from .array import DecimalDtype, DecimalArray
12-
13-
14-
def make_data():
15-
return [decimal.Decimal(random.random()) for _ in range(100)]
11+
from .array import DecimalDtype, DecimalArray, make_data
1612

1713

1814
@pytest.fixture
@@ -275,3 +271,47 @@ def test_compare_array(self, data, all_compare_operators):
275271
other = pd.Series(data) * [decimal.Decimal(pow(2.0, i))
276272
for i in alter]
277273
self._compare_other(s, data, op_name, other)
274+
275+
276+
class DecimalArrayWithoutFromSequence(DecimalArray):
277+
"""Helper class for testing error handling in _from_sequence."""
278+
def _from_sequence(cls, scalars, dtype=None, copy=False):
279+
raise KeyError("For the test")
280+
281+
282+
class DecimalArrayWithoutCoercion(DecimalArrayWithoutFromSequence):
283+
@classmethod
284+
def _create_arithmetic_method(cls, op):
285+
return cls._create_method(op, coerce_to_dtype=False)
286+
287+
288+
DecimalArrayWithoutCoercion._add_arithmetic_ops()
289+
290+
291+
def test_combine_from_sequence_raises():
292+
# https://github.com/pandas-dev/pandas/issues/22850
293+
ser = pd.Series(DecimalArrayWithoutFromSequence([
294+
decimal.Decimal("1.0"),
295+
decimal.Decimal("2.0")
296+
]))
297+
result = ser.combine(ser, operator.add)
298+
299+
# note: object dtype
300+
expected = pd.Series([decimal.Decimal("2.0"),
301+
decimal.Decimal("4.0")], dtype="object")
302+
tm.assert_series_equal(result, expected)
303+
304+
305+
@pytest.mark.parametrize("class_", [DecimalArrayWithoutFromSequence,
306+
DecimalArrayWithoutCoercion])
307+
def test_scalar_ops_from_sequence_raises(class_):
308+
# op(EA, EA) should return an EA, or an ndarray if it's not possible
309+
# to return an EA with the return values.
310+
arr = class_([
311+
decimal.Decimal("1.0"),
312+
decimal.Decimal("2.0")
313+
])
314+
result = arr + arr
315+
expected = np.array([decimal.Decimal("2.0"), decimal.Decimal("4.0")],
316+
dtype="object")
317+
tm.assert_numpy_array_equal(result, expected)
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .array import JSONArray, JSONDtype, make_data
2+
3+
__all__ = ['JSONArray', 'JSONDtype', 'make_data']

pandas/tests/extension/json/array.py

+9
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import collections
1414
import itertools
1515
import numbers
16+
import random
17+
import string
1618
import sys
1719

1820
import numpy as np
@@ -179,3 +181,10 @@ def _values_for_argsort(self):
179181
# cast them to an (N, P) array, instead of an (N,) array of tuples.
180182
frozen = [()] + [tuple(x.items()) for x in self]
181183
return np.array(frozen, dtype=object)[1:]
184+
185+
186+
def make_data():
187+
# TODO: Use a regular dict. See _NDFrameIndexer._setitem_with_indexer
188+
return [collections.UserDict([
189+
(random.choice(string.ascii_letters), random.randint(0, 100))
190+
for _ in range(random.randint(0, 10))]) for _ in range(100)]

pandas/tests/extension/json/test_json.py

+1-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import operator
22
import collections
3-
import random
4-
import string
53

64
import pytest
75

@@ -10,18 +8,11 @@
108
from pandas.compat import PY2, PY36
119
from pandas.tests.extension import base
1210

13-
from .array import JSONArray, JSONDtype
11+
from .array import JSONArray, JSONDtype, make_data
1412

1513
pytestmark = pytest.mark.skipif(PY2, reason="Py2 doesn't have a UserDict")
1614

1715

18-
def make_data():
19-
# TODO: Use a regular dict. See _NDFrameIndexer._setitem_with_indexer
20-
return [collections.UserDict([
21-
(random.choice(string.ascii_letters), random.randint(0, 100))
22-
for _ in range(random.randint(0, 10))]) for _ in range(100)]
23-
24-
2516
@pytest.fixture
2617
def dtype():
2718
return JSONDtype()

0 commit comments

Comments
 (0)