From 718cec9c4474f39860006b6780560eb0363bbd8d Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Wed, 22 Dec 2021 16:57:27 -0800 Subject: [PATCH] ENH: Implement Index.__invert__ (#45006) --- pandas/_libs/tslibs/timedeltas.pyx | 1 - pandas/core/indexes/base.py | 7 +++--- pandas/core/indexes/multi.py | 2 +- pandas/tests/frame/test_unary.py | 2 +- pandas/tests/indexes/common.py | 24 +++++++++++++++++++ pandas/tests/indexes/multi/test_compat.py | 2 +- .../tests/scalar/timedelta/test_timedelta.py | 15 ++++++++++++ pandas/tests/series/test_unary.py | 2 +- 8 files changed, 46 insertions(+), 9 deletions(-) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index be39ccd444865..2ac635ad4fb9c 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -1401,7 +1401,6 @@ class Timedelta(_Timedelta): # Arithmetic Methods # TODO: Can some of these be defined in the cython class? - __inv__ = _op_unary_method(lambda x: -x, '__inv__') __neg__ = _op_unary_method(lambda x: -x, '__neg__') __pos__ = _op_unary_method(lambda x: x, '__pos__') __abs__ = _op_unary_method(lambda x: abs(x), '__abs__') diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 53fe84e472f3f..74044e55b5de6 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -6638,10 +6638,9 @@ def __neg__(self): def __pos__(self): return self._unary_method(operator.pos) - def __inv__(self): - # TODO: why not operator.inv? - # TODO: __inv__ vs __invert__? - return self._unary_method(lambda x: -x) + def __invert__(self): + # GH#8875 + return self._unary_method(operator.inv) # -------------------------------------------------------------------- # Reductions diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index 87ba545fabb3a..830f3afc8a1e7 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -3804,7 +3804,7 @@ def drop_duplicates(self, keep: str | bool = "first") -> MultiIndex: __neg__ = make_invalid_op("__neg__") __pos__ = make_invalid_op("__pos__") __abs__ = make_invalid_op("__abs__") - __inv__ = make_invalid_op("__inv__") + __invert__ = make_invalid_op("__invert__") def _lexsort_depth(codes: list[np.ndarray], nlevels: int) -> int: diff --git a/pandas/tests/frame/test_unary.py b/pandas/tests/frame/test_unary.py index 2129586455333..a69ca0fef7f8b 100644 --- a/pandas/tests/frame/test_unary.py +++ b/pandas/tests/frame/test_unary.py @@ -8,7 +8,7 @@ class TestDataFrameUnaryOperators: - # __pos__, __neg__, __inv__ + # __pos__, __neg__, __invert__ @pytest.mark.parametrize( "df,expected", diff --git a/pandas/tests/indexes/common.py b/pandas/tests/indexes/common.py index c60c74479f8b6..6b6caf1f8affd 100644 --- a/pandas/tests/indexes/common.py +++ b/pandas/tests/indexes/common.py @@ -740,6 +740,30 @@ def test_append_preserves_dtype(self, simple_index): alt = index.take(list(range(N)) * 2) tm.assert_index_equal(result, alt, check_exact=True) + def test_inv(self, simple_index): + idx = simple_index + + if idx.dtype.kind in ["i", "u"]: + res = ~idx + expected = Index(~idx.values, name=idx.name) + tm.assert_index_equal(res, expected) + + # check that we are matching Series behavior + res2 = ~Series(idx) + # TODO(2.0): once we preserve dtype, check_dtype can be True + tm.assert_series_equal(res2, Series(expected), check_dtype=False) + else: + if idx.dtype.kind == "f": + msg = "ufunc 'invert' not supported for the input types" + else: + msg = "bad operand" + with pytest.raises(TypeError, match=msg): + ~idx + + # check that we get the same behavior with Series + with pytest.raises(TypeError, match=msg): + ~Series(idx) + class NumericBase(Base): """ diff --git a/pandas/tests/indexes/multi/test_compat.py b/pandas/tests/indexes/multi/test_compat.py index cbb4ae0b0d09b..d50a44057bd26 100644 --- a/pandas/tests/indexes/multi/test_compat.py +++ b/pandas/tests/indexes/multi/test_compat.py @@ -27,7 +27,7 @@ def test_numeric_compat(idx): 1 // idx -@pytest.mark.parametrize("method", ["all", "any"]) +@pytest.mark.parametrize("method", ["all", "any", "__invert__"]) def test_logical_compat(idx, method): msg = f"cannot perform {method}" diff --git a/pandas/tests/scalar/timedelta/test_timedelta.py b/pandas/tests/scalar/timedelta/test_timedelta.py index 448ec4353d7e7..7a32c932aee77 100644 --- a/pandas/tests/scalar/timedelta/test_timedelta.py +++ b/pandas/tests/scalar/timedelta/test_timedelta.py @@ -25,6 +25,21 @@ class TestTimedeltaUnaryOps: + def test_invert(self): + td = Timedelta(10, unit="d") + + msg = "bad operand type for unary ~" + with pytest.raises(TypeError, match=msg): + ~td + + # check this matches pytimedelta and timedelta64 + with pytest.raises(TypeError, match=msg): + ~(td.to_pytimedelta()) + + umsg = "ufunc 'invert' not supported for the input types" + with pytest.raises(TypeError, match=umsg): + ~(td.to_timedelta64()) + def test_unary_ops(self): td = Timedelta(10, unit="d") diff --git a/pandas/tests/series/test_unary.py b/pandas/tests/series/test_unary.py index 54fd467431489..ad0e344fa4420 100644 --- a/pandas/tests/series/test_unary.py +++ b/pandas/tests/series/test_unary.py @@ -5,7 +5,7 @@ class TestSeriesUnaryOps: - # __neg__, __pos__, __inv__ + # __neg__, __pos__, __invert__ def test_neg(self): ser = tm.makeStringSeries()