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

BUG: fix index op names and pinning #19723

Merged
merged 15 commits into from
Feb 23, 2018
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
260 changes: 125 additions & 135 deletions pandas/core/indexes/base.py

Large diffs are not rendered by default.

26 changes: 19 additions & 7 deletions pandas/core/indexes/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,7 @@ def __add__(self, other):
return self._add_offset_array(other)
elif isinstance(self, TimedeltaIndex) and isinstance(other, Index):
if hasattr(other, '_add_delta'):
# i.e. DatetimeIndex, TimedeltaIndex, or PeriodIndex
return other._add_delta(self)
raise TypeError("cannot add TimedeltaIndex and {typ}"
.format(typ=type(other)))
Expand All @@ -685,7 +686,11 @@ def __add__(self, other):
return NotImplemented

cls.__add__ = __add__
cls.__radd__ = __add__

def __radd__(self, other):
# alias for __add__
return self.__add__(other)
cls.__radd__ = __radd__

def __sub__(self, other):
from pandas.core.index import Index
Expand All @@ -704,10 +709,10 @@ def __sub__(self, other):
# Array/Index of DateOffset objects
return self._sub_offset_array(other)
elif isinstance(self, TimedeltaIndex) and isinstance(other, Index):
if not isinstance(other, TimedeltaIndex):
raise TypeError("cannot subtract TimedeltaIndex and {typ}"
.format(typ=type(other).__name__))
return self._add_delta(-other)
# We checked above for timedelta64_dtype(other) so this
# must be invalid.
raise TypeError("cannot subtract TimedeltaIndex and {typ}"
.format(typ=type(other).__name__))
elif isinstance(other, DatetimeIndex):
return self._sub_datelike(other)
elif is_integer(other):
Expand All @@ -732,8 +737,15 @@ def __rsub__(self, other):
return -(self - other)
cls.__rsub__ = __rsub__

cls.__iadd__ = __add__
cls.__isub__ = __sub__
def __iadd__(self, other):
# alias for __add__
return self.__add__(other)
cls.__iadd__ = __iadd__
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this double assignment ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we just set cls.__iadd__ = __add__ then when we check for Index.__iadd__.__name__ we'll get __add__ instead of __iadd__. Not a big deal, but its cheap to make it pretty.


def __isub__(self, other):
# alias for __sub__
return self.__sub__(other)
cls.__isub__ = __isub__

def _add_delta(self, other):
return NotImplemented
Expand Down
5 changes: 3 additions & 2 deletions pandas/core/indexes/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,11 @@ def f(self):
return property(f)


def _dt_index_cmp(opname, cls, nat_result=False):
def _dt_index_cmp(opname, cls):
"""
Wrap comparison operations to convert datetime-like to datetime64
"""
nat_result = True if opname == '__ne__' else False

def wrapper(self, other):
func = getattr(super(DatetimeIndex, self), opname)
Expand Down Expand Up @@ -291,7 +292,7 @@ def _join_i8_wrapper(joinf, **kwargs):
def _add_comparison_methods(cls):
""" add in comparison methods """
cls.__eq__ = _dt_index_cmp('__eq__', cls)
cls.__ne__ = _dt_index_cmp('__ne__', cls, nat_result=True)
cls.__ne__ = _dt_index_cmp('__ne__', cls)
cls.__lt__ = _dt_index_cmp('__lt__', cls)
cls.__gt__ = _dt_index_cmp('__gt__', cls)
cls.__le__ = _dt_index_cmp('__le__', cls)
Expand Down
16 changes: 7 additions & 9 deletions pandas/core/indexes/period.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,26 +75,25 @@ def dt64arr_to_periodarr(data, freq, tz):
_DIFFERENT_FREQ_INDEX = period._DIFFERENT_FREQ_INDEX


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

def wrapper(self, other):
op = getattr(self._ndarray_values, opname)
if isinstance(other, Period):
func = getattr(self._ndarray_values, opname)
other_base, _ = _gfc(other.freq)
if other.freq != self.freq:
msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr)
raise IncompatibleFrequency(msg)

result = func(other.ordinal)
result = op(other.ordinal)
elif isinstance(other, PeriodIndex):
if other.freq != self.freq:
msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr)
raise IncompatibleFrequency(msg)

op = getattr(self._ndarray_values, opname)
result = op(other._ndarray_values)

mask = self._isnan | other._isnan
Expand All @@ -107,8 +106,7 @@ def wrapper(self, other):
result.fill(nat_result)
else:
other = Period(other, freq=self.freq)
func = getattr(self._ndarray_values, opname)
result = func(other.ordinal)
result = op(other.ordinal)

