diff --git a/pandas/_libs/tslibs/offsets.pyi b/pandas/_libs/tslibs/offsets.pyi index c3d550c7a5ba9..34c5c661f6ec9 100644 --- a/pandas/_libs/tslibs/offsets.pyi +++ b/pandas/_libs/tslibs/offsets.pyi @@ -13,6 +13,7 @@ from typing import ( import numpy as np +from pandas._libs.tslibs.nattype import NaTType from pandas._typing import npt from .timedeltas import Timedelta @@ -49,6 +50,8 @@ class BaseOffset: @overload def __radd__(self, other: npt.NDArray[np.object_]) -> npt.NDArray[np.object_]: ... @overload + def __radd__(self, other: NaTType) -> NaTType: ... + @overload def __radd__(self: _BaseOffsetT, other: BaseOffset) -> _BaseOffsetT: ... @overload def __radd__(self, other: _DatetimeT) -> _DatetimeT: ... diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 9d32bc008eb25..c946fc2ac6e13 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -925,7 +925,7 @@ def _maybe_mask_results( # Frequency Properties/Methods @property - def freq(self): + def freq(self) -> BaseOffset | None: """ Return the frequency object if it is set, otherwise None. """ @@ -1220,7 +1220,9 @@ def _sub_period(self, other: Period) -> npt.NDArray[np.object_]: new_i8_data = checked_add_with_arr( self.asi8, -other.ordinal, arr_mask=self._isnan ) - new_data = np.array([self.freq.base * x for x in new_i8_data]) + new_data = np.array( + [cast("PeriodArray", self).freq.base * x for x in new_i8_data] + ) if self._hasna: new_data[self._isnan] = NaT @@ -1456,8 +1458,9 @@ def __add__(self, other): # as is_integer returns True for these if not is_period_dtype(self.dtype): raise integer_op_not_supported(self) - result = cast("PeriodArray", self)._addsub_int_array_or_scalar( - other * self.freq.n, operator.add + self_periodarray = cast("PeriodArray", self) + result = self_periodarray._addsub_int_array_or_scalar( + other * self_periodarray.freq.n, operator.add ) # array-like others @@ -1473,8 +1476,9 @@ def __add__(self, other): elif is_integer_dtype(other_dtype): if not is_period_dtype(self.dtype): raise integer_op_not_supported(self) + # error: Item "None" of "Optional[BaseOffset]" has no attribute "n" result = cast("PeriodArray", self)._addsub_int_array_or_scalar( - other * self.freq.n, operator.add + other * self.freq.n, operator.add # type: ignore[union-attr] ) else: # Includes Categorical, other ExtensionArrays @@ -1514,8 +1518,9 @@ def __sub__(self, other): # as is_integer returns True for these if not is_period_dtype(self.dtype): raise integer_op_not_supported(self) + # error: Item "None" of "Optional[BaseOffset]" has no attribute "n" result = cast("PeriodArray", self)._addsub_int_array_or_scalar( - other * self.freq.n, operator.sub + other * self.freq.n, operator.sub # type: ignore[union-attr] ) elif isinstance(other, Period): @@ -1537,8 +1542,9 @@ def __sub__(self, other): elif is_integer_dtype(other_dtype): if not is_period_dtype(self.dtype): raise integer_op_not_supported(self) + # error: Item "None" of "Optional[BaseOffset]" has no attribute "n" result = cast("PeriodArray", self)._addsub_int_array_or_scalar( - other * self.freq.n, operator.sub + other * self.freq.n, operator.sub # type: ignore[union-attr] ) else: # Includes ExtensionArrays, float_dtype diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index 193ab6dc99350..a76491e04b93c 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -36,6 +36,7 @@ npt, ) from pandas.compat.numpy import function as nv +from pandas.util._decorators import doc from pandas.util._validators import validate_endpoints from pandas.core.dtypes.astype import astype_td64_unit_conversion @@ -143,6 +144,23 @@ def _box_func(self, x: np.timedelta64) -> Timedelta | NaTType: return NaT return Timedelta._from_value_and_reso(y, reso=self._reso) + # error: Decorated property not supported + @property # type: ignore[misc] + @doc(dtl.DatetimeLikeArrayMixin.freq) + def freq(self) -> Tick | None: + # error: Incompatible return value type (got "Optional[BaseOffset]", expected + # "Optional[Tick]") + return self._freq # type: ignore[return-value] + + @freq.setter + def freq(self, value) -> None: + # python doesn't support super().freq = value (any mypy has some + # issue with the workaround) + # error: overloaded function has no attribute "fset" + super(TimedeltaArray, TimedeltaArray).freq.fset( # type: ignore[attr-defined] + self, value + ) + @property # error: Return type "dtype" of "dtype" incompatible with return type # "ExtensionDtype" in supertype "ExtensionArray" diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 6867ef936d45e..ab5fc8f06f5ab 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -3,6 +3,10 @@ """ from __future__ import annotations +from abc import ( + ABC, + abstractmethod, +) from datetime import datetime import inspect from typing import ( @@ -30,6 +34,7 @@ parsing, to_offset, ) +from pandas._typing import npt from pandas.compat.numpy import function as nv from pandas.util._decorators import ( Appender, @@ -59,10 +64,7 @@ Index, _index_shared_docs, ) -from pandas.core.indexes.extension import ( - NDArrayBackedExtensionIndex, - inherit_names, -) +from pandas.core.indexes.extension import NDArrayBackedExtensionIndex from pandas.core.indexes.range import RangeIndex from pandas.core.tools.timedeltas import to_timedelta @@ -75,13 +77,7 @@ _TDT = TypeVar("_TDT", bound="DatetimeTimedeltaMixin") -@inherit_names( - ["inferred_freq", "_resolution_obj", "resolution"], - DatetimeLikeArrayMixin, - cache=True, -) -@inherit_names(["mean", "asi8", "freq", "freqstr"], DatetimeLikeArrayMixin) -class DatetimeIndexOpsMixin(NDArrayBackedExtensionIndex): +class DatetimeIndexOpsMixin(NDArrayBackedExtensionIndex, ABC): """ Common ops mixin to support a unified interface datetimelike Index. """ @@ -89,9 +85,41 @@ class DatetimeIndexOpsMixin(NDArrayBackedExtensionIndex): _is_numeric_dtype = False _can_hold_strings = False _data: DatetimeArray | TimedeltaArray | PeriodArray - freq: BaseOffset | None - freqstr: str | None - _resolution_obj: Resolution + + # ------------------------------------------------------------------------ + + @doc(DatetimeLikeArrayMixin.mean) + def mean(self, *, skipna: bool = True, axis: int | None = 0): + return self._data.mean(skipna=skipna, axis=axis) + + # error: Decorated property not supported + @property # type: ignore[misc] + @doc(DatetimeLikeArrayMixin.asi8) + def asi8(self) -> npt.NDArray[np.int64]: + return self._data.asi8 + + # error: Decorated property not supported + @property # type: ignore[misc] + @doc(DatetimeLikeArrayMixin.freq) + def freq(self) -> BaseOffset | None: + return self._data.freq + + # error: Decorated property not supported + @property # type: ignore[misc] + @doc(DatetimeLikeArrayMixin.freqstr) + def freqstr(self) -> str | None: + return self._data.freqstr + + @cache_readonly + @abstractmethod + def _resolution_obj(self) -> Resolution: + ... + + # error: Decorated property not supported + @cache_readonly # type: ignore[misc] + @doc(DatetimeLikeArrayMixin.resolution) + def resolution(self) -> str: + return self._data.resolution # ------------------------------------------------------------------------ @@ -373,7 +401,7 @@ def _maybe_cast_listlike_indexer(self, keyarr): return Index(res, dtype=res.dtype) -class DatetimeTimedeltaMixin(DatetimeIndexOpsMixin): +class DatetimeTimedeltaMixin(DatetimeIndexOpsMixin, ABC): """ Mixin class for methods shared by DatetimeIndex and TimedeltaIndex, but not PeriodIndex @@ -408,6 +436,23 @@ def values(self) -> np.ndarray: # NB: For Datetime64TZ this is lossy return self._data._ndarray + # error: Decorated property not supported + @property # type: ignore[misc] + @doc(DatetimeLikeArrayMixin.freq) + def freq(self) -> BaseOffset | None: + # needed to define the setter (same as in DatetimeIndexOpsMixin) + return self._data.freq + + @freq.setter + def freq(self, value) -> None: + self._data.freq = value + + # error: Decorated property not supported + @cache_readonly # type: ignore[misc] + @doc(DatetimeLikeArrayMixin.inferred_freq) + def inferred_freq(self) -> str | None: + return self._data.inferred_freq + # -------------------------------------------------------------------- # Set Operation Methods diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 80138c25b0c27..d6c5d81a8fae1 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -121,7 +121,7 @@ def _new_DatetimeIndex(cls, d): DatetimeArray, wrap=True, ) -@inherit_names(["is_normalized", "_resolution_obj"], DatetimeArray, cache=True) +@inherit_names(["is_normalized"], DatetimeArray, cache=True) @inherit_names( [ "tz", @@ -261,7 +261,6 @@ def _engine_type(self) -> type[libindex.DatetimeEngine]: return libindex.DatetimeEngine _data: DatetimeArray - inferred_freq: str | None tz: tzinfo | None # -------------------------------------------------------------------- @@ -308,6 +307,10 @@ def isocalendar(self) -> DataFrame: df = self._data.isocalendar() return df.set_index(self) + @cache_readonly + def _resolution_obj(self) -> Resolution: + return self._data._resolution_obj + # -------------------------------------------------------------------- # Constructors diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index fedcba7aa9644..e2741aa185051 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -167,8 +167,7 @@ def _engine_type(self) -> type[libindex.PeriodEngine]: return libindex.PeriodEngine @cache_readonly - # Signature of "_resolution_obj" incompatible with supertype "DatetimeIndexOpsMixin" - def _resolution_obj(self) -> Resolution: # type: ignore[override] + def _resolution_obj(self) -> Resolution: # for compat with DatetimeIndex return self.dtype._resolution_obj @@ -393,7 +392,7 @@ def is_full(self) -> bool: if not self.is_monotonic_increasing: raise ValueError("Index is not monotonic") values = self.asi8 - return ((values[1:] - values[:-1]) < 2).all() + return ((values[1:] - values[:-1]) < 2).all().item() @property def inferred_type(self) -> str: diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 12a8f2c0d5a9d..c7bcdac9c6739 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -112,6 +112,13 @@ def _engine_type(self) -> type[libindex.TimedeltaEngine]: # Use base class method instead of DatetimeTimedeltaMixin._get_string_slice _get_string_slice = Index._get_string_slice + # error: Return type "None" of "_resolution_obj" incompatible with return type + # "Resolution" in supertype "DatetimeIndexOpsMixin" + @property + def _resolution_obj(self) -> None: # type: ignore[override] + # not used but need to implement it because it is an abstract method + return None + # ------------------------------------------------------------------- # Constructors diff --git a/pandas/tests/indexes/period/test_freq_attr.py b/pandas/tests/indexes/period/test_freq_attr.py index e1ecffa4982bd..71115929121bc 100644 --- a/pandas/tests/indexes/period/test_freq_attr.py +++ b/pandas/tests/indexes/period/test_freq_attr.py @@ -20,7 +20,7 @@ def test_freq_setter_deprecated(self): # warning for setter msg = ( - "property 'freq' of 'PeriodArray' object has no setter" + "property 'freq' of 'PeriodIndex' object has no setter" if PY311 else "can't set attribute" )