From a0bd29d04ffafeb342938503126b828a876a9099 Mon Sep 17 00:00:00 2001 From: Daniel Saxton <2658661+dsaxton@users.noreply.github.com> Date: Sun, 13 Sep 2020 07:44:15 -0500 Subject: [PATCH] Backport PR #36303: REGR: Fix IntegerArray unary ops regression --- doc/source/whatsnew/v1.1.3.rst | 1 + pandas/conftest.py | 13 ++++++ pandas/core/arrays/integer.py | 9 ++++ pandas/core/generic.py | 5 ++- .../tests/arrays/integer/test_arithmetic.py | 38 +++++++++++++++++ pandas/tests/frame/test_operators.py | 2 +- pandas/tests/series/test_operators.py | 41 +++++++++++++++++++ 7 files changed, 107 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v1.1.3.rst b/doc/source/whatsnew/v1.1.3.rst index 25d223418fc92..c78adcf42f56b 100644 --- a/doc/source/whatsnew/v1.1.3.rst +++ b/doc/source/whatsnew/v1.1.3.rst @@ -14,6 +14,7 @@ including other versions of pandas. Fixed regressions ~~~~~~~~~~~~~~~~~ +- Fixed regression in :class:`IntegerArray` unary plus and minus operations raising a ``TypeError`` (:issue:`36063`) - Fixed regression in :meth:`Series.__getitem__` incorrectly raising when the input was a tuple (:issue:`35534`) - Fixed regression in :meth:`Series.__getitem__` incorrectly raising when the input was a frozenset (:issue:`35747`) - diff --git a/pandas/conftest.py b/pandas/conftest.py index e0adb37e7d2f5..74cab2e038a27 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -1032,6 +1032,19 @@ def any_nullable_int_dtype(request): return request.param +@pytest.fixture(params=tm.SIGNED_EA_INT_DTYPES) +def any_signed_nullable_int_dtype(request): + """ + Parameterized fixture for any signed nullable integer dtype. + + * 'Int8' + * 'Int16' + * 'Int32' + * 'Int64' + """ + return request.param + + @pytest.fixture(params=tm.ALL_REAL_DTYPES) def any_real_dtype(request): """ diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py index 57df067c7b16e..0cbcd905e4a8c 100644 --- a/pandas/core/arrays/integer.py +++ b/pandas/core/arrays/integer.py @@ -364,6 +364,15 @@ def __init__(self, values: np.ndarray, mask: np.ndarray, copy: bool = False): ) super().__init__(values, mask, copy=copy) + def __neg__(self): + return type(self)(-self._data, self._mask) + + def __pos__(self): + return self + + def __abs__(self): + return type(self)(np.abs(self._data), self._mask) + @classmethod def _from_sequence(cls, scalars, dtype=None, copy: bool = False) -> "IntegerArray": return integer_array(scalars, dtype=dtype, copy=copy) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 935bad2624637..be85ab251c0c3 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -1310,7 +1310,10 @@ def __pos__(self): ): arr = operator.pos(values) else: - raise TypeError(f"Unary plus expects numeric dtype, not {values.dtype}") + raise TypeError( + "Unary plus expects bool, numeric, timedelta, " + f"or object dtype, not {values.dtype}" + ) return self.__array_wrap__(arr) def __invert__(self): diff --git a/pandas/tests/arrays/integer/test_arithmetic.py b/pandas/tests/arrays/integer/test_arithmetic.py index d309f6423e0c1..f549a7caeab1d 100644 --- a/pandas/tests/arrays/integer/test_arithmetic.py +++ b/pandas/tests/arrays/integer/test_arithmetic.py @@ -261,3 +261,41 @@ def test_reduce_to_float(op): index=pd.Index(["a", "b"], name="A"), ) tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize( + "source, target", + [ + ([1, 2, 3], [-1, -2, -3]), + ([1, 2, None], [-1, -2, None]), + ([-1, 0, 1], [1, 0, -1]), + ], +) +def test_unary_minus_nullable_int(any_signed_nullable_int_dtype, source, target): + dtype = any_signed_nullable_int_dtype + arr = pd.array(source, dtype=dtype) + result = -arr + expected = pd.array(target, dtype=dtype) + tm.assert_extension_array_equal(result, expected) + + +@pytest.mark.parametrize( + "source", [[1, 2, 3], [1, 2, None], [-1, 0, 1]], +) +def test_unary_plus_nullable_int(any_signed_nullable_int_dtype, source): + dtype = any_signed_nullable_int_dtype + expected = pd.array(source, dtype=dtype) + result = +expected + tm.assert_extension_array_equal(result, expected) + + +@pytest.mark.parametrize( + "source, target", + [([1, 2, 3], [1, 2, 3]), ([1, -2, None], [1, 2, None]), ([-1, 0, 1], [1, 0, 1])], +) +def test_abs_nullable_int(any_signed_nullable_int_dtype, source, target): + dtype = any_signed_nullable_int_dtype + s = pd.array(source, dtype=dtype) + result = abs(s) + expected = pd.array(target, dtype=dtype) + tm.assert_extension_array_equal(result, expected) diff --git a/pandas/tests/frame/test_operators.py b/pandas/tests/frame/test_operators.py index fede1ca23a8ce..8cf66e2737249 100644 --- a/pandas/tests/frame/test_operators.py +++ b/pandas/tests/frame/test_operators.py @@ -119,7 +119,7 @@ def test_pos_object(self, df): "df", [pd.DataFrame({"a": pd.to_datetime(["2017-01-22", "1970-01-01"])})] ) def test_pos_raises(self, df): - msg = re.escape("Unary plus expects numeric dtype, not datetime64[ns]") + msg = "Unary plus expects .* dtype, not datetime64\\[ns\\]" with pytest.raises(TypeError, match=msg): (+df) with pytest.raises(TypeError, match=msg): diff --git a/pandas/tests/series/test_operators.py b/pandas/tests/series/test_operators.py index e1c9682329271..aee947e738525 100644 --- a/pandas/tests/series/test_operators.py +++ b/pandas/tests/series/test_operators.py @@ -536,3 +536,44 @@ def test_invert(self): ser = tm.makeStringSeries() ser.name = "series" tm.assert_series_equal(-(ser < 0), ~(ser < 0)) + + @pytest.mark.parametrize( + "source, target", + [ + ([1, 2, 3], [-1, -2, -3]), + ([1, 2, None], [-1, -2, None]), + ([-1, 0, 1], [1, 0, -1]), + ], + ) + def test_unary_minus_nullable_int( + self, any_signed_nullable_int_dtype, source, target + ): + dtype = any_signed_nullable_int_dtype + s = pd.Series(source, dtype=dtype) + result = -s + expected = pd.Series(target, dtype=dtype) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize( + "source", [[1, 2, 3], [1, 2, None], [-1, 0, 1]], + ) + def test_unary_plus_nullable_int(self, any_signed_nullable_int_dtype, source): + dtype = any_signed_nullable_int_dtype + expected = pd.Series(source, dtype=dtype) + result = +expected + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize( + "source, target", + [ + ([1, 2, 3], [1, 2, 3]), + ([1, -2, None], [1, 2, None]), + ([-1, 0, 1], [1, 0, 1]), + ], + ) + def test_abs_nullable_int(self, any_signed_nullable_int_dtype, source, target): + dtype = any_signed_nullable_int_dtype + s = pd.Series(source, dtype=dtype) + result = abs(s) + expected = pd.Series(target, dtype=dtype) + tm.assert_series_equal(result, expected)