From b71171cfe81b42ab17c009b8ecd9de9792d01d7a Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Fri, 11 Sep 2020 23:02:36 -0500 Subject: [PATCH 01/10] REGR: Fix IntegerArray unary ops regression --- doc/source/whatsnew/v1.1.3.rst | 2 +- pandas/core/arrays/integer.py | 3 +++ pandas/core/generic.py | 2 +- pandas/tests/series/test_arithmetic.py | 24 ++++++++++++++++++++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v1.1.3.rst b/doc/source/whatsnew/v1.1.3.rst index e3161012da5d1..4b3e54a8ae988 100644 --- a/doc/source/whatsnew/v1.1.3.rst +++ b/doc/source/whatsnew/v1.1.3.rst @@ -14,7 +14,7 @@ including other versions of pandas. Fixed regressions ~~~~~~~~~~~~~~~~~ -- +- Fixed regression in :class:`IntegerArray` unary plus and minus operations raising a ``TypeError`` (:issue:`36063`) .. --------------------------------------------------------------------------- diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py index d83ff91a1315f..f601cf1f7ad44 100644 --- a/pandas/core/arrays/integer.py +++ b/pandas/core/arrays/integer.py @@ -364,6 +364,9 @@ def __init__(self, values: np.ndarray, mask: np.ndarray, copy: bool = False): ) super().__init__(values, mask, copy=copy) + def __neg__(self): + return type(self)(0 - 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 fffd2e068ebcf..bb7e6e482841b 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -1408,7 +1408,7 @@ def __neg__(self): def __pos__(self): values = self._values - if is_bool_dtype(values): + if is_bool_dtype(values) or is_numeric_dtype(values): arr = values elif ( is_numeric_dtype(values) diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py index c937e357b9dbc..480a8c44baaf7 100644 --- a/pandas/tests/series/test_arithmetic.py +++ b/pandas/tests/series/test_arithmetic.py @@ -687,3 +687,27 @@ def test_datetime_understood(self): result = series - offset expected = pd.Series(pd.to_datetime(["2011-12-26", "2011-12-27", "2011-12-28"])) tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16"]) + @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, dtype, source, target): + s = pd.Series(source, dtype=dtype) + result = -s + expected = pd.Series(target, dtype=dtype) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16"]) + @pytest.mark.parametrize( + "source", [[1, 2, 3], [1, 2, None], [-1, 0, 1]], + ) + def test_unary_plus_nullable_int(self, dtype, source): + expected = pd.Series(source, dtype=dtype) + result = +expected + tm.assert_series_equal(result, expected) From 20ef382b6bce0266a8b04d325f8ff37218bce41e Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Fri, 11 Sep 2020 23:15:50 -0500 Subject: [PATCH 02/10] Add type --- pandas/tests/series/test_arithmetic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py index 480a8c44baaf7..c16e74af34cab 100644 --- a/pandas/tests/series/test_arithmetic.py +++ b/pandas/tests/series/test_arithmetic.py @@ -688,7 +688,7 @@ def test_datetime_understood(self): expected = pd.Series(pd.to_datetime(["2011-12-26", "2011-12-27", "2011-12-28"])) tm.assert_series_equal(result, expected) - @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16"]) + @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16", "Int8"]) @pytest.mark.parametrize( "source, target", [ @@ -703,7 +703,7 @@ def test_unary_minus_nullable_int(self, dtype, source, target): expected = pd.Series(target, dtype=dtype) tm.assert_series_equal(result, expected) - @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16"]) + @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16", "Int8"]) @pytest.mark.parametrize( "source", [[1, 2, 3], [1, 2, None], [-1, 0, 1]], ) From 319acfb9e074268f3cba840766fc04b33ab164c5 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Fri, 11 Sep 2020 23:27:22 -0500 Subject: [PATCH 03/10] Fix --- pandas/core/generic.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index bb7e6e482841b..465b7b7dbb537 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -1410,11 +1410,7 @@ def __pos__(self): values = self._values if is_bool_dtype(values) or is_numeric_dtype(values): arr = values - elif ( - is_numeric_dtype(values) - or is_timedelta64_dtype(values) - or is_object_dtype(values) - ): + elif is_timedelta64_dtype(values) or is_object_dtype(values): arr = operator.pos(values) else: raise TypeError(f"Unary plus expects numeric dtype, not {values.dtype}") From 697bff4ce96ba41959858094672544b1a9d747e7 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sat, 12 Sep 2020 12:29:46 -0500 Subject: [PATCH 04/10] pos and abs --- pandas/core/arrays/integer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py index f601cf1f7ad44..b01109e08aefb 100644 --- a/pandas/core/arrays/integer.py +++ b/pandas/core/arrays/integer.py @@ -367,6 +367,12 @@ def __init__(self, values: np.ndarray, mask: np.ndarray, copy: bool = False): def __neg__(self): return type(self)(0 - self._data, self._mask) + def __pos__(self): + return type(self)(0 + self._data, self._mask) + + def __abs__(self): + return type(self)(np.sign(self._data) * self._data, self._mask) + @classmethod def _from_sequence(cls, scalars, dtype=None, copy: bool = False) -> "IntegerArray": return integer_array(scalars, dtype=dtype, copy=copy) From 5dd332639507139a665a827c2d576b6f48853846 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sat, 12 Sep 2020 12:30:59 -0500 Subject: [PATCH 05/10] Move and add tests --- pandas/tests/series/test_arithmetic.py | 24 ---------------- pandas/tests/series/test_operators.py | 39 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py index c16e74af34cab..c937e357b9dbc 100644 --- a/pandas/tests/series/test_arithmetic.py +++ b/pandas/tests/series/test_arithmetic.py @@ -687,27 +687,3 @@ def test_datetime_understood(self): result = series - offset expected = pd.Series(pd.to_datetime(["2011-12-26", "2011-12-27", "2011-12-28"])) tm.assert_series_equal(result, expected) - - @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16", "Int8"]) - @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, dtype, source, target): - s = pd.Series(source, dtype=dtype) - result = -s - expected = pd.Series(target, dtype=dtype) - tm.assert_series_equal(result, expected) - - @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16", "Int8"]) - @pytest.mark.parametrize( - "source", [[1, 2, 3], [1, 2, None], [-1, 0, 1]], - ) - def test_unary_plus_nullable_int(self, dtype, source): - expected = pd.Series(source, dtype=dtype) - result = +expected - tm.assert_series_equal(result, expected) diff --git a/pandas/tests/series/test_operators.py b/pandas/tests/series/test_operators.py index e1c9682329271..914416cd5510a 100644 --- a/pandas/tests/series/test_operators.py +++ b/pandas/tests/series/test_operators.py @@ -536,3 +536,42 @@ def test_invert(self): ser = tm.makeStringSeries() ser.name = "series" tm.assert_series_equal(-(ser < 0), ~(ser < 0)) + + @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16", "Int8"]) + @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, dtype, source, target): + s = pd.Series(source, dtype=dtype) + result = -s + expected = pd.Series(target, dtype=dtype) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16", "Int8"]) + @pytest.mark.parametrize( + "source", [[1, 2, 3], [1, 2, None], [-1, 0, 1]], + ) + def test_unary_plus_nullable_int(self, dtype, source): + expected = pd.Series(source, dtype=dtype) + result = +expected + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16", "Int8"]) + @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, dtype, source, target): + s = pd.Series(source, dtype=dtype) + result = abs(s) + expected = pd.Series(target, dtype=dtype) + tm.assert_series_equal(result, expected) From 0b79d3c44a7168cb493d879b1d56b0c80206f603 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sat, 12 Sep 2020 12:40:01 -0500 Subject: [PATCH 06/10] Add int array tests --- .../tests/arrays/integer/test_arithmetic.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/pandas/tests/arrays/integer/test_arithmetic.py b/pandas/tests/arrays/integer/test_arithmetic.py index d309f6423e0c1..70cebe1249243 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("dtype", ["Int64", "Int32", "Int16", "Int8"]) +@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(dtype, source, target): + arr = pd.array(source, dtype=dtype) + result = -arr + expected = pd.array(target, dtype=dtype) + tm.assert_extension_array_equal(result, expected) + + +@pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16", "Int8"]) +@pytest.mark.parametrize( + "source", [[1, 2, 3], [1, 2, None], [-1, 0, 1]], +) +def test_unary_plus_nullable_int(dtype, source): + expected = pd.array(source, dtype=dtype) + result = +expected + tm.assert_extension_array_equal(result, expected) + + +@pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16", "Int8"]) +@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(dtype, source, target): + s = pd.array(source, dtype=dtype) + result = abs(s) + expected = pd.array(target, dtype=dtype) + tm.assert_extension_array_equal(result, expected) From c07ffe31cd6c68b497f887c7189afb564db972f9 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sat, 12 Sep 2020 12:45:18 -0500 Subject: [PATCH 07/10] Fix --- pandas/core/arrays/integer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py index b01109e08aefb..65fb4329c16a8 100644 --- a/pandas/core/arrays/integer.py +++ b/pandas/core/arrays/integer.py @@ -365,10 +365,10 @@ def __init__(self, values: np.ndarray, mask: np.ndarray, copy: bool = False): super().__init__(values, mask, copy=copy) def __neg__(self): - return type(self)(0 - self._data, self._mask) + return type(self)(-self._data, self._mask) def __pos__(self): - return type(self)(0 + self._data, self._mask) + return self def __abs__(self): return type(self)(np.sign(self._data) * self._data, self._mask) From 6b9cfe5aa752891983c4190cea230434cd754288 Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sat, 12 Sep 2020 13:48:39 -0500 Subject: [PATCH 08/10] Update --- pandas/core/generic.py | 14 ++++++++++---- pandas/tests/frame/test_operators.py | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 465b7b7dbb537..9a306f5ce19e0 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -1408,12 +1408,18 @@ def __neg__(self): def __pos__(self): values = self._values - if is_bool_dtype(values) or is_numeric_dtype(values): + if ( + is_bool_dtype(values) + or is_numeric_dtype(values) + or is_timedelta64_dtype(values) + or is_object_dtype(values) + ): arr = values - elif is_timedelta64_dtype(values) or is_object_dtype(values): - 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/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): From 914cd9f066333e00e9afe821def4c0d7ffe3941c Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sat, 12 Sep 2020 15:47:40 -0500 Subject: [PATCH 09/10] Fix --- pandas/core/arrays/integer.py | 2 +- pandas/core/generic.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py index 65fb4329c16a8..677deb8c32922 100644 --- a/pandas/core/arrays/integer.py +++ b/pandas/core/arrays/integer.py @@ -371,7 +371,7 @@ def __pos__(self): return self def __abs__(self): - return type(self)(np.sign(self._data) * self._data, self._mask) + return type(self)(np.abs(self._data), self._mask) @classmethod def _from_sequence(cls, scalars, dtype=None, copy: bool = False) -> "IntegerArray": diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 9a306f5ce19e0..7dfb7cb38314b 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -1408,13 +1408,14 @@ def __neg__(self): def __pos__(self): values = self._values - if ( - is_bool_dtype(values) - or is_numeric_dtype(values) + if is_bool_dtype(values): + arr = values + elif ( + is_numeric_dtype(values) or is_timedelta64_dtype(values) or is_object_dtype(values) ): - arr = values + arr = operator.pos(values) else: raise TypeError( "Unary plus expects bool, numeric, timedelta, " From e7b3043ff00ba5c11933d5f33b2ef68e9fd71b4e Mon Sep 17 00:00:00 2001 From: Daniel Saxton Date: Sat, 12 Sep 2020 15:55:18 -0500 Subject: [PATCH 10/10] New fixture --- pandas/conftest.py | 13 +++++++++++++ pandas/tests/arrays/integer/test_arithmetic.py | 12 ++++++------ pandas/tests/series/test_operators.py | 14 ++++++++------ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/pandas/conftest.py b/pandas/conftest.py index 5474005a63b8e..e79370e53ead6 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -1055,6 +1055,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/tests/arrays/integer/test_arithmetic.py b/pandas/tests/arrays/integer/test_arithmetic.py index 70cebe1249243..f549a7caeab1d 100644 --- a/pandas/tests/arrays/integer/test_arithmetic.py +++ b/pandas/tests/arrays/integer/test_arithmetic.py @@ -263,7 +263,6 @@ def test_reduce_to_float(op): tm.assert_frame_equal(result, expected) -@pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16", "Int8"]) @pytest.mark.parametrize( "source, target", [ @@ -272,29 +271,30 @@ def test_reduce_to_float(op): ([-1, 0, 1], [1, 0, -1]), ], ) -def test_unary_minus_nullable_int(dtype, source, target): +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("dtype", ["Int64", "Int32", "Int16", "Int8"]) @pytest.mark.parametrize( "source", [[1, 2, 3], [1, 2, None], [-1, 0, 1]], ) -def test_unary_plus_nullable_int(dtype, source): +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("dtype", ["Int64", "Int32", "Int16", "Int8"]) @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(dtype, source, target): +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) diff --git a/pandas/tests/series/test_operators.py b/pandas/tests/series/test_operators.py index 914416cd5510a..aee947e738525 100644 --- a/pandas/tests/series/test_operators.py +++ b/pandas/tests/series/test_operators.py @@ -537,7 +537,6 @@ def test_invert(self): ser.name = "series" tm.assert_series_equal(-(ser < 0), ~(ser < 0)) - @pytest.mark.parametrize("dtype", ["Int64", "Int32", "Int16", "Int8"]) @pytest.mark.parametrize( "source, target", [ @@ -546,22 +545,24 @@ def test_invert(self): ([-1, 0, 1], [1, 0, -1]), ], ) - def test_unary_minus_nullable_int(self, dtype, source, target): + 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("dtype", ["Int64", "Int32", "Int16", "Int8"]) @pytest.mark.parametrize( "source", [[1, 2, 3], [1, 2, None], [-1, 0, 1]], ) - def test_unary_plus_nullable_int(self, dtype, source): + 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("dtype", ["Int64", "Int32", "Int16", "Int8"]) @pytest.mark.parametrize( "source, target", [ @@ -570,7 +571,8 @@ def test_unary_plus_nullable_int(self, dtype, source): ([-1, 0, 1], [1, 0, 1]), ], ) - def test_abs_nullable_int(self, dtype, source, target): + 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)