From ab326d6b67cf6a90a707007bb2a083ed54548023 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 27 Dec 2018 18:41:15 -0800 Subject: [PATCH] implement Delegate portions of #24024 --- pandas/core/indexes/accessors.py | 20 +++--- pandas/core/indexes/datetimelike.py | 63 +------------------ pandas/core/indexes/datetimes.py | 95 +++++++++++++++++------------ pandas/core/indexes/period.py | 4 ++ pandas/core/indexes/timedeltas.py | 49 ++++++++++++--- 5 files changed, 112 insertions(+), 119 deletions(-) diff --git a/pandas/core/indexes/accessors.py b/pandas/core/indexes/accessors.py index 791ef8223de98..842fcd0680467 100644 --- a/pandas/core/indexes/accessors.py +++ b/pandas/core/indexes/accessors.py @@ -11,9 +11,11 @@ from pandas.core.accessor import PandasDelegate, delegate_names from pandas.core.algorithms import take_1d +from pandas.core.arrays import ( + DatetimeArrayMixin as DatetimeArray, PeriodArray, + TimedeltaArrayMixin as TimedeltaArray) from pandas.core.base import NoNewAttributesMixin, PandasObject from pandas.core.indexes.datetimes import DatetimeIndex -from pandas.core.indexes.period import PeriodArray from pandas.core.indexes.timedeltas import TimedeltaIndex @@ -106,11 +108,11 @@ def _delegate_method(self, name, *args, **kwargs): return result -@delegate_names(delegate=DatetimeIndex, - accessors=DatetimeIndex._datetimelike_ops, +@delegate_names(delegate=DatetimeArray, + accessors=DatetimeArray._datetimelike_ops, typ="property") -@delegate_names(delegate=DatetimeIndex, - accessors=DatetimeIndex._datetimelike_methods, +@delegate_names(delegate=DatetimeArray, + accessors=DatetimeArray._datetimelike_methods, typ="method") class DatetimeProperties(Properties): """ @@ -177,11 +179,11 @@ def freq(self): return self._get_values().inferred_freq -@delegate_names(delegate=TimedeltaIndex, - accessors=TimedeltaIndex._datetimelike_ops, +@delegate_names(delegate=TimedeltaArray, + accessors=TimedeltaArray._datetimelike_ops, typ="property") -@delegate_names(delegate=TimedeltaIndex, - accessors=TimedeltaIndex._datetimelike_methods, +@delegate_names(delegate=TimedeltaArray, + accessors=TimedeltaArray._datetimelike_methods, typ="method") class TimedeltaProperties(Properties): """ diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index e3d24bfbed7c3..255238a59c423 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -572,63 +572,6 @@ def wrap_arithmetic_op(self, other, result): return result -def wrap_array_method(method, pin_name=False): - """ - Wrap a DatetimeArray/TimedeltaArray/PeriodArray method so that the - returned object is an Index subclass instead of ndarray or ExtensionArray - subclass. - - Parameters - ---------- - method : method of Datetime/Timedelta/Period Array class - pin_name : bool - Whether to set name=self.name on the output Index - - Returns - ------- - method - """ - def index_method(self, *args, **kwargs): - result = method(self, *args, **kwargs) - - # Index.__new__ will choose the appropriate subclass to return - result = Index(result) - if pin_name: - result.name = self.name - return result - - index_method.__name__ = method.__name__ - index_method.__doc__ = method.__doc__ - return index_method - - -def wrap_field_accessor(prop): - """ - Wrap a DatetimeArray/TimedeltaArray/PeriodArray array-returning property - to return an Index subclass instead of ndarray or ExtensionArray subclass. - - Parameters - ---------- - prop : property - - Returns - ------- - new_prop : property - """ - fget = prop.fget - - def f(self): - result = fget(self) - if is_bool_dtype(result): - # return numpy array b/c there is no BoolIndex - return result - return Index(result, name=self.name) - - f.__name__ = fget.__name__ - f.__doc__ = fget.__doc__ - return property(f) - - class DatetimelikeDelegateMixin(PandasDelegate): """ Delegation mechanism, specific for Datetime, Timedelta, and Period types. @@ -659,16 +602,16 @@ def _delegate_class(self): raise AbstractMethodError def _delegate_property_get(self, name, *args, **kwargs): - result = getattr(self._data, name) + result = getattr(self._eadata, name) if name not in self._raw_properties: result = Index(result, name=self.name) return result def _delegate_property_set(self, name, value, *args, **kwargs): - setattr(self._data, name, value) + setattr(self._eadata, name, value) def _delegate_method(self, name, *args, **kwargs): - result = operator.methodcaller(name, *args, **kwargs)(self._data) + result = operator.methodcaller(name, *args, **kwargs)(self._eadata) if name not in self._raw_methods: result = Index(result, name=self.name) return result diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 09e741af363da..2e3120aab944c 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -20,13 +20,14 @@ import pandas.core.dtypes.concat as _concat from pandas.core.dtypes.missing import isna +from pandas.core.accessor import delegate_names from pandas.core.arrays.datetimes import ( DatetimeArrayMixin as DatetimeArray, _to_m8) from pandas.core.base import _shared_docs import pandas.core.common as com from pandas.core.indexes.base import Index, _index_shared_docs from pandas.core.indexes.datetimelike import ( - DatetimeIndexOpsMixin, wrap_array_method, wrap_field_accessor) + DatetimeIndexOpsMixin, DatetimelikeDelegateMixin) from pandas.core.indexes.numeric import Int64Index from pandas.core.ops import get_op_result_name import pandas.core.tools.datetimes as tools @@ -61,7 +62,54 @@ def _new_DatetimeIndex(cls, d): return result -class DatetimeIndex(DatetimeArray, DatetimeIndexOpsMixin, Int64Index): +class DatetimeDelegateMixin(DatetimelikeDelegateMixin): + # Most attrs are dispatched via datetimelike_{ops,methods} + # Some are "raw" methods, the result is not not re-boxed in an Index + # We also have a few "extra" attrs, which may or may not be raw, + # which we we dont' want to expose in the .dt accessor. + _extra_methods = [ + 'to_period', + 'to_perioddelta', + 'to_julian_date', + ] + _extra_raw_methods = [ + 'to_pydatetime', + '_local_timestamps', + '_has_same_tz', + ] + _extra_raw_properties = [ + '_box_func', + 'tz', 'tzinfo', + ] + _delegated_properties = ( + DatetimeArray._datetimelike_ops + _extra_raw_properties + ) + _delegated_methods = ( + DatetimeArray._datetimelike_methods + _extra_methods + + _extra_raw_methods + ) + _raw_properties = { + 'date', + 'time', + 'timetz', + } | set(DatetimeArray._bool_ops) | set(_extra_raw_properties) + _raw_methods = set(_extra_raw_methods) + _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): """ Immutable ndarray of datetime64 data, represented internally as int64, and which can be boxed to Timestamp objects that are subclasses of datetime and @@ -1089,6 +1137,11 @@ def slice_indexer(self, start=None, end=None, step=None, kind=None): # -------------------------------------------------------------------- # Wrapping DatetimeArray + @property + def _eadata(self): + return DatetimeArray._simple_new(self._data, + tz=self.tz, freq=self.freq) + # Compat for frequency inference, see GH#23789 _is_monotonic_increasing = Index.is_monotonic_increasing _is_monotonic_decreasing = Index.is_monotonic_decreasing @@ -1098,44 +1151,6 @@ def slice_indexer(self, start=None, end=None, step=None, kind=None): is_normalized = cache_readonly(DatetimeArray.is_normalized.fget) _resolution = cache_readonly(DatetimeArray._resolution.fget) - year = wrap_field_accessor(DatetimeArray.year) - month = wrap_field_accessor(DatetimeArray.month) - day = wrap_field_accessor(DatetimeArray.day) - hour = wrap_field_accessor(DatetimeArray.hour) - minute = wrap_field_accessor(DatetimeArray.minute) - second = wrap_field_accessor(DatetimeArray.second) - microsecond = wrap_field_accessor(DatetimeArray.microsecond) - nanosecond = wrap_field_accessor(DatetimeArray.nanosecond) - weekofyear = wrap_field_accessor(DatetimeArray.weekofyear) - week = weekofyear - dayofweek = wrap_field_accessor(DatetimeArray.dayofweek) - weekday = dayofweek - - weekday_name = wrap_field_accessor(DatetimeArray.weekday_name) - - dayofyear = wrap_field_accessor(DatetimeArray.dayofyear) - quarter = wrap_field_accessor(DatetimeArray.quarter) - days_in_month = wrap_field_accessor(DatetimeArray.days_in_month) - daysinmonth = days_in_month - is_month_start = wrap_field_accessor(DatetimeArray.is_month_start) - is_month_end = wrap_field_accessor(DatetimeArray.is_month_end) - is_quarter_start = wrap_field_accessor(DatetimeArray.is_quarter_start) - is_quarter_end = wrap_field_accessor(DatetimeArray.is_quarter_end) - is_year_start = wrap_field_accessor(DatetimeArray.is_year_start) - is_year_end = wrap_field_accessor(DatetimeArray.is_year_end) - is_leap_year = wrap_field_accessor(DatetimeArray.is_leap_year) - - tz_localize = wrap_array_method(DatetimeArray.tz_localize, True) - tz_convert = wrap_array_method(DatetimeArray.tz_convert, True) - to_perioddelta = wrap_array_method(DatetimeArray.to_perioddelta, - False) - to_period = wrap_array_method(DatetimeArray.to_period, True) - normalize = wrap_array_method(DatetimeArray.normalize, True) - to_julian_date = wrap_array_method(DatetimeArray.to_julian_date, - False) - month_name = wrap_array_method(DatetimeArray.month_name, True) - day_name = wrap_array_method(DatetimeArray.day_name, True) - # -------------------------------------------------------------------- @Substitution(klass='DatetimeIndex') diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index b15604a57fb81..05df87365a411 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -247,6 +247,10 @@ def _simple_new(cls, values, name=None, freq=None, **kwargs): # ------------------------------------------------------------------------ # Data + @property + def _eadata(self): + return self._data + @property def _ndarray_values(self): return self._data._ndarray_values diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 47f7f7cf860fc..0151f17e65f9b 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -15,6 +15,7 @@ import pandas.core.dtypes.concat as _concat from pandas.core.dtypes.missing import isna +from pandas.core.accessor import delegate_names from pandas.core.arrays import datetimelike as dtl from pandas.core.arrays.timedeltas import ( TimedeltaArrayMixin as TimedeltaArray, _is_convertible_to_td, _to_m8) @@ -22,8 +23,7 @@ import pandas.core.common as com from pandas.core.indexes.base import Index, _index_shared_docs from pandas.core.indexes.datetimelike import ( - DatetimeIndexOpsMixin, wrap_arithmetic_op, wrap_array_method, - wrap_field_accessor) + DatetimeIndexOpsMixin, DatetimelikeDelegateMixin, wrap_arithmetic_op) from pandas.core.indexes.numeric import Int64Index from pandas.core.ops import get_op_result_name from pandas.core.tools.timedeltas import _coerce_scalar_to_timedelta_type @@ -47,8 +47,40 @@ def method(self, other): return method +class TimedeltaDelegateMixin(DatetimelikeDelegateMixin): + # Most attrs are dispatched via datetimelike_{ops,methods} + # Some are "raw" methods, the result is not not re-boxed in an Index + # We also have a few "extra" attrs, which may or may not be raw, + # which we we dont' want to expose in the .dt accessor. + _delegate_class = TimedeltaArray + _delegated_properties = (TimedeltaArray._datetimelike_ops + [ + 'components', + ]) + _delegated_methods = TimedeltaArray._datetimelike_methods + [ + '_box_values', + ] + _raw_properties = { + 'components', + } + _raw_methods = { + 'to_pytimedelta', + } + + +@delegate_names(TimedeltaArray, + ["to_pytimedelta", "total_seconds"], + typ="method", overwrite=True) +@delegate_names(TimedeltaArray, + ["days", "seconds", "microseconds", "nanoseconds"], + typ="property", overwrite=True) +@delegate_names(TimedeltaArray, + TimedeltaDelegateMixin._delegated_properties, + typ="property") +@delegate_names(TimedeltaArray, + TimedeltaDelegateMixin._delegated_methods, + typ="method", overwrite=False) class TimedeltaIndex(TimedeltaArray, DatetimeIndexOpsMixin, - dtl.TimelikeOps, Int64Index): + dtl.TimelikeOps, Int64Index, TimedeltaDelegateMixin): """ Immutable ndarray of timedelta64 data, represented internally as int64, and which can be boxed to timedelta objects @@ -237,6 +269,10 @@ def _format_native_types(self, na_rep=u'NaT', date_format=None, **kwargs): # ------------------------------------------------------------------- # Wrapping TimedeltaArray + @property + def _eadata(self): + return TimedeltaArray._simple_new(self._data, freq=self.freq) + __mul__ = _make_wrapped_arith_op("__mul__") __rmul__ = _make_wrapped_arith_op("__rmul__") __floordiv__ = _make_wrapped_arith_op("__floordiv__") @@ -246,13 +282,6 @@ def _format_native_types(self, na_rep=u'NaT', date_format=None, **kwargs): __divmod__ = _make_wrapped_arith_op("__divmod__") __rdivmod__ = _make_wrapped_arith_op("__rdivmod__") - days = wrap_field_accessor(TimedeltaArray.days) - seconds = wrap_field_accessor(TimedeltaArray.seconds) - microseconds = wrap_field_accessor(TimedeltaArray.microseconds) - nanoseconds = wrap_field_accessor(TimedeltaArray.nanoseconds) - - total_seconds = wrap_array_method(TimedeltaArray.total_seconds, True) - def __truediv__(self, other): oth = other if isinstance(other, Index):