Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEPR: Index.__and__, __or__, __xor__ #49503

Merged
merged 2 commits into from
Nov 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -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`)
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