diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 6c73f0ec16c15..eb878a9071d55 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -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)): @@ -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): """ diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 3810f204185fd..d090d0e7d9caa 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -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, @@ -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 @@ -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 @@ -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) @@ -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): @@ -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) @@ -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: diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 1e6daabcc0445..a8651a25eef6b 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -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 @@ -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 @@ -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 @@ -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 " @@ -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 @@ -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 @@ -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 @@ -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): @@ -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') @@ -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, diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index aa0e1edf06af0..53cd358e2f906 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -64,19 +64,13 @@ class TimedeltaDelegateMixin(DatetimelikeDelegateMixin): } -@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, +class TimedeltaIndex(DatetimeIndexOpsMixin, dtl.TimelikeOps, Int64Index, TimedeltaDelegateMixin): """ Immutable ndarray of timedelta64 data, represented internally as int64, and @@ -206,9 +200,9 @@ def __new__(cls, data=None, unit=None, freq=None, start=None, end=None, "endpoints is deprecated. Use " "`pandas.timedelta_range` instead.", FutureWarning, stacklevel=2) - tdarr = TimedeltaArray._generate_range(start, end, periods, freq, - closed=closed) - return cls(tdarr, name=name) + result = TimedeltaArray._generate_range(start, end, periods, freq, + closed=closed) + return cls._simple_new(result._data, freq=freq, name=name) if is_scalar(data): raise TypeError('{cls}() must be called with a ' @@ -223,10 +217,9 @@ def __new__(cls, data=None, unit=None, freq=None, start=None, end=None, # - Cases checked above all return/raise before reaching here - # - result = cls._from_sequence(data, freq=freq, unit=unit, - dtype=dtype, copy=copy) - result.name = name - return result + tdarr = TimedeltaArray._from_sequence(data, freq=freq, unit=unit, + dtype=dtype, copy=copy) + return cls._simple_new(tdarr._data, freq=tdarr.freq, name=name) @classmethod def _simple_new(cls, values, name=None, freq=None, dtype=_TD_DTYPE): @@ -239,7 +232,11 @@ def _simple_new(cls, values, name=None, freq=None, dtype=_TD_DTYPE): values = values.view('m8[ns]') assert values.dtype == 'm8[ns]', values.dtype - result = super(TimedeltaIndex, cls)._simple_new(values, freq) + freq = to_offset(freq) + tdarr = TimedeltaArray._simple_new(values, freq=freq) + result = object.__new__(cls) + result._data = tdarr._data + result._freq = tdarr._freq result.name = name # For groupby perf. See note in indexes/base about _index_data result._index_data = result._data @@ -304,6 +301,33 @@ def _eadata(self): _is_monotonic_decreasing = Index.is_monotonic_decreasing _is_unique = Index.is_unique + _create_comparison_method = DatetimeIndexOpsMixin._create_comparison_method + # TODO: make sure we have a test for name retention analogous + # to series.test_arithmetic.test_ser_cmp_result_names; + # also for PeriodIndex which I think may be missing one + + @property + def _box_func(self): + return lambda x: Timedelta(x, unit='ns') + + def __getitem__(self, key): + result = self._eadata.__getitem__(key) + if is_scalar(result): + return result + return type(self)(result, name=self.name) + + @property + def freq(self): # TODO: get via eadata + return self._freq + + @freq.setter + def freq(self, value): # TODO: get via eadata + if value is not None: + # dispatch to TimedeltaArray to validate frequency + self._eadata.freq = value + + self._freq = to_offset(value) + # ------------------------------------------------------------------- @Appender(_index_shared_docs['astype']) @@ -792,4 +816,4 @@ def timedelta_range(start=None, end=None, periods=None, freq=None, freq, freq_infer = dtl.maybe_infer_freq(freq) tdarr = TimedeltaArray._generate_range(start, end, periods, freq, closed=closed) - return TimedeltaIndex(tdarr, name=name) + return TimedeltaIndex._simple_new(tdarr._data, freq=tdarr.freq, name=name) diff --git a/pandas/tests/arithmetic/test_datetime64.py b/pandas/tests/arithmetic/test_datetime64.py index 3065785649359..44817467b4694 100644 --- a/pandas/tests/arithmetic/test_datetime64.py +++ b/pandas/tests/arithmetic/test_datetime64.py @@ -593,12 +593,17 @@ def test_comparison_tzawareness_compat(self, op, box_with_array): # DataFrame op is invalid until transpose bug is fixed with pytest.raises(TypeError): op(dr, list(dz)) + with pytest.raises(TypeError): + op(dr, np.array(list(dz), dtype=object)) + with pytest.raises(TypeError): op(dz, dr) if box_with_array is not pd.DataFrame: # DataFrame op is invalid until transpose bug is fixed with pytest.raises(TypeError): op(dz, list(dr)) + with pytest.raises(TypeError): + op(dz, np.array(list(dr), dtype=object)) # Check that there isn't a problem aware-aware and naive-naive do not # raise @@ -1998,7 +2003,7 @@ def test_dti_isub_tdi(self, tz_naive_fixture): result -= tdi tm.assert_index_equal(result, expected) - msg = 'cannot subtract .*TimedeltaArrayMixin' + msg = 'cannot subtract .* from a TimedeltaArrayMixin' with pytest.raises(TypeError, match=msg): tdi -= dti diff --git a/pandas/tests/indexes/common.py b/pandas/tests/indexes/common.py index 0c886b9fd3c4b..499f01f0e7f7b 100644 --- a/pandas/tests/indexes/common.py +++ b/pandas/tests/indexes/common.py @@ -610,7 +610,9 @@ def test_equals_op(self): index_b = index_a[0:-1] index_c = index_a[0:-1].append(index_a[-2:-1]) index_d = index_a[0:1] - with pytest.raises(ValueError, match="Lengths must match"): + + msg = "Lengths must match|could not be broadcast" + with pytest.raises(ValueError, match=msg): index_a == index_b expected1 = np.array([True] * n) expected2 = np.array([True] * (n - 1) + [False]) @@ -622,7 +624,7 @@ def test_equals_op(self): array_b = np.array(index_a[0:-1]) array_c = np.array(index_a[0:-1].append(index_a[-2:-1])) array_d = np.array(index_a[0:1]) - with pytest.raises(ValueError, match="Lengths must match"): + with pytest.raises(ValueError, match=msg): index_a == array_b tm.assert_numpy_array_equal(index_a == array_a, expected1) tm.assert_numpy_array_equal(index_a == array_c, expected2) @@ -632,7 +634,7 @@ def test_equals_op(self): series_b = Series(array_b) series_c = Series(array_c) series_d = Series(array_d) - with pytest.raises(ValueError, match="Lengths must match"): + with pytest.raises(ValueError, match=msg): index_a == series_b tm.assert_numpy_array_equal(index_a == series_a, expected1)