Skip to content

TYP: avoid inherit_names for DatetimeIndexOpsMixin #48015

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
3 changes: 3 additions & 0 deletions pandas/_libs/tslibs/offsets.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: ...
Expand Down
20 changes: 13 additions & 7 deletions pandas/core/arrays/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions pandas/core/arrays/timedeltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
75 changes: 60 additions & 15 deletions pandas/core/indexes/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
"""
from __future__ import annotations

from abc import (
ABC,
abstractmethod,
)
from datetime import datetime
import inspect
from typing import (
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand All @@ -75,23 +77,49 @@
_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.
"""

_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

# ------------------------------------------------------------------------

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
7 changes: 5 additions & 2 deletions pandas/core/indexes/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -261,7 +261,6 @@ def _engine_type(self) -> type[libindex.DatetimeEngine]:
return libindex.DatetimeEngine

_data: DatetimeArray
inferred_freq: str | None
tz: tzinfo | None

# --------------------------------------------------------------------
Expand Down Expand Up @@ -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

Expand Down
5 changes: 2 additions & 3 deletions pandas/core/indexes/period.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this needed? seems like the kind of thing that might be fragile across numpy versions

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function is annotated as returning bool, but it currently return numpy.bool_ (which does not seem to inherit from bool):

>>> type((np.random.rand(3) > 0.5).all())
<class 'numpy.bool_'>
>>> isinstance((np.random.rand(3) > 0.5).all(), bool)
False


@property
def inferred_type(self) -> str:
Expand Down
7 changes: 7 additions & 0 deletions pandas/core/indexes/timedeltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/indexes/period/test_freq_attr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down