Skip to content

Commit

Permalink
DEPR: Index.__and__, __or__, __xor__ (pandas-dev#49503)
Browse files Browse the repository at this point in the history
* DEPR: Index.__and__, __or__, __xor__

* construct expected explicitly
  • Loading branch information
jbrockmendel authored and noatamir committed Nov 9, 2022
1 parent 79cf81c commit cd3676d
Show file tree
Hide file tree
Showing 9 changed files with 35 additions and 116 deletions.
1 change: 1 addition & 0 deletions doc/source/whatsnew/v2.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,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`)
Expand Down
43 changes: 10 additions & 33 deletions pandas/core/indexes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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):
Expand Down
3 changes: 1 addition & 2 deletions pandas/tests/indexes/datetimes/test_setops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 2 additions & 4 deletions pandas/tests/indexes/multi/test_setops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]


Expand Down
6 changes: 0 additions & 6 deletions pandas/tests/indexes/numeric/test_setops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)])
Expand Down
14 changes: 0 additions & 14 deletions pandas/tests/indexes/test_setops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions pandas/tests/indexes/timedeltas/test_setops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 1 addition & 11 deletions pandas/tests/series/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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]:
Expand Down
60 changes: 18 additions & 42 deletions pandas/tests/series/test_logical_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,44 +264,26 @@ 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 = Series([False, True, True, False])
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)
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):
Expand All @@ -310,37 +292,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#49503
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
Expand Down

0 comments on commit cd3676d

Please sign in to comment.