Skip to content

BUG: fix index op names and pinning #19723

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

Merged
merged 15 commits into from
Feb 23, 2018
Merged
Show file tree
Hide file tree
Changes from 4 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
268 changes: 131 additions & 137 deletions pandas/core/indexes/base.py

Large diffs are not rendered by default.

27 changes: 20 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,11 @@ 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)
assert not is_timedelta64_dtype(other)
Copy link
Contributor

Choose a reason for hiding this comment

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

this is redundant

Copy link
Member Author

Choose a reason for hiding this comment

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

Will remove.

# 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 +738,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
55 changes: 17 additions & 38 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,27 @@ def _evaluate_numeric_binop(self, other):

return result

except (ValueError, TypeError, AttributeError,
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
6 changes: 5 additions & 1 deletion pandas/tests/indexes/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2301,7 +2301,11 @@ 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
opname = '__{name}__'.format(name=opname)
Expand Down