From 563d5addb4ad10885f826bb3c0a77e06d179589e Mon Sep 17 00:00:00 2001 From: Brock Date: Thu, 3 Nov 2022 10:13:25 -0700 Subject: [PATCH 1/2] DEPR: Index.__and__, __or__, __xor__ --- doc/source/whatsnew/v2.0.0.rst | 1 + pandas/core/indexes/base.py | 43 +++---------- pandas/tests/indexes/datetimes/test_setops.py | 3 +- pandas/tests/indexes/multi/test_setops.py | 6 +- pandas/tests/indexes/numeric/test_setops.py | 6 -- pandas/tests/indexes/test_setops.py | 14 ----- .../tests/indexes/timedeltas/test_setops.py | 6 +- pandas/tests/series/test_arithmetic.py | 12 +--- pandas/tests/series/test_logical_ops.py | 61 ++++++------------- 9 files changed, 36 insertions(+), 116 deletions(-) diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst index df190a4df393c..3ca3fbbe58f00 100644 --- a/doc/source/whatsnew/v2.0.0.rst +++ b/doc/source/whatsnew/v2.0.0.rst @@ -415,6 +415,7 @@ Removal of prior version deprecations/changes - Changed behavior of :meth:`Index.to_frame` with explicit ``name=None`` to use ``None`` for the column name instead of the index's name or default ``0`` (:issue:`45523`) - Changed behavior of :class:`DataFrame` constructor given floating-point ``data`` and an integer ``dtype``, when the data cannot be cast losslessly, the floating point dtype is retained, matching :class:`Series` behavior (:issue:`41170`) - Changed behavior of :class:`Index` constructor when given a ``np.ndarray`` with object-dtype containing numeric entries; this now retains object dtype rather than inferring a numeric dtype, consistent with :class:`Series` behavior (:issue:`42870`) +- Changed behavior of :meth:`Index.__and__`, :meth:`Index.__or__` and :meth:`Index.__xor__` to behave as logical operations (matching :class:`Series` behavior) instead of aliases for set operations (:issue:`37374`) - Changed behavior of :class:`DataFrame` constructor when passed a ``dtype`` (other than int) that the data cannot be cast to; it now raises instead of silently ignoring the dtype (:issue:`41733`) - Changed the behavior of :class:`Series` constructor, it will no longer infer a datetime64 or timedelta64 dtype from string entries (:issue:`41731`) - Changed behavior of :class:`Timestamp` constructor with a ``np.datetime64`` object and a ``tz`` passed to interpret the input as a wall-time as opposed to a UTC time (:issue:`42288`) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 10c2349f05dfd..92a8a7a7adcae 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -2932,39 +2932,6 @@ def __iadd__(self, other): # alias for __add__ return self + other - @final - def __and__(self, other): - warnings.warn( - "Index.__and__ operating as a set operation is deprecated, " - "in the future this will be a logical operation matching " - "Series.__and__. Use index.intersection(other) instead.", - FutureWarning, - stacklevel=find_stack_level(), - ) - return self.intersection(other) - - @final - def __or__(self, other): - warnings.warn( - "Index.__or__ operating as a set operation is deprecated, " - "in the future this will be a logical operation matching " - "Series.__or__. Use index.union(other) instead.", - FutureWarning, - stacklevel=find_stack_level(), - ) - return self.union(other) - - @final - def __xor__(self, other): - warnings.warn( - "Index.__xor__ operating as a set operation is deprecated, " - "in the future this will be a logical operation matching " - "Series.__xor__. Use index.symmetric_difference(other) instead.", - FutureWarning, - stacklevel=find_stack_level(), - ) - return self.symmetric_difference(other) - @final def __nonzero__(self) -> NoReturn: raise ValueError( @@ -6692,6 +6659,16 @@ def _cmp_method(self, other, op): return result + @final + def _logical_method(self, other, op): + res_name = ops.get_op_result_name(self, other) + + lvalues = self._values + rvalues = extract_array(other, extract_numpy=True, extract_range=True) + + res_values = ops.logical_op(lvalues, rvalues, op) + return self._construct_result(res_values, name=res_name) + @final def _construct_result(self, result, name): if isinstance(result, tuple): diff --git a/pandas/tests/indexes/datetimes/test_setops.py b/pandas/tests/indexes/datetimes/test_setops.py index a4ae7c5fd6fa3..07e1fd27b2f96 100644 --- a/pandas/tests/indexes/datetimes/test_setops.py +++ b/pandas/tests/indexes/datetimes/test_setops.py @@ -305,8 +305,7 @@ def test_intersection_bug_1708(self): index_1 = date_range("1/1/2012", periods=4, freq="12H") index_2 = index_1 + DateOffset(hours=1) - with tm.assert_produces_warning(FutureWarning): - result = index_1 & index_2 + result = index_1.intersection(index_2) assert len(result) == 0 @pytest.mark.parametrize("tz", tz) diff --git a/pandas/tests/indexes/multi/test_setops.py b/pandas/tests/indexes/multi/test_setops.py index eaa4e0a7b5256..e3fdd62b908c4 100644 --- a/pandas/tests/indexes/multi/test_setops.py +++ b/pandas/tests/indexes/multi/test_setops.py @@ -111,13 +111,11 @@ def test_symmetric_difference(idx, sort): def test_multiindex_symmetric_difference(): # GH 13490 idx = MultiIndex.from_product([["a", "b"], ["A", "B"]], names=["a", "b"]) - with tm.assert_produces_warning(FutureWarning): - result = idx ^ idx + result = idx.symmetric_difference(idx) assert result.names == idx.names idx2 = idx.copy().rename(["A", "B"]) - with tm.assert_produces_warning(FutureWarning): - result = idx ^ idx2 + result = idx.symmetric_difference(idx2) assert result.names == [None, None] diff --git a/pandas/tests/indexes/numeric/test_setops.py b/pandas/tests/indexes/numeric/test_setops.py index 9f2174c2de51e..c8f348fc1bb27 100644 --- a/pandas/tests/indexes/numeric/test_setops.py +++ b/pandas/tests/indexes/numeric/test_setops.py @@ -131,12 +131,6 @@ def test_symmetric_difference(self, sort): expected = expected.sort_values() tm.assert_index_equal(result, expected) - # __xor__ syntax - with tm.assert_produces_warning(FutureWarning): - expected = index1 ^ index2 - assert tm.equalContents(result, expected) - assert result.name is None - class TestSetOpsSort: @pytest.mark.parametrize("slice_", [slice(None), slice(0)]) diff --git a/pandas/tests/indexes/test_setops.py b/pandas/tests/indexes/test_setops.py index 1939a30ad66ce..14cde141d1c60 100644 --- a/pandas/tests/indexes/test_setops.py +++ b/pandas/tests/indexes/test_setops.py @@ -191,20 +191,6 @@ def test_union_dtypes(left, right, expected, names): assert result.name == names[2] -def test_dunder_inplace_setops_deprecated(index): - # GH#37374 these will become logical ops, not setops - - with tm.assert_produces_warning(FutureWarning): - index |= index - - with tm.assert_produces_warning(FutureWarning): - index &= index - - is_pyarrow = str(index.dtype) == "string[pyarrow]" and pa_version_under7p0 - with tm.assert_produces_warning(FutureWarning, raise_on_extra_warnings=is_pyarrow): - index ^= index - - @pytest.mark.parametrize("values", [[1, 2, 2, 3], [3, 3]]) def test_intersection_duplicates(values): # GH#31326 diff --git a/pandas/tests/indexes/timedeltas/test_setops.py b/pandas/tests/indexes/timedeltas/test_setops.py index 4574c15343391..976d4a61f27e3 100644 --- a/pandas/tests/indexes/timedeltas/test_setops.py +++ b/pandas/tests/indexes/timedeltas/test_setops.py @@ -101,15 +101,13 @@ def test_intersection_bug_1708(self): index_1 = timedelta_range("1 day", periods=4, freq="h") index_2 = index_1 + pd.offsets.Hour(5) - with tm.assert_produces_warning(FutureWarning): - result = index_1 & index_2 + result = index_1.intersection(index_2) assert len(result) == 0 index_1 = timedelta_range("1 day", periods=4, freq="h") index_2 = index_1 + pd.offsets.Hour(1) - with tm.assert_produces_warning(FutureWarning): - result = index_1 & index_2 + result = index_1.intersection(index_2) expected = timedelta_range("1 day 01:00:00", periods=3, freq="h") tm.assert_index_equal(result, expected) assert result.freq == expected.freq diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py index 37711054f2285..8f9164fce4977 100644 --- a/pandas/tests/series/test_arithmetic.py +++ b/pandas/tests/series/test_arithmetic.py @@ -729,7 +729,6 @@ def test_series_ops_name_retention(self, flex, box, names, all_binary_operators) name = op.__name__.strip("_") is_logical = name in ["and", "rand", "xor", "rxor", "or", "ror"] - is_rlogical = is_logical and name.startswith("r") right = box(right) if flex: @@ -739,16 +738,7 @@ def test_series_ops_name_retention(self, flex, box, names, all_binary_operators) result = getattr(left, name)(right) else: # GH#37374 logical ops behaving as set ops deprecated - warn = FutureWarning if is_rlogical and box is Index else None - msg = "operating as a set operation is deprecated" - with tm.assert_produces_warning(warn, match=msg): - # stacklevel is correct for Index op, not reversed op - result = op(left, right) - - if box is Index and is_rlogical: - # Index treats these as set operators, so does not defer - assert isinstance(result, Index) - return + result = op(left, right) assert isinstance(result, Series) if box in [Index, Series]: diff --git a/pandas/tests/series/test_logical_ops.py b/pandas/tests/series/test_logical_ops.py index 38e3c5ec8a6f2..c622c3c10e602 100644 --- a/pandas/tests/series/test_logical_ops.py +++ b/pandas/tests/series/test_logical_ops.py @@ -264,44 +264,27 @@ def test_logical_ops_with_index(self, op): result = op(ser, idx2) tm.assert_series_equal(result, expected) - @pytest.mark.filterwarnings("ignore:passing object-dtype arraylike:FutureWarning") - def test_reversed_xor_with_index_returns_index(self): - # GH#22092, GH#19792 + def test_reversed_xor_with_index_returns_series(self): + # GH#22092, GH#19792 pre-2.0 these were aliased to setops ser = Series([True, True, False, False]) idx1 = Index( [True, False, True, False], dtype=object ) # TODO: raises if bool-dtype idx2 = Index([1, 0, 1, 0]) - msg = "operating as a set operation" - - expected = Index.symmetric_difference(idx1, ser) - with tm.assert_produces_warning(FutureWarning, match=msg): - result = idx1 ^ ser - tm.assert_index_equal(result, expected) + expected = ser ^ idx1.values + result = idx1 ^ ser + tm.assert_series_equal(result, expected) - expected = Index.symmetric_difference(idx2, ser) - with tm.assert_produces_warning(FutureWarning, match=msg): - result = idx2 ^ ser - tm.assert_index_equal(result, expected) + expected = ser ^ idx2.values + result = idx2 ^ ser + tm.assert_series_equal(result, expected) @pytest.mark.parametrize( "op", [ - pytest.param( - ops.rand_, - marks=pytest.mark.xfail( - reason="GH#22092 Index __and__ returns Index intersection", - raises=AssertionError, - ), - ), - pytest.param( - ops.ror_, - marks=pytest.mark.xfail( - reason="GH#22092 Index __or__ returns Index union", - raises=AssertionError, - ), - ), + ops.rand_, + ops.ror_, ], ) def test_reversed_logical_op_with_index_returns_series(self, op): @@ -310,37 +293,31 @@ def test_reversed_logical_op_with_index_returns_series(self, op): idx1 = Index([True, False, True, False]) idx2 = Index([1, 0, 1, 0]) - msg = "operating as a set operation" - expected = Series(op(idx1.values, ser.values)) - with tm.assert_produces_warning(FutureWarning, match=msg): - result = op(ser, idx1) + result = op(ser, idx1) tm.assert_series_equal(result, expected) - expected = Series(op(idx2.values, ser.values)) - with tm.assert_produces_warning(FutureWarning, match=msg): - result = op(ser, idx2) + expected = op(ser, Series(idx2)) + result = op(ser, idx2) tm.assert_series_equal(result, expected) @pytest.mark.parametrize( "op, expected", [ - (ops.rand_, Index([False, True])), - (ops.ror_, Index([False, True])), - (ops.rxor, Index([], dtype=bool)), + (ops.rand_, Series([False, False])), + (ops.ror_, Series([True, True])), + (ops.rxor, Series([True, True])), ], ) def test_reverse_ops_with_index(self, op, expected): # https://github.com/pandas-dev/pandas/pull/23628 # multi-set Index ops are buggy, so let's avoid duplicates... + # GH#... ser = Series([True, False]) idx = Index([False, True]) - msg = "operating as a set operation" - with tm.assert_produces_warning(FutureWarning, match=msg): - # behaving as set ops is deprecated, will become logical ops - result = op(ser, idx) - tm.assert_index_equal(result, expected) + result = op(ser, idx) + tm.assert_series_equal(result, expected) def test_logical_ops_label_based(self): # GH#4947 From a3ba3559493aa4440fd85167c7129ba0aa43073d Mon Sep 17 00:00:00 2001 From: Brock Date: Fri, 4 Nov 2022 13:14:11 -0700 Subject: [PATCH 2/2] construct expected explicitly --- pandas/tests/series/test_logical_ops.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pandas/tests/series/test_logical_ops.py b/pandas/tests/series/test_logical_ops.py index c622c3c10e602..0d661f19087e6 100644 --- a/pandas/tests/series/test_logical_ops.py +++ b/pandas/tests/series/test_logical_ops.py @@ -272,11 +272,10 @@ def test_reversed_xor_with_index_returns_series(self): ) # TODO: raises if bool-dtype idx2 = Index([1, 0, 1, 0]) - expected = ser ^ idx1.values + expected = Series([False, True, True, False]) result = idx1 ^ ser tm.assert_series_equal(result, expected) - expected = ser ^ idx2.values result = idx2 ^ ser tm.assert_series_equal(result, expected) @@ -312,7 +311,7 @@ def test_reversed_logical_op_with_index_returns_series(self, op): def test_reverse_ops_with_index(self, op, expected): # https://github.com/pandas-dev/pandas/pull/23628 # multi-set Index ops are buggy, so let's avoid duplicates... - # GH#... + # GH#49503 ser = Series([True, False]) idx = Index([False, True])