Skip to content

Commit cd3676d

Browse files
jbrockmendelnoatamir
authored andcommitted
DEPR: Index.__and__, __or__, __xor__ (pandas-dev#49503)
* DEPR: Index.__and__, __or__, __xor__ * construct expected explicitly
1 parent 79cf81c commit cd3676d

File tree

9 files changed

+35
-116
lines changed

9 files changed

+35
-116
lines changed

doc/source/whatsnew/v2.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ Removal of prior version deprecations/changes
429429
- 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`)
430430
- 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`)
431431
- 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`)
432+
- 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`)
432433
- 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`)
433434
- Changed the behavior of :class:`Series` constructor, it will no longer infer a datetime64 or timedelta64 dtype from string entries (:issue:`41731`)
434435
- 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`)

pandas/core/indexes/base.py

+10-33
Original file line numberDiff line numberDiff line change
@@ -2932,39 +2932,6 @@ def __iadd__(self, other):
29322932
# alias for __add__
29332933
return self + other
29342934

2935-
@final
2936-
def __and__(self, other):
2937-
warnings.warn(
2938-
"Index.__and__ operating as a set operation is deprecated, "
2939-
"in the future this will be a logical operation matching "
2940-
"Series.__and__. Use index.intersection(other) instead.",
2941-
FutureWarning,
2942-
stacklevel=find_stack_level(),
2943-
)
2944-
return self.intersection(other)
2945-
2946-
@final
2947-
def __or__(self, other):
2948-
warnings.warn(
2949-
"Index.__or__ operating as a set operation is deprecated, "
2950-
"in the future this will be a logical operation matching "
2951-
"Series.__or__. Use index.union(other) instead.",
2952-
FutureWarning,
2953-
stacklevel=find_stack_level(),
2954-
)
2955-
return self.union(other)
2956-
2957-
@final
2958-
def __xor__(self, other):
2959-
warnings.warn(
2960-
"Index.__xor__ operating as a set operation is deprecated, "
2961-
"in the future this will be a logical operation matching "
2962-
"Series.__xor__. Use index.symmetric_difference(other) instead.",
2963-
FutureWarning,
2964-
stacklevel=find_stack_level(),
2965-
)
2966-
return self.symmetric_difference(other)
2967-
29682935
@final
29692936
def __nonzero__(self) -> NoReturn:
29702937
raise ValueError(
@@ -6692,6 +6659,16 @@ def _cmp_method(self, other, op):
66926659

66936660
return result
66946661

6662+
@final
6663+
def _logical_method(self, other, op):
6664+
res_name = ops.get_op_result_name(self, other)
6665+
6666+
lvalues = self._values
6667+
rvalues = extract_array(other, extract_numpy=True, extract_range=True)
6668+
6669+
res_values = ops.logical_op(lvalues, rvalues, op)
6670+
return self._construct_result(res_values, name=res_name)
6671+
66956672
@final
66966673
def _construct_result(self, result, name):
66976674
if isinstance(result, tuple):

pandas/tests/indexes/datetimes/test_setops.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -305,8 +305,7 @@ def test_intersection_bug_1708(self):
305305
index_1 = date_range("1/1/2012", periods=4, freq="12H")
306306
index_2 = index_1 + DateOffset(hours=1)
307307

308-
with tm.assert_produces_warning(FutureWarning):
309-
result = index_1 & index_2
308+
result = index_1.intersection(index_2)
310309
assert len(result) == 0
311310

312311
@pytest.mark.parametrize("tz", tz)

pandas/tests/indexes/multi/test_setops.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,11 @@ def test_symmetric_difference(idx, sort):
111111
def test_multiindex_symmetric_difference():
112112
# GH 13490
113113
idx = MultiIndex.from_product([["a", "b"], ["A", "B"]], names=["a", "b"])
114-
with tm.assert_produces_warning(FutureWarning):
115-
result = idx ^ idx
114+
result = idx.symmetric_difference(idx)
116115
assert result.names == idx.names
117116

118117
idx2 = idx.copy().rename(["A", "B"])
119-
with tm.assert_produces_warning(FutureWarning):
120-
result = idx ^ idx2
118+
result = idx.symmetric_difference(idx2)
121119
assert result.names == [None, None]
122120

123121

pandas/tests/indexes/numeric/test_setops.py

-6
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,6 @@ def test_symmetric_difference(self, sort):
131131
expected = expected.sort_values()
132132
tm.assert_index_equal(result, expected)
133133

134-
# __xor__ syntax
135-
with tm.assert_produces_warning(FutureWarning):
136-
expected = index1 ^ index2
137-
assert tm.equalContents(result, expected)
138-
assert result.name is None
139-
140134

141135
class TestSetOpsSort:
142136
@pytest.mark.parametrize("slice_", [slice(None), slice(0)])

pandas/tests/indexes/test_setops.py

-14
Original file line numberDiff line numberDiff line change
@@ -191,20 +191,6 @@ def test_union_dtypes(left, right, expected, names):
191191
assert result.name == names[2]
192192

193193

194-
def test_dunder_inplace_setops_deprecated(index):
195-
# GH#37374 these will become logical ops, not setops
196-
197-
with tm.assert_produces_warning(FutureWarning):
198-
index |= index
199-
200-
with tm.assert_produces_warning(FutureWarning):
201-
index &= index
202-
203-
is_pyarrow = str(index.dtype) == "string[pyarrow]" and pa_version_under7p0
204-
with tm.assert_produces_warning(FutureWarning, raise_on_extra_warnings=is_pyarrow):
205-
index ^= index
206-
207-
208194
@pytest.mark.parametrize("values", [[1, 2, 2, 3], [3, 3]])
209195
def test_intersection_duplicates(values):
210196
# GH#31326

pandas/tests/indexes/timedeltas/test_setops.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,13 @@ def test_intersection_bug_1708(self):
101101
index_1 = timedelta_range("1 day", periods=4, freq="h")
102102
index_2 = index_1 + pd.offsets.Hour(5)
103103

104-
with tm.assert_produces_warning(FutureWarning):
105-
result = index_1 & index_2
104+
result = index_1.intersection(index_2)
106105
assert len(result) == 0
107106

108107
index_1 = timedelta_range("1 day", periods=4, freq="h")
109108
index_2 = index_1 + pd.offsets.Hour(1)
110109

111-
with tm.assert_produces_warning(FutureWarning):
112-
result = index_1 & index_2
110+
result = index_1.intersection(index_2)
113111
expected = timedelta_range("1 day 01:00:00", periods=3, freq="h")
114112
tm.assert_index_equal(result, expected)
115113
assert result.freq == expected.freq

pandas/tests/series/test_arithmetic.py

+1-11
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,6 @@ def test_series_ops_name_retention(self, flex, box, names, all_binary_operators)
729729

730730
name = op.__name__.strip("_")
731731
is_logical = name in ["and", "rand", "xor", "rxor", "or", "ror"]
732-
is_rlogical = is_logical and name.startswith("r")
733732

734733
right = box(right)
735734
if flex:
@@ -739,16 +738,7 @@ def test_series_ops_name_retention(self, flex, box, names, all_binary_operators)
739738
result = getattr(left, name)(right)
740739
else:
741740
# GH#37374 logical ops behaving as set ops deprecated
742-
warn = FutureWarning if is_rlogical and box is Index else None
743-
msg = "operating as a set operation is deprecated"
744-
with tm.assert_produces_warning(warn, match=msg):
745-
# stacklevel is correct for Index op, not reversed op
746-
result = op(left, right)
747-
748-
if box is Index and is_rlogical:
749-
# Index treats these as set operators, so does not defer
750-
assert isinstance(result, Index)
751-
return
741+
result = op(left, right)
752742

753743
assert isinstance(result, Series)
754744
if box in [Index, Series]:

pandas/tests/series/test_logical_ops.py

+18-42
Original file line numberDiff line numberDiff line change
@@ -264,44 +264,26 @@ def test_logical_ops_with_index(self, op):
264264
result = op(ser, idx2)
265265
tm.assert_series_equal(result, expected)
266266

267-
@pytest.mark.filterwarnings("ignore:passing object-dtype arraylike:FutureWarning")
268-
def test_reversed_xor_with_index_returns_index(self):
269-
# GH#22092, GH#19792
267+
def test_reversed_xor_with_index_returns_series(self):
268+
# GH#22092, GH#19792 pre-2.0 these were aliased to setops
270269
ser = Series([True, True, False, False])
271270
idx1 = Index(
272271
[True, False, True, False], dtype=object
273272
) # TODO: raises if bool-dtype
274273
idx2 = Index([1, 0, 1, 0])
275274

276-
msg = "operating as a set operation"
277-
278-
expected = Index.symmetric_difference(idx1, ser)
279-
with tm.assert_produces_warning(FutureWarning, match=msg):
280-
result = idx1 ^ ser
281-
tm.assert_index_equal(result, expected)
275+
expected = Series([False, True, True, False])
276+
result = idx1 ^ ser
277+
tm.assert_series_equal(result, expected)
282278

283-
expected = Index.symmetric_difference(idx2, ser)
284-
with tm.assert_produces_warning(FutureWarning, match=msg):
285-
result = idx2 ^ ser
286-
tm.assert_index_equal(result, expected)
279+
result = idx2 ^ ser
280+
tm.assert_series_equal(result, expected)
287281

288282
@pytest.mark.parametrize(
289283
"op",
290284
[
291-
pytest.param(
292-
ops.rand_,
293-
marks=pytest.mark.xfail(
294-
reason="GH#22092 Index __and__ returns Index intersection",
295-
raises=AssertionError,
296-
),
297-
),
298-
pytest.param(
299-
ops.ror_,
300-
marks=pytest.mark.xfail(
301-
reason="GH#22092 Index __or__ returns Index union",
302-
raises=AssertionError,
303-
),
304-
),
285+
ops.rand_,
286+
ops.ror_,
305287
],
306288
)
307289
def test_reversed_logical_op_with_index_returns_series(self, op):
@@ -310,37 +292,31 @@ def test_reversed_logical_op_with_index_returns_series(self, op):
310292
idx1 = Index([True, False, True, False])
311293
idx2 = Index([1, 0, 1, 0])
312294

313-
msg = "operating as a set operation"
314-
315295
expected = Series(op(idx1.values, ser.values))
316-
with tm.assert_produces_warning(FutureWarning, match=msg):
317-
result = op(ser, idx1)
296+
result = op(ser, idx1)
318297
tm.assert_series_equal(result, expected)
319298

320-
expected = Series(op(idx2.values, ser.values))
321-
with tm.assert_produces_warning(FutureWarning, match=msg):
322-
result = op(ser, idx2)
299+
expected = op(ser, Series(idx2))
300+
result = op(ser, idx2)
323301
tm.assert_series_equal(result, expected)
324302

325303
@pytest.mark.parametrize(
326304
"op, expected",
327305
[
328-
(ops.rand_, Index([False, True])),
329-
(ops.ror_, Index([False, True])),
330-
(ops.rxor, Index([], dtype=bool)),
306+
(ops.rand_, Series([False, False])),
307+
(ops.ror_, Series([True, True])),
308+
(ops.rxor, Series([True, True])),
331309
],
332310
)
333311
def test_reverse_ops_with_index(self, op, expected):
334312
# https://github.com/pandas-dev/pandas/pull/23628
335313
# multi-set Index ops are buggy, so let's avoid duplicates...
314+
# GH#49503
336315
ser = Series([True, False])
337316
idx = Index([False, True])
338317

339-
msg = "operating as a set operation"
340-
with tm.assert_produces_warning(FutureWarning, match=msg):
341-
# behaving as set ops is deprecated, will become logical ops
342-
result = op(ser, idx)
343-
tm.assert_index_equal(result, expected)
318+
result = op(ser, idx)
319+
tm.assert_series_equal(result, expected)
344320

345321
def test_logical_ops_label_based(self):
346322
# GH#4947

0 commit comments

Comments
 (0)