Skip to content

REF: move _validate_frequency to more accurate subclass #50532

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

Merged
merged 3 commits into from
Jan 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 65 additions & 66 deletions pandas/core/arrays/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,17 +190,17 @@ 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
_infer_matches: tuple[str, ...]
_is_recognized_dtype: Callable[[DtypeObj], bool]
_recognized_scalars: tuple[type, ...]
_ndarray: np.ndarray
freq: BaseOffset | None

@cache_readonly
def _can_hold_na(self) -> bool:
Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pandas/core/arrays/period.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 10 additions & 2 deletions pandas/core/indexes/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,26 @@
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.
"""

_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
Expand Down