Skip to content

Commit 0ffc4b5

Browse files
jbrockmendeljreback
authored andcommitted
BUG: fix index op names and pinning (#19723)
1 parent c3e35a0 commit 0ffc4b5

File tree

8 files changed

+204
-207
lines changed

8 files changed

+204
-207
lines changed

pandas/core/indexes/base.py

Lines changed: 128 additions & 139 deletions
Large diffs are not rendered by default.

pandas/core/indexes/datetimelike.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,7 @@ def __add__(self, other):
669669
result = self._add_offset_array(other)
670670
elif isinstance(self, TimedeltaIndex) and isinstance(other, Index):
671671
if hasattr(other, '_add_delta'):
672+
# i.e. DatetimeIndex, TimedeltaIndex, or PeriodIndex
672673
result = other._add_delta(self)
673674
else:
674675
raise TypeError("cannot add TimedeltaIndex and {typ}"
@@ -693,7 +694,11 @@ def __add__(self, other):
693694
return result
694695

695696
cls.__add__ = __add__
696-
cls.__radd__ = __add__
697+
698+
def __radd__(self, other):
699+
# alias for __add__
700+
return self.__add__(other)
701+
cls.__radd__ = __radd__
697702

698703
def __sub__(self, other):
699704
from pandas.core.index import Index
@@ -712,10 +717,10 @@ def __sub__(self, other):
712717
# Array/Index of DateOffset objects
713718
result = self._sub_offset_array(other)
714719
elif isinstance(self, TimedeltaIndex) and isinstance(other, Index):
715-
if not isinstance(other, TimedeltaIndex):
716-
raise TypeError("cannot subtract TimedeltaIndex and {typ}"
717-
.format(typ=type(other).__name__))
718-
result = self._add_delta(-other)
720+
# We checked above for timedelta64_dtype(other) so this
721+
# must be invalid.
722+
raise TypeError("cannot subtract TimedeltaIndex and {typ}"
723+
.format(typ=type(other).__name__))
719724
elif isinstance(other, DatetimeIndex):
720725
result = self._sub_datelike(other)
721726
elif is_integer(other):
@@ -747,8 +752,15 @@ def __rsub__(self, other):
747752
return -(self - other)
748753
cls.__rsub__ = __rsub__
749754

750-
cls.__iadd__ = __add__
751-
cls.__isub__ = __sub__
755+
def __iadd__(self, other):
756+
# alias for __add__
757+
return self.__add__(other)
758+
cls.__iadd__ = __iadd__
759+
760+
def __isub__(self, other):
761+
# alias for __sub__
762+
return self.__sub__(other)
763+
cls.__isub__ = __isub__
752764

753765
def _add_delta(self, other):
754766
return NotImplemented

pandas/core/indexes/datetimes.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,11 @@ def f(self):
100100
return property(f)
101101

102102

103-
def _dt_index_cmp(opname, cls, nat_result=False):
103+
def _dt_index_cmp(opname, cls):
104104
"""
105105
Wrap comparison operations to convert datetime-like to datetime64
106106
"""
107+
nat_result = True if opname == '__ne__' else False
107108

108109
def wrapper(self, other):
109110
func = getattr(super(DatetimeIndex, self), opname)
@@ -291,7 +292,7 @@ def _join_i8_wrapper(joinf, **kwargs):
291292
def _add_comparison_methods(cls):
292293
""" add in comparison methods """
293294
cls.__eq__ = _dt_index_cmp('__eq__', cls)
294-
cls.__ne__ = _dt_index_cmp('__ne__', cls, nat_result=True)
295+
cls.__ne__ = _dt_index_cmp('__ne__', cls)
295296
cls.__lt__ = _dt_index_cmp('__lt__', cls)
296297
cls.__gt__ = _dt_index_cmp('__gt__', cls)
297298
cls.__le__ = _dt_index_cmp('__le__', cls)

pandas/core/indexes/period.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,26 +76,25 @@ def dt64arr_to_periodarr(data, freq, tz):
7676
_DIFFERENT_FREQ_INDEX = period._DIFFERENT_FREQ_INDEX
7777

7878

79-
def _period_index_cmp(opname, cls, nat_result=False):
79+
def _period_index_cmp(opname, cls):
8080
"""
81-
Wrap comparison operations to convert datetime-like to datetime64
81+
Wrap comparison operations to convert Period-like to PeriodDtype
8282
"""
83+
nat_result = True if opname == '__ne__' else False
8384

8485
def wrapper(self, other):
86+
op = getattr(self._ndarray_values, opname)
8587
if isinstance(other, Period):
86-
func = getattr(self._ndarray_values, opname)
87-
other_base, _ = _gfc(other.freq)
8888
if other.freq != self.freq:
8989
msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr)
9090
raise IncompatibleFrequency(msg)
9191

92-
result = func(other.ordinal)
92+
result = op(other.ordinal)
9393
elif isinstance(other, PeriodIndex):
9494
if other.freq != self.freq:
9595
msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr)
9696
raise IncompatibleFrequency(msg)
9797

