Skip to content

Commit af208b3

Browse files
Backport PR #31136: COMPAT: Return NotImplemented for subclassing (#31405)
Co-authored-by: Tom Augspurger <[email protected]>
1 parent c836828 commit af208b3

File tree

2 files changed

+60
-1
lines changed

2 files changed

+60
-1
lines changed

pandas/core/indexes/extension.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
from pandas.compat.numpy import function as nv
99
from pandas.util._decorators import Appender, cache_readonly
1010

11-
from pandas.core.dtypes.common import ensure_platform_int, is_dtype_equal
11+
from pandas.core.dtypes.common import (
12+
ensure_platform_int,
13+
is_dtype_equal,
14+
is_object_dtype,
15+
)
1216
from pandas.core.dtypes.generic import ABCSeries
1317

1418
from pandas.core.arrays import ExtensionArray
@@ -129,6 +133,15 @@ def wrapper(self, other):
129133

130134
def make_wrapped_arith_op(opname):
131135
def method(self, other):
136+
if (
137+
isinstance(other, Index)
138+
and is_object_dtype(other.dtype)
139+
and type(other) is not Index
140+
):
141+
# We return NotImplemented for object-dtype index *subclasses* so they have
142+
# a chance to implement ops before we unwrap them.
143+
# See https://github.com/pandas-dev/pandas/issues/31109
144+
return NotImplemented
132145
meth = getattr(self._data, opname)
133146
result = meth(_maybe_unwrap_index(other))
134147
return _wrap_arithmetic_op(self, other, result)

pandas/tests/arithmetic/test_object.py

+46
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Arithmetic tests for DataFrame/Series/Index/Array classes that should
22
# behave identically.
33
# Specifically for object dtype
4+
import datetime
45
from decimal import Decimal
56
import operator
67

@@ -317,3 +318,48 @@ def test_rsub_object(self):
317318

318319
with pytest.raises(TypeError):
319320
np.array([True, pd.Timestamp.now()]) - index
321+
322+
323+
class MyIndex(pd.Index):
324+
# Simple index subclass that tracks ops calls.
325+
326+
_calls: int
327+
328+
@classmethod
329+
def _simple_new(cls, values, name=None, dtype=None):
330+
result = object.__new__(cls)
331+
result._data = values
332+
result._index_data = values
333+
result._name = name
334+
result._calls = 0
335+
336+
return result._reset_identity()
337+
338+
def __add__(self, other):
339+
self._calls += 1
340+
return self._simple_new(self._index_data)
341+
342+
def __radd__(self, other):
343+
return self.__add__(other)
344+
345+
346+
@pytest.mark.parametrize(
347+
"other",
348+
[
349+
[datetime.timedelta(1), datetime.timedelta(2)],
350+
[datetime.datetime(2000, 1, 1), datetime.datetime(2000, 1, 2)],
351+
[pd.Period("2000"), pd.Period("2001")],
352+
["a", "b"],
353+
],
354+
ids=["timedelta", "datetime", "period", "object"],
355+
)
356+
def test_index_ops_defer_to_unknown_subclasses(other):
357+
# https://github.com/pandas-dev/pandas/issues/31109
358+
values = np.array(
359+
[datetime.date(2000, 1, 1), datetime.date(2000, 1, 2)], dtype=object
360+
)
361+
a = MyIndex._simple_new(values)
362+
other = pd.Index(other)
363+
result = other + a
364+
assert isinstance(result, MyIndex)
365+
assert a._calls == 1

0 commit comments

Comments
 (0)