if self.hasnans:
result[self._isnan] = nat_result
Expand Down Expand Up @@ -230,7 +228,7 @@ class PeriodIndex(DatelikeOps, DatetimeIndexOpsMixin, Int64Index):
def _add_comparison_methods(cls):
""" add in comparison methods """
cls.__eq__ = _period_index_cmp('__eq__', cls)
cls.__ne__ = _period_index_cmp('__ne__', cls, nat_result=True)
cls.__ne__ = _period_index_cmp('__ne__', cls)
cls.__lt__ = _period_index_cmp('__lt__', cls)
cls.__gt__ = _period_index_cmp('__gt__', cls)
cls.__le__ = _period_index_cmp('__le__', cls)
Expand Down
56 changes: 17 additions & 39 deletions pandas/core/indexes/range.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from pandas.compat.numpy import function as nv

import pandas.core.common as com
from pandas.core import ops
from pandas.core.indexes.base import Index, _index_shared_docs
from pandas.util._decorators import Appender, cache_readonly
import pandas.core.dtypes.concat as _concat
Expand Down Expand Up @@ -569,16 +570,12 @@ def __floordiv__(self, other):
def _add_numeric_methods_binary(cls):
""" add in numeric methods, specialized to RangeIndex """

def _make_evaluate_binop(op, opstr, reversed=False, step=False):
def _make_evaluate_binop(op, step=False):
"""
Parameters
----------
op : callable that accepts 2 parms
perform the binary op
opstr : string
string name of ops
reversed : boolean, default False
if this is a reversed op, e.g. radd
step : callable, optional, default to False
op to apply to the step parm if not None
if False, use the existing step
Expand All @@ -588,13 +585,11 @@ def _evaluate_numeric_binop(self, other):
if isinstance(other, ABCSeries):
return NotImplemented

other = self._validate_for_numeric_binop(other, op, opstr)
other = self._validate_for_numeric_binop(other, op)
attrs = self._get_attributes_dict()
attrs = self._maybe_update_attributes(attrs)

left, right = self, other
if reversed:
left, right = right, left

try:
# apply if we have an override
Expand Down Expand Up @@ -628,43 +623,26 @@ def _evaluate_numeric_binop(self, other):

return result

except (ValueError, TypeError, AttributeError,
ZeroDivisionError):
except (ValueError, TypeError, ZeroDivisionError):
# Defer to Int64Index implementation
if reversed:
return op(other, self._int64index)
return op(self._int64index, other)
# TODO: Do attrs get handled reliably?

return _evaluate_numeric_binop

cls.__add__ = cls.__radd__ = _make_evaluate_binop(
operator.add, '__add__')
cls.__sub__ = _make_evaluate_binop(operator.sub, '__sub__')
cls.__rsub__ = _make_evaluate_binop(
operator.sub, '__sub__', reversed=True)
cls.__mul__ = cls.__rmul__ = _make_evaluate_binop(
operator.mul,
'__mul__',
step=operator.mul)
cls.__truediv__ = _make_evaluate_binop(
operator.truediv,
'__truediv__',
step=operator.truediv)
cls.__rtruediv__ = _make_evaluate_binop(
operator.truediv,
'__truediv__',
reversed=True,
step=operator.truediv)
cls.__add__ = _make_evaluate_binop(operator.add)
cls.__radd__ = _make_evaluate_binop(ops.radd)
cls.__sub__ = _make_evaluate_binop(operator.sub)
cls.__rsub__ = _make_evaluate_binop(ops.rsub)
cls.__mul__ = _make_evaluate_binop(operator.mul, step=operator.mul)
cls.__rmul__ = _make_evaluate_binop(ops.rmul, step=ops.rmul)
cls.__truediv__ = _make_evaluate_binop(operator.truediv,
step=operator.truediv)
cls.__rtruediv__ = _make_evaluate_binop(ops.rtruediv,
step=ops.rtruediv)
if not compat.PY3:
cls.__div__ = _make_evaluate_binop(
operator.div,
'__div__',
step=operator.div)
cls.__rdiv__ = _make_evaluate_binop(
operator.div,
'__div__',
reversed=True,
step=operator.div)
cls.__div__ = _make_evaluate_binop(operator.div, step=operator.div)
cls.__rdiv__ = _make_evaluate_binop(ops.rdiv, step=ops.rdiv)


RangeIndex._add_numeric_methods()
Expand Down
12 changes: 6 additions & 6 deletions pandas/core/indexes/timedeltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ def f(self):
return property(f)


def _td_index_cmp(opname, cls, nat_result=False):
def _td_index_cmp(opname, cls):
"""
Wrap comparison operations to convert timedelta-like to timedelta64
"""
nat_result = True if opname == '__ne__' else False

def wrapper(self, other):
msg = "cannot compare a TimedeltaIndex with type {0}"
Expand Down Expand Up @@ -184,7 +185,7 @@ def _join_i8_wrapper(joinf, **kwargs):
def _add_comparison_methods(cls):
""" add in comparison methods """
cls.__eq__ = _td_index_cmp('__eq__', cls)
cls.__ne__ = _td_index_cmp('__ne__', cls, nat_result=True)
cls.__ne__ = _td_index_cmp('__ne__', cls)
cls.__lt__ = _td_index_cmp('__lt__', cls)
cls.__gt__ = _td_index_cmp('__gt__', cls)
cls.__le__ = _td_index_cmp('__le__', cls)
Expand Down Expand Up @@ -370,11 +371,12 @@ def _add_delta(self, delta):
result = TimedeltaIndex(new_values, freq='infer', name=name)
return result

def _evaluate_with_timedelta_like(self, other, op, opstr, reversed=False):
def _evaluate_with_timedelta_like(self, other, op):
if isinstance(other, ABCSeries):
# GH#19042
return NotImplemented

opstr = '__{opname}__'.format(opname=op.__name__).replace('__r', '__')
# allow division by a timedelta
if opstr in ['__div__', '__truediv__', '__floordiv__']:
if _is_convertible_to_td(other):
Expand All @@ -385,11 +387,9 @@ def _evaluate_with_timedelta_like(self, other, op, opstr, reversed=False):

i8 = self.asi8
left, right = i8, other.value
if reversed:
left, right = right, left

if opstr in ['__floordiv__']:
result = left // right
result = op(left, right)
else:
result = op(left, np.float64(right))
result = self._maybe_mask_results(result, convert='float64')
Expand Down
5 changes: 3 additions & 2 deletions pandas/tests/indexes/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,17 @@ def test_numeric_compat(self):
idx = self.create_index()
tm.assert_raises_regex(TypeError, "cannot perform __mul__",
lambda: idx * 1)
tm.assert_raises_regex(TypeError, "cannot perform __mul__",
tm.assert_raises_regex(TypeError, "cannot perform __rmul__",
lambda: 1 * idx)

div_err = "cannot perform __truediv__" if PY3 \
else "cannot perform __div__"
tm.assert_raises_regex(TypeError, div_err, lambda: idx / 1)
div_err = div_err.replace(' __', ' __r')
tm.assert_raises_regex(TypeError, div_err, lambda: 1 / idx)
tm.assert_raises_regex(TypeError, "cannot perform __floordiv__",
lambda: idx // 1)
tm.assert_raises_regex(TypeError, "cannot perform __floordiv__",
tm.assert_raises_regex(TypeError, "cannot perform __rfloordiv__",
lambda: 1 // idx)

def test_logical_compat(self):
Expand Down
22 changes: 21 additions & 1 deletion pandas/tests/indexes/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from collections import defaultdict

import pandas.util.testing as tm
from pandas.core.dtypes.generic import ABCIndex
from pandas.core.dtypes.common import is_unsigned_integer_dtype
from pandas.core.indexes.api import Index, MultiIndex
from pandas.tests.indexes.common import Base
Expand Down Expand Up @@ -1988,6 +1989,17 @@ def test_addsub_arithmetic(self, dtype, delta):
tm.assert_index_equal(idx - idx, 0 * idx)
assert not (idx - idx).empty

def test_iadd_preserves_name(self):
# GH#17067, GH#19723 __iadd__ and __isub__ should preserve index name
ser = pd.Series([1, 2, 3])
ser.index.name = 'foo'

ser.index += 1
assert ser.index.name == "foo"

ser.index -= 1
assert ser.index.name == "foo"


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


@pytest.mark.parametrize('opname', ['eq', 'ne', 'le', 'lt', 'ge', 'gt'])
@pytest.mark.parametrize('opname', ['eq', 'ne', 'le', 'lt', 'ge', 'gt',
'add', 'radd', 'sub', 'rsub',
'mul', 'rmul', 'truediv', 'rtruediv',
'floordiv', 'rfloordiv',
'pow', 'rpow', 'mod', 'divmod'])
def test_generated_op_names(opname, indices):
index = indices
if isinstance(index, ABCIndex) and opname == 'rsub':
# pd.Index.__rsub__ does not exist; though the method does exist
# for subclasses. see GH#19723
return
opname = '__{name}__'.format(name=opname)
method = getattr(index, opname)
assert method.__name__ == opname