98-
op = getattr(self._ndarray_values, opname)
9998
result = op(other._ndarray_values)
10099

101100
mask = self._isnan | other._isnan
@@ -108,8 +107,7 @@ def wrapper(self, other):
108107
result.fill(nat_result)
109108
else:
110109
other = Period(other, freq=self.freq)
111-
func = getattr(self._ndarray_values, opname)
112-
result = func(other.ordinal)
110+
result = op(other.ordinal)
113111

114112
if self.hasnans:
115113
result[self._isnan] = nat_result
@@ -231,7 +229,7 @@ class PeriodIndex(DatelikeOps, DatetimeIndexOpsMixin, Int64Index):
231229
def _add_comparison_methods(cls):
232230
""" add in comparison methods """
233231
cls.__eq__ = _period_index_cmp('__eq__', cls)
234-
cls.__ne__ = _period_index_cmp('__ne__', cls, nat_result=True)
232+
cls.__ne__ = _period_index_cmp('__ne__', cls)
235233
cls.__lt__ = _period_index_cmp('__lt__', cls)
236234
cls.__gt__ = _period_index_cmp('__gt__', cls)
237235
cls.__le__ = _period_index_cmp('__le__', cls)

pandas/core/indexes/range.py

Lines changed: 17 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from pandas.compat.numpy import function as nv
1717

1818
import pandas.core.common as com
19+
from pandas.core import ops
1920
from pandas.core.indexes.base import Index, _index_shared_docs
2021
from pandas.util._decorators import Appender, cache_readonly
2122
import pandas.core.dtypes.concat as _concat
@@ -570,16 +571,12 @@ def __floordiv__(self, other):
570571
def _add_numeric_methods_binary(cls):
571572
""" add in numeric methods, specialized to RangeIndex """
572573

