Skip to content

Commit 6c76b49

Browse files
authored
REF: move _validate_frequency to more accurate subclass (#50532)
* REF: move _validate_frequency * fix inheritance * pyright fixup
1 parent 9a5702c commit 6c76b49

File tree

3 files changed

+77
-70
lines changed

3 files changed

+77
-70
lines changed

pandas/core/arrays/datetimelike.py

+65-66
Original file line numberDiff line numberDiff line change
@@ -190,17 +190,17 @@ class DatetimeLikeArrayMixin(OpsMixin, NDArrayBackedExtensionArray):
190190
191191
Assumes that __new__/__init__ defines:
192192
_ndarray
193-
_freq
194193
195-
and that the inheriting class has methods:
196-
_generate_range
194+
and that inheriting subclass implements:
195+
freq
197196
"""
198197

199198
# _infer_matches -> which infer_dtype strings are close enough to our own
200199
_infer_matches: tuple[str, ...]
201200
_is_recognized_dtype: Callable[[DtypeObj], bool]
202201
_recognized_scalars: tuple[type, ...]
203202
_ndarray: np.ndarray
203+
freq: BaseOffset | None
204204

205205
@cache_readonly
206206
def _can_hold_na(self) -> bool:
@@ -896,24 +896,6 @@ def _maybe_mask_results(
896896
# ------------------------------------------------------------------
897897
# Frequency Properties/Methods
898898

899-
@property
900-
def freq(self):
901-
"""
902-
Return the frequency object if it is set, otherwise None.
903-
"""
904-
return self._freq
905-
906-
@freq.setter
907-
def freq(self, value) -> None:
908-
if value is not None:
909-
value = to_offset(value)
910-
self._validate_frequency(self, value)
911-
912-
if self.ndim > 1:
913-
raise ValueError("Cannot set freq with ndim > 1")
914-
915-
self._freq = value
916-
917899
@property
918900
def freqstr(self) -> str | None:
919901
"""
@@ -955,51 +937,6 @@ def resolution(self) -> str:
955937
# error: Item "None" of "Optional[Any]" has no attribute "attrname"
956938
return self._resolution_obj.attrname # type: ignore[union-attr]
957939

958-
@classmethod
959-
def _validate_frequency(cls, index, freq, **kwargs):
960-
"""
961-
Validate that a frequency is compatible with the values of a given
962-
Datetime Array/Index or Timedelta Array/Index
963-
964-
Parameters
965-
----------
966-
index : DatetimeIndex or TimedeltaIndex
967-
The index on which to determine if the given frequency is valid
968-
freq : DateOffset
969-
The frequency to validate
970-
"""
971-
# TODO: this is not applicable to PeriodArray, move to correct Mixin
972-
inferred = index.inferred_freq
973-
if index.size == 0 or inferred == freq.freqstr:
974-
return None
975-
976-
try:
977-
on_freq = cls._generate_range(
978-
start=index[0], end=None, periods=len(index), freq=freq, **kwargs
979-
)
980-
if not np.array_equal(index.asi8, on_freq.asi8):
981-
raise ValueError
982-
except ValueError as e:
983-
if "non-fixed" in str(e):
984-
# non-fixed frequencies are not meaningful for timedelta64;
985-
# we retain that error message
986-
raise e
987-
# GH#11587 the main way this is reached is if the `np.array_equal`
988-
# check above is False. This can also be reached if index[0]
989-
# is `NaT`, in which case the call to `cls._generate_range` will
990-
# raise a ValueError, which we re-raise with a more targeted
991-
# message.
992-
raise ValueError(
993-
f"Inferred frequency {inferred} from passed values "
994-
f"does not conform to passed frequency {freq.freqstr}"
995-
) from e
996-
997-
@classmethod
998-
def _generate_range(
999-
cls: type[DatetimeLikeArrayT], start, end, periods, freq, *args, **kwargs
1000-
) -> DatetimeLikeArrayT:
1001-
raise AbstractMethodError(cls)
1002-
1003940
# monotonicity/uniqueness properties are called via frequencies.infer_freq,
1004941
# see GH#23789
1005942

@@ -1953,6 +1890,68 @@ def __init__(
19531890
def _validate_dtype(cls, values, dtype):
19541891
raise AbstractMethodError(cls)
19551892

1893+
@property
1894+
def freq(self):
1895+
"""
1896+
Return the frequency object if it is set, otherwise None.
1897+
"""
1898+
return self._freq
1899+
1900+
@freq.setter
1901+
def freq(self, value) -> None:
1902+
if value is not None:
1903+
value = to_offset(value)
1904+
self._validate_frequency(self, value)
1905+
1906+
if self.ndim > 1:
1907+
raise ValueError("Cannot set freq with ndim > 1")
1908+
1909+
self._freq = value
1910+
1911+
@classmethod
1912+
def _validate_frequency(cls, index, freq, **kwargs):
1913+
"""
1914+
Validate that a frequency is compatible with the values of a given
1915+
Datetime Array/Index or Timedelta Array/Index
1916+
1917+
Parameters
1918+
----------
1919+
index : DatetimeIndex or TimedeltaIndex
1920+
The index on which to determine if the given frequency is valid
1921+
freq : DateOffset
1922+
The frequency to validate
1923+
"""
1924+
inferred = index.inferred_freq
1925+
if index.size == 0 or inferred == freq.freqstr:
1926+
return None
1927+
1928+
try:
1929+
on_freq = cls._generate_range(
1930+
start=index[0], end=None, periods=len(index), freq=freq, **kwargs
1931+
)
1932+
if not np.array_equal(index.asi8, on_freq.asi8):
1933+
raise ValueError
1934+
except ValueError as err:
1935+
if "non-fixed" in str(err):
1936+
# non-fixed frequencies are not meaningful for timedelta64;
1937+
# we retain that error message
1938+
raise err
1939+
# GH#11587 the main way this is reached is if the `np.array_equal`
1940+
# check above is False. This can also be reached if index[0]
1941+
# is `NaT`, in which case the call to `cls._generate_range` will
1942+
# raise a ValueError, which we re-raise with a more targeted
1943+
# message.
1944+
raise ValueError(
1945+
f"Inferred frequency {inferred} from passed values "
1946+
f"does not conform to passed frequency {freq.freqstr}"
1947+
) from err
1948+
1949+
@classmethod
1950+
def _generate_range(
1951+
cls: type[DatetimeLikeArrayT], start, end, periods, freq, *args, **kwargs
1952+
) -> DatetimeLikeArrayT:
1953+
raise AbstractMethodError(cls)
1954+
19561955
# --------------------------------------------------------------
19571956

19581957
@cache_readonly

pandas/core/arrays/period.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -353,8 +353,8 @@ def _check_compatible_with(self, other) -> None:
353353
def dtype(self) -> PeriodDtype:
354354
return self._dtype
355355

356-
# error: Read-only property cannot override read-write property
357-
@property # type: ignore[misc]
356+
# error: Cannot override writeable attribute with read-only property
357+
@property # type: ignore[override]
358358
def freq(self) -> BaseOffset:
359359
"""
360360
Return the frequency object for this PeriodArray.

pandas/core/indexes/datetimelike.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,26 @@
8282
DatetimeLikeArrayMixin,
8383
cache=True,
8484
)
85-
@inherit_names(["mean", "freq", "freqstr"], DatetimeLikeArrayMixin)
85+
@inherit_names(["mean", "freqstr"], DatetimeLikeArrayMixin)
8686
class DatetimeIndexOpsMixin(NDArrayBackedExtensionIndex):
8787
"""
8888
Common ops mixin to support a unified interface datetimelike Index.
8989
"""
9090

9191
_can_hold_strings = False
9292
_data: DatetimeArray | TimedeltaArray | PeriodArray
93-
freq: BaseOffset | None
9493
freqstr: str | None
9594
_resolution_obj: Resolution
9695

96+
@property
97+
def freq(self) -> BaseOffset | None:
98+
return self._data.freq
99+
100+
@freq.setter
101+
def freq(self, value) -> None:
102+
# error: Property "freq" defined in "PeriodArray" is read-only [misc]
103+
self._data.freq = value # type: ignore[misc]
104+
97105
@property
98106
def asi8(self) -> npt.NDArray[np.int64]:
99107
return self._data.asi8

0 commit comments

Comments
 (0)