diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index adf6522f76a1a..a84f168164946 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -190,10 +190,9 @@ class DatetimeLikeArrayMixin(OpsMixin, NDArrayBackedExtensionArray): Assumes that __new__/__init__ defines: _ndarray - _freq - and that the inheriting class has methods: - _generate_range + and that inheriting subclass implements: + freq """ # _infer_matches -> which infer_dtype strings are close enough to our own @@ -201,6 +200,7 @@ class DatetimeLikeArrayMixin(OpsMixin, NDArrayBackedExtensionArray): _is_recognized_dtype: Callable[[DtypeObj], bool] _recognized_scalars: tuple[type, ...] _ndarray: np.ndarray + freq: BaseOffset | None @cache_readonly def _can_hold_na(self) -> bool: @@ -896,24 +896,6 @@ def _maybe_mask_results( # ------------------------------------------------------------------ # Frequency Properties/Methods - @property - def freq(self): - """ - Return the frequency object if it is set, otherwise None. - """ - return self._freq - - @freq.setter - def freq(self, value) -> None: - if value is not None: - value = to_offset(value) - self._validate_frequency(self, value) - - if self.ndim > 1: - raise ValueError("Cannot set freq with ndim > 1") - - self._freq = value - @property def freqstr(self) -> str | None: """ @@ -955,51 +937,6 @@ def resolution(self) -> str: # error: Item "None" of "Optional[Any]" has no attribute "attrname" return self._resolution_obj.attrname # type: ignore[union-attr] - @classmethod - def _validate_frequency(cls, index, freq, **kwargs): - """ - Validate that a frequency is compatible with the values of a given - Datetime Array/Index or Timedelta Array/Index - - Parameters - ---------- - index : DatetimeIndex or TimedeltaIndex - The index on which to determine if the given frequency is valid - freq : DateOffset - The frequency to validate - """ - # TODO: this is not applicable to PeriodArray, move to correct Mixin - inferred = index.inferred_freq - if index.size == 0 or inferred == freq.freqstr: - return None - - try: - on_freq = cls._generate_range( - start=index[0], end=None, periods=len(index), freq=freq, **kwargs - ) - if not np.array_equal(index.asi8, on_freq.asi8): - raise ValueError - except ValueError as e: - if "non-fixed" in str(e): - # non-fixed frequencies are not meaningful for timedelta64; - # we retain that error message - raise e - # GH#11587 the main way this is reached is if the `np.array_equal` - # check above is False. This can also be reached if index[0] - # is `NaT`, in which case the call to `cls._generate_range` will - # raise a ValueError, which we re-raise with a more targeted - # message. - raise ValueError( - f"Inferred frequency {inferred} from passed values " - f"does not conform to passed frequency {freq.freqstr}" - ) from e - - @classmethod - def _generate_range( - cls: type[DatetimeLikeArrayT], start, end, periods, freq, *args, **kwargs - ) -> DatetimeLikeArrayT: - raise AbstractMethodError(cls) - # monotonicity/uniqueness properties are called via frequencies.infer_freq, # see GH#23789 @@ -1953,6 +1890,68 @@ def __init__( def _validate_dtype(cls, values, dtype): raise AbstractMethodError(cls) + @property + def freq(self): + """ + Return the frequency object if it is set, otherwise None. + """ + return self._freq + + @freq.setter + def freq(self, value) -> None: + if value is not None: + value = to_offset(value) + self._validate_frequency(self, value) + + if self.ndim > 1: + raise ValueError("Cannot set freq with ndim > 1") + + self._freq = value + + @classmethod + def _validate_frequency(cls, index, freq, **kwargs): + """ + Validate that a frequency is compatible with the values of a given + Datetime Array/Index or Timedelta Array/Index + + Parameters + ---------- + index : DatetimeIndex or TimedeltaIndex + The index on which to determine if the given frequency is valid + freq : DateOffset + The frequency to validate + """ + inferred = index.inferred_freq + if index.size == 0 or inferred == freq.freqstr: + return None + + try: + on_freq = cls._generate_range( + start=index[0], end=None, periods=len(index), freq=freq, **kwargs + ) + if not np.array_equal(index.asi8, on_freq.asi8): + raise ValueError + except ValueError as err: + if "non-fixed" in str(err): + # non-fixed frequencies are not meaningful for timedelta64; + # we retain that error message + raise err + # GH#11587 the main way this is reached is if the `np.array_equal` + # check above is False. This can also be reached if index[0] + # is `NaT`, in which case the call to `cls._generate_range` will + # raise a ValueError, which we re-raise with a more targeted + # message. + raise ValueError( + f"Inferred frequency {inferred} from passed values " + f"does not conform to passed frequency {freq.freqstr}" + ) from err + + @classmethod + def _generate_range( + cls: type[DatetimeLikeArrayT], start, end, periods, freq, *args, **kwargs + ) -> DatetimeLikeArrayT: + raise AbstractMethodError(cls) + # -------------------------------------------------------------- @cache_readonly diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 859bb53b6489a..e6682b0dea814 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -353,8 +353,8 @@ def _check_compatible_with(self, other) -> None: def dtype(self) -> PeriodDtype: return self._dtype - # error: Read-only property cannot override read-write property - @property # type: ignore[misc] + # error: Cannot override writeable attribute with read-only property + @property # type: ignore[override] def freq(self) -> BaseOffset: """ Return the frequency object for this PeriodArray. diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 1119b6e3b83ad..fde000f84e581 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -82,7 +82,7 @@ DatetimeLikeArrayMixin, cache=True, ) -@inherit_names(["mean", "freq", "freqstr"], DatetimeLikeArrayMixin) +@inherit_names(["mean", "freqstr"], DatetimeLikeArrayMixin) class DatetimeIndexOpsMixin(NDArrayBackedExtensionIndex): """ Common ops mixin to support a unified interface datetimelike Index. @@ -90,10 +90,18 @@ class DatetimeIndexOpsMixin(NDArrayBackedExtensionIndex): _can_hold_strings = False _data: DatetimeArray | TimedeltaArray | PeriodArray - freq: BaseOffset | None freqstr: str | None _resolution_obj: Resolution + @property + def freq(self) -> BaseOffset | None: + return self._data.freq + + @freq.setter + def freq(self, value) -> None: + # error: Property "freq" defined in "PeriodArray" is read-only [misc] + self._data.freq = value # type: ignore[misc] + @property def asi8(self) -> npt.NDArray[np.int64]: return self._data.asi8