573-
def _make_evaluate_binop(op, opstr, reversed=False, step=False):
574+
def _make_evaluate_binop(op, step=False):
574575
"""
575576
Parameters
576577
----------
577578
op : callable that accepts 2 parms
578579
perform the binary op
579-
opstr : string
580-
string name of ops
581-
reversed : boolean, default False
582-
if this is a reversed op, e.g. radd
583580
step : callable, optional, default to False
584581
op to apply to the step parm if not None
585582
if False, use the existing step
@@ -594,17 +591,13 @@ def _evaluate_numeric_binop(self, other):
594591
elif isinstance(other, (timedelta, np.timedelta64)):
595592
# GH#19333 is_integer evaluated True on timedelta64,
596593
# so we need to catch these explicitly
597-
if reversed:
598-
return op(other, self._int64index)
599594
return op(self._int64index, other)
600595

601-
other = self._validate_for_numeric_binop(other, op, opstr)
596+
other = self._validate_for_numeric_binop(other, op)
602597
attrs = self._get_attributes_dict()
603598
attrs = self._maybe_update_attributes(attrs)
604599

605600
left, right = self, other
606-
if reversed:
607-
left, right = right, left
608601

609602
try:
610603
# apply if we have an override
@@ -638,43 +631,26 @@ def _evaluate_numeric_binop(self, other):
638631

639632
return result
640633

641-
except (ValueError, TypeError, AttributeError,
642-
ZeroDivisionError):
634+
except (ValueError, TypeError, ZeroDivisionError):
643635
# Defer to Int64Index implementation
644-
if reversed:
645-
return op(other, self._int64index)
646636
return op(self._int64index, other)
637+
# TODO: Do attrs get handled reliably?
647638

648639
return _evaluate_numeric_binop
649640

650-
cls.__add__ = cls.__radd__ = _make_evaluate_binop(
651-
operator.add, '__add__')
652-
cls.__sub__ = _make_evaluate_binop(operator.sub, '__sub__')
653-
cls.__rsub__ = _make_evaluate_binop(
654-
operator.sub, '__sub__', reversed=True)
655-
cls.__mul__ = cls.__rmul__ = _make_evaluate_binop(
656-
operator.mul,
657-
'__mul__',
658-
step=operator.mul)
659-
cls.__truediv__ = _make_evaluate_binop(
660-
operator.truediv,
661-
'__truediv__',
662-
step=operator.truediv)
663-
cls.__rtruediv__ = _make_evaluate_binop(
664-
operator.truediv,
665-
'__truediv__',
666-
reversed=True,
667-
step=operator.truediv)
641+
cls.__add__ = _make_evaluate_binop(operator.add)
642+
cls.__radd__ = _make_evaluate_binop(ops.radd)
643+
cls.__sub__ = _make_evaluate_binop(operator.sub)
644+
cls.__rsub__ = _make_evaluate_binop(ops.rsub)
645+
cls.__mul__ = _make_evaluate_binop(operator.mul, step=operator.mul)
646+
cls.__rmul__ = _make_evaluate_binop(ops.rmul, step=ops.rmul)
647+
cls.__truediv__ = _make_evaluate_binop(operator.truediv,
648+
step=operator.truediv)
649+
cls.__rtruediv__ = _make_evaluate_binop(ops.rtruediv,
650+
step=ops.rtruediv)
668651
if not compat.PY3:
669-
cls.__div__ = _make_evaluate_binop(
670-
operator.div,
671-
'__div__',
672-
step=operator.div)
673-
cls.__rdiv__ = _make_evaluate_binop(
674-
operator.div,
675-
'__div__',
676-
reversed=True,
677-
step=operator.div)
652+
cls.__div__ = _make_evaluate_binop(operator.div, step=operator.div)
653+
cls.__rdiv__ = _make_evaluate_binop(ops.rdiv, step=ops.rdiv)
678654

679655

680656
RangeIndex._add_numeric_methods()

pandas/core/indexes/timedeltas.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,11 @@ def f(self):
5353
return property(f)
5454

5555

56-
def _td_index_cmp(opname, cls, nat_result=False):
56+
def _td_index_cmp(opname, cls):
5757
"""
5858
Wrap comparison operations to convert timedelta-like to timedelta64
5959
"""
60+
nat_result = True if opname == '__ne__' else False
6061

6162
def wrapper(self, other):
6263
msg = "cannot compare a TimedeltaIndex with type {0}"
@@ -184,7 +185,7 @@ def _join_i8_wrapper(joinf, **kwargs):
184185
def _add_comparison_methods(cls):
185186
""" add in comparison methods """
186187
cls.__eq__ = _td_index_cmp('__eq__', cls)
187-
cls.__ne__ = _td_index_cmp('__ne__', cls, nat_result=True)
188+
cls.__ne__ = _td_index_cmp('__ne__', cls)
188189
cls.__lt__ = _td_index_cmp('__lt__', cls)
189190
cls.__gt__ = _td_index_cmp('__gt__', cls)
190191
cls.__le__ = _td_index_cmp('__le__', cls)
@@ -383,11 +384,12 @@ def _add_delta(self, delta):
383384

384385
return TimedeltaIndex(new_values, freq='infer')
385386

386-
def _evaluate_with_timedelta_like(self, other, op, opstr, reversed=False):
387+
def _evaluate_with_timedelta_like(self, other, op):
387388
if isinstance(other, ABCSeries):
388389
# GH#19042
389390
return NotImplemented
390391

392+
opstr = '__{opname}__'.format(opname=op.__name__).replace('__r', '__')
391393
# allow division by a timedelta
392394
if opstr in ['__div__', '__truediv__', '__floordiv__']:
393395
if _is_convertible_to_td(other):
@@ -398,11 +400,9 @@ def _evaluate_with_timedelta_like(self, other, op, opstr, reversed=False):
398400

399401
i8 = self.asi8
400402
left, right = i8, other.value
401-
if reversed:
402-
left, right = right, left
403403

404404
if opstr in ['__floordiv__']:
405-
result = left // right
405+
result = op(left, right)
406406
else:
407407
result = op(left, np.float64(right))
408408
result = self._maybe_mask_results(result, convert='float64')

pandas/tests/indexes/common.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,16 +127,17 @@ def test_numeric_compat(self):
127127
idx = self.create_index()
128128
tm.assert_raises_regex(TypeError, "cannot perform __mul__",
129129
lambda: idx * 1)
130-
tm.assert_raises_regex(TypeError, "cannot perform __mul__",
130+
tm.assert_raises_regex(TypeError, "cannot perform __rmul__",
131131
lambda: 1 * idx)
132132

133133
div_err = "cannot perform __truediv__" if PY3 \
134134
else "cannot perform __div__"
135135
tm.assert_raises_regex(TypeError, div_err, lambda: idx / 1)
136+
div_err = div_err.replace(' __', ' __r')
136137
tm.assert_raises_regex(TypeError, div_err, lambda: 1 / idx)
137138
tm.assert_raises_regex(TypeError, "cannot perform __floordiv__",
138139
lambda: idx // 1)
139-
tm.assert_raises_regex(TypeError, "cannot perform __floordiv__",
140+
tm.assert_raises_regex(TypeError, "cannot perform __rfloordiv__",
140141
lambda: 1 // idx)
141142

142143
def test_logical_compat(self):

pandas/tests/indexes/test_base.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from collections import defaultdict
88

99
import pandas.util.testing as tm
10+
from pandas.core.dtypes.generic import ABCIndex
1011
from pandas.core.dtypes.common import is_unsigned_integer_dtype
1112
from pandas.core.indexes.api import Index, MultiIndex
1213
from pandas.tests.indexes.common import Base
@@ -1988,6 +1989,17 @@ def test_addsub_arithmetic(self, dtype, delta):
19881989
tm.assert_index_equal(idx - idx, 0 * idx)
19891990
assert not (idx - idx).empty
19901991

1992+
def test_iadd_preserves_name(self):
1993+
# GH#17067, GH#19723 __iadd__ and __isub__ should preserve index name
1994+
ser = pd.Series([1, 2, 3])
1995+
ser.index.name = 'foo'
1996+
1997+
ser.index += 1
1998+
assert ser.index.name == "foo"
1999+
2000+
ser.index -= 1
2001+
assert ser.index.name == "foo"
2002+
19912003

19922004
class TestMixedIntIndex(Base):
19932005
# Mostly the tests from common.py for which the results differ
@@ -2301,9 +2313,17 @@ def test_ensure_index_from_sequences(self, data, names, expected):
23012313
tm.assert_index_equal(result, expected)
23022314

23032315

2304-
@pytest.mark.parametrize('opname', ['eq', 'ne', 'le', 'lt', 'ge', 'gt'])
2316+
@pytest.mark.parametrize('opname', ['eq', 'ne', 'le', 'lt', 'ge', 'gt',
2317+
'add', 'radd', 'sub', 'rsub',
2318+
'mul', 'rmul', 'truediv', 'rtruediv',
2319+
'floordiv', 'rfloordiv',
2320+
'pow', 'rpow', 'mod', 'divmod'])
23052321
def test_generated_op_names(opname, indices):
23062322
index = indices
2323+
if isinstance(index, ABCIndex) and opname == 'rsub':
2324+
# pd.Index.__rsub__ does not exist; though the method does exist
2325+
# for subclasses. see GH#19723
2326+
return
23072327
opname = '__{name}__'.format(name=opname)
23082328
method = getattr(index, opname)
23092329
assert method.__name__ == opname

0 commit comments

Comments
 (0)