Skip to content

Commit

Permalink
REF: Stop mixing DTA/TDA into DTI/TDI (#24476)
Browse files Browse the repository at this point in the history
* implement _index_data parts of #24024

* implement _eadata, dispatch arithmetic methods to it

* dont mix DatetimeLikeArrayMixin into DatetimeIndexOpsMixin

* dont inherit TimedeltaIndex from TimedeltaArray

* dont inherit from DatetimeArray

* use ea_passthrough

* remove previously-overriden overridings

* stop double-mixing

* stop over-writing

* handle+test object arrays

* Remove unused import

* flake8 fixup

* edits per comments
  • Loading branch information
jbrockmendel authored and TomAugspurger committed Dec 29, 2018
1 parent aeff38d commit 41b2b18
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 70 deletions.
30 changes: 7 additions & 23 deletions pandas/core/arrays/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,13 @@ def wrapper(self, other):
return ops.invalid_comparison(self, other, op)

if is_object_dtype(other):
result = op(self.astype('O'), np.array(other))
# We have to use _comp_method_OBJECT_ARRAY instead of numpy
# comparison otherwise it would fail to raise when
# comparing tz-aware and tz-naive
with np.errstate(all='ignore'):
result = ops._comp_method_OBJECT_ARRAY(op,
self.astype(object),
other)
o_mask = isna(other)
elif not (is_datetime64_dtype(other) or
is_datetime64tz_dtype(other)):
Expand Down Expand Up @@ -430,28 +436,6 @@ def _timezone(self):
"""
return timezones.get_timezone(self.tzinfo)

@property
def offset(self):
"""
get/set the frequency of the instance
"""
msg = ('{cls}.offset has been deprecated and will be removed '
'in a future version; use {cls}.freq instead.'
.format(cls=type(self).__name__))
warnings.warn(msg, FutureWarning, stacklevel=2)
return self.freq

@offset.setter
def offset(self, value):
"""
get/set the frequency of the instance
"""
msg = ('{cls}.offset has been deprecated and will be removed '
'in a future version; use {cls}.freq instead.'
.format(cls=type(self).__name__))
warnings.warn(msg, FutureWarning, stacklevel=2)
self.freq = value

@property # NB: override with cache_readonly in immutable subclasses
def is_normalized(self):
"""
Expand Down
49 changes: 38 additions & 11 deletions pandas/core/indexes/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from pandas._libs import NaT, iNaT, lib
from pandas.compat.numpy import function as nv
from pandas.errors import AbstractMethodError
from pandas.util._decorators import Appender, cache_readonly
from pandas.util._decorators import Appender, cache_readonly, deprecate_kwarg

from pandas.core.dtypes.common import (
ensure_int64, is_bool_dtype, is_dtype_equal, is_float, is_integer,
Expand All @@ -19,6 +19,7 @@

from pandas.core import algorithms, ops
from pandas.core.accessor import PandasDelegate
from pandas.core.arrays import ExtensionOpsMixin
from pandas.core.arrays.datetimelike import (
DatetimeLikeArrayMixin, _ensure_datetimelike_to_i8)
import pandas.core.indexes.base as ibase
Expand All @@ -30,15 +31,30 @@
_index_doc_kwargs = dict(ibase._index_doc_kwargs)


class DatetimeIndexOpsMixin(DatetimeLikeArrayMixin):
def ea_passthrough(name):
"""
common ops mixin to support a unified interface datetimelike Index
Make an alias for a method of the underlying ExtensionArray.
Parameters
----------
name : str
Returns
-------
method
"""
def method(self, *args, **kwargs):
return getattr(self._eadata, name)(*args, **kwargs)

method.__name__ = name
# TODO: docstrings
return method


# override DatetimeLikeArrayMixin method
copy = Index.copy
view = Index.view
__setitem__ = Index.__setitem__
class DatetimeIndexOpsMixin(ExtensionOpsMixin):
"""
common ops mixin to support a unified interface datetimelike Index
"""

# DatetimeLikeArrayMixin assumes subclasses are mutable, so these are
# properties there. They can be made into cache_readonly for Index
Expand All @@ -50,6 +66,14 @@ class DatetimeIndexOpsMixin(DatetimeLikeArrayMixin):
_resolution = cache_readonly(DatetimeLikeArrayMixin._resolution.fget)
resolution = cache_readonly(DatetimeLikeArrayMixin.resolution.fget)

_box_values = ea_passthrough("_box_values")
_maybe_mask_results = ea_passthrough("_maybe_mask_results")
__iter__ = ea_passthrough("__iter__")

@property
def freqstr(self):
return self._eadata.freqstr

def unique(self, level=None):
if level is not None:
self._validate_index_level(level)
Expand All @@ -74,9 +98,6 @@ def wrapper(self, other):
wrapper.__name__ = '__{}__'.format(op.__name__)
return wrapper

# A few methods that are shared
_maybe_mask_results = DatetimeLikeArrayMixin._maybe_mask_results

# ------------------------------------------------------------------------

def equals(self, other):
Expand Down Expand Up @@ -549,7 +570,7 @@ def _concat_same_dtype(self, to_concat, name):
# - remove the .asi8 here
# - remove the _maybe_box_as_values
# - combine with the `else` block
new_data = self._concat_same_type(to_concat).asi8
new_data = self._eadata._concat_same_type(to_concat).asi8
else:
new_data = type(self._values)._concat_same_type(to_concat)

Expand Down Expand Up @@ -581,6 +602,12 @@ def _time_shift(self, periods, freq=None):
result = self._eadata._time_shift(periods, freq=freq)
return type(self)(result, name=self.name)

@deprecate_kwarg(old_arg_name='n', new_arg_name='periods')
@Appender(DatetimeLikeArrayMixin.shift.__doc__)
def shift(self, periods, freq=None):
result = self._eadata.shift(periods, freq=freq)
return type(self)(result, name=self.name)


def wrap_arithmetic_op(self, other, result):
if result is NotImplemented:
Expand Down
88 changes: 72 additions & 16 deletions pandas/core/indexes/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import pandas.core.common as com
from pandas.core.indexes.base import Index
from pandas.core.indexes.datetimelike import (
DatetimeIndexOpsMixin, DatetimelikeDelegateMixin)
DatetimeIndexOpsMixin, DatetimelikeDelegateMixin, ea_passthrough)
from pandas.core.indexes.numeric import Int64Index
from pandas.core.ops import get_op_result_name
import pandas.core.tools.datetimes as tools
Expand Down Expand Up @@ -96,19 +96,13 @@ class DatetimeDelegateMixin(DatetimelikeDelegateMixin):
_delegate_class = DatetimeArray


@delegate_names(DatetimeArray, ["to_period", "tz_localize", "tz_convert",
"day_name", "month_name"],
typ="method", overwrite=True)
@delegate_names(DatetimeArray,
DatetimeArray._field_ops, typ="property", overwrite=True)
@delegate_names(DatetimeArray,
DatetimeDelegateMixin._delegated_properties,
typ="property")
@delegate_names(DatetimeArray,
DatetimeDelegateMixin._delegated_methods,
typ="method", overwrite=False)
class DatetimeIndex(DatetimeArray, DatetimeIndexOpsMixin, Int64Index,
DatetimeDelegateMixin):
class DatetimeIndex(DatetimeIndexOpsMixin, Int64Index, DatetimeDelegateMixin):
"""
Immutable ndarray of datetime64 data, represented internally as int64, and
which can be boxed to Timestamp objects that are subclasses of datetime and
Expand Down Expand Up @@ -268,6 +262,7 @@ def _join_i8_wrapper(joinf, **kwargs):
_object_ops = DatetimeArray._object_ops
_field_ops = DatetimeArray._field_ops
_datetimelike_ops = DatetimeArray._datetimelike_ops
_datetimelike_methods = DatetimeArray._datetimelike_methods

# --------------------------------------------------------------------
# Constructors
Expand All @@ -294,8 +289,8 @@ def __new__(cls, data=None,
"endpoints is deprecated. Use "
"`pandas.date_range` instead.",
FutureWarning, stacklevel=2)

return cls(dtarr, name=name)
return cls._simple_new(
dtarr._data, freq=dtarr.freq, tz=dtarr.tz, name=name)

if is_scalar(data):
raise TypeError("{cls}() must be called with a "
Expand Down Expand Up @@ -331,7 +326,11 @@ def _simple_new(cls, values, name=None, freq=None, tz=None, dtype=None):
# DatetimeArray._simple_new will accept either i8 or M8[ns] dtypes
assert isinstance(values, np.ndarray), type(values)

result = super(DatetimeIndex, cls)._simple_new(values, freq, tz)
dtarr = DatetimeArray._simple_new(values, freq=freq, tz=tz)
result = object.__new__(cls)
result._data = dtarr._data
result._freq = dtarr.freq
result._tz = dtarr.tz
result.name = name
# For groupby perf. See note in indexes/base about _index_data
result._index_data = result._data
Expand All @@ -340,6 +339,10 @@ def _simple_new(cls, values, name=None, freq=None, tz=None, dtype=None):

# --------------------------------------------------------------------

@property
def dtype(self):
return self._eadata.dtype

@property
def _values(self):
# tz-naive -> ndarray
Expand All @@ -360,6 +363,8 @@ def tz(self, value):
raise AttributeError("Cannot directly set timezone. Use tz_localize() "
"or tz_convert() as appropriate")

tzinfo = tz

@property
def size(self):
# TODO: Remove this when we have a DatetimeTZArray
Expand Down Expand Up @@ -670,7 +675,7 @@ def intersection(self, other):
def _get_time_micros(self):
values = self.asi8
if self.tz is not None and not timezones.is_utc(self.tz):
values = self._local_timestamps()
values = self._eadata._local_timestamps()
return fields.get_time_micros(values)

def to_series(self, keep_tz=None, index=None, name=None):
Expand Down Expand Up @@ -1139,12 +1144,64 @@ def _eadata(self):
_is_monotonic_increasing = Index.is_monotonic_increasing
_is_monotonic_decreasing = Index.is_monotonic_decreasing
_is_unique = Index.is_unique
astype = DatetimeIndexOpsMixin.astype

_timezone = cache_readonly(DatetimeArray._timezone.fget)
is_normalized = cache_readonly(DatetimeArray.is_normalized.fget)
_resolution = cache_readonly(DatetimeArray._resolution.fget)

strftime = ea_passthrough("strftime")
_has_same_tz = ea_passthrough("_has_same_tz")
__array__ = ea_passthrough("__array__")

@property
def offset(self):
"""
get/set the frequency of the instance
"""
msg = ('{cls}.offset has been deprecated and will be removed '
'in a future version; use {cls}.freq instead.'
.format(cls=type(self).__name__))
warnings.warn(msg, FutureWarning, stacklevel=2)
return self.freq

@offset.setter
def offset(self, value):
"""
get/set the frequency of the instance
"""
msg = ('{cls}.offset has been deprecated and will be removed '
'in a future version; use {cls}.freq instead.'
.format(cls=type(self).__name__))
warnings.warn(msg, FutureWarning, stacklevel=2)
self.freq = value

@property
def freq(self):
return self._freq

@freq.setter
def freq(self, value):
if value is not None:
# let DatetimeArray to validation
self._eadata.freq = value

self._freq = to_offset(value)

def __getitem__(self, key):
result = self._eadata.__getitem__(key)
if is_scalar(result):
return result
elif result.ndim > 1:
# To support MPL which performs slicing with 2 dim
# even though it only has 1 dim by definition
assert isinstance(result, np.ndarray), result
return result
return type(self)(result, name=self.name)

@property
def _box_func(self):
return lambda x: Timestamp(x, tz=self.tz)

# --------------------------------------------------------------------

@Substitution(klass='DatetimeIndex')
Expand Down Expand Up @@ -1486,9 +1543,8 @@ def date_range(start=None, end=None, periods=None, freq=None, tz=None,
start=start, end=end, periods=periods,
freq=freq, tz=tz, normalize=normalize,
closed=closed, **kwargs)

result = DatetimeIndex(dtarr, name=name)
return result
return DatetimeIndex._simple_new(
dtarr._data, tz=dtarr.tz, freq=dtarr.freq, name=name)


def bdate_range(start=None, end=None, periods=None, freq='B', tz=None,
Expand Down
Loading

0 comments on commit 41b2b18

Please sign in to comment.