diff --git a/pandas/core/indexes/accessors.py b/pandas/core/indexes/accessors.py index ce3143b342cec..02729ff90d51a 100644 --- a/pandas/core/indexes/accessors.py +++ b/pandas/core/indexes/accessors.py @@ -11,6 +11,7 @@ is_timedelta64_dtype, is_categorical_dtype, is_list_like) + from pandas.core.base import PandasDelegate, NoNewAttributesMixin from pandas.core.indexes.datetimes import DatetimeIndex from pandas._libs.period import IncompatibleFrequency # noqa @@ -48,8 +49,10 @@ def maybe_to_datetimelike(data, copy=False): DelegatedClass """ - from pandas import Series + from pandas import Series, Index + if isinstance(data, Index): + data = data.to_series() if not isinstance(data, Series): raise TypeError("cannot convert an object of type {0} to a " "datetimelike index".format(type(data))) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 4aecc75d95971..e5ac7cf601369 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -1592,6 +1592,13 @@ def is_all_dates(self): return False return is_datetime_array(_ensure_object(self.values)) + @property + def dt(self): + # Non-raising versions of the `.dt` attribute are available in + # DatetimeIndex, PeriodIndex, and TimedeltaIndex. + raise AttributeError("Can only use .dt accessor with datetimelike " + "values") + def __iter__(self): return iter(self.values) diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 845c71b6c41d8..09602408fa2bb 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -125,6 +125,21 @@ def ceil(self, freq): class DatetimeIndexOpsMixin(object): """ common ops mixin to support a unified inteface datetimelike Index """ + @cache_readonly + def dt(self): + """ + For a datetime-like Index object, `self.dt` returns `self` so that + datetime-like attributes can be accessed symmetrically for Index + and Series objects. + + For Index objects that are not datetime-like, `self.dt` will raise + and AttributeError. + """ + # Note: we use a `cache_readonly` instead of AccessorProperty + # to avoid circular imports. + from pandas.core.indexes import accessors + return accessors.CombinedDatetimelikeProperties._make_accessor(self) + def equals(self, other): """ Determines if two Index objects contain the same elements. diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index ef36e4a91aa1c..4c46c83b76b74 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -1444,6 +1444,47 @@ def test_join_self(self): joined = res.join(res, how=kind) assert res is joined + def test_dt_accessor(self): + # index.dt should raise AttributeError for all index classes other + # than DatetimeIndex, PeriodIndex, and TimedeltaIndex + no_dt_examples = [pd.Index([np.nan]), + pd.Index([np.nan, 1]), + pd.Index([1, 2, np.nan]), + pd.Index(['foo', 'bar']), + pd.RangeIndex(12), + pd.CategoricalIndex(['a', 'b', np.nan])] + + dr = pd.date_range('1994-10-06', periods=4, freq='D') + mi = pd.MultiIndex.from_product([[1, 2], ['foo', 'bar'], dr]) + no_dt_examples.append(mi) + mi2 = pd.MultiIndex.from_product([range(3), dr]) + no_dt_examples.append(mi2) + + for index in no_dt_examples: + # Note: in py2, `hasattr` will return False when attempting + # to access the attribute raises. + assert not hasattr(index, 'dt') + + dt_examples = [dr, + pd.to_datetime(['NaT']), + pd.to_datetime(['NaT', '2000-01-01']), + pd.to_datetime(['2000-01-01', 'NaT', '2000-01-02']), + pd.to_timedelta(['1 day', 'NaT']) + ] + + for index in dt_examples: + assert hasattr(index, 'dt') + # Note: index.dt.day returns a Series that has index as its + # index, whereas index.day is just a index containing the day + # values. + dt = index.dt + if isinstance(index, pd.TimedeltaIndex): + tm.assert_almost_equal(dt.days.values, index.days.values) + tm.assert_almost_equal(dt.seconds.values, index.seconds.values) + else: + tm.assert_almost_equal(dt.day.values, index.day.values) + tm.assert_almost_equal(dt.year.values, index.year.values) + def test_str_attribute(self): # GH9068 methods = ['strip', 'rstrip', 'lstrip']