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 685ad1101efb9..2338fbe896eb0 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -594,63 +594,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._eadata, *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._eadata) - 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) - - def maybe_unwrap_index(obj): """ If operating against another Index object, we need to unwrap the underlying @@ -703,16 +646,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 380341f05252c..b61c2c6cd5bc6 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 @@ -1094,44 +1142,6 @@ def _eadata(self): 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/timedeltas.py b/pandas/core/indexes/timedeltas.py index 885902967d398..642f4557e65e6 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,8 @@ import pandas.core.common as com from pandas.core.indexes.base import Index, _index_shared_docs from pandas.core.indexes.datetimelike import ( - DatetimeIndexOpsMixin, maybe_unwrap_index, wrap_arithmetic_op, - wrap_array_method, wrap_field_accessor) + DatetimeIndexOpsMixin, DatetimelikeDelegateMixin, maybe_unwrap_index, + 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 @@ -43,8 +44,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 @@ -251,13 +284,6 @@ def _eadata(self): __div__ = __truediv__ __rdiv__ = __rtruediv__ - 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) - # Compat for frequency inference, see GH#23789 _is_monotonic_increasing = Index.is_monotonic_increasing _is_monotonic_decreasing = Index.is_monotonic_decreasing