From dd987967df49c7a8d96aa9bc0201c087f59427fb Mon Sep 17 00:00:00 2001 From: Brock Date: Mon, 16 May 2022 20:12:45 -0700 Subject: [PATCH] REF: share parts of DTI and PI --- pandas/core/indexes/datetimelike.py | 6 ++- pandas/core/indexes/datetimes.py | 46 ++++++++---------- pandas/core/indexes/period.py | 73 +++++++++++++++++------------ 3 files changed, 68 insertions(+), 57 deletions(-) diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 25b7a5c3d3689..811dc72e9b908 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -210,8 +210,12 @@ def _summary(self, name=None) -> str: # -------------------------------------------------------------------- # Indexing Methods + @final def _can_partial_date_slice(self, reso: Resolution) -> bool: - raise NotImplementedError + # e.g. test_getitem_setitem_periodindex + # History of conversation GH#3452, GH#3931, GH#2369, GH#14826 + return reso > self._resolution_obj + # NB: for DTI/PI, not TDI def _parsed_string_to_bounds(self, reso: Resolution, parsed): raise NotImplementedError diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 3954cb28c2aca..5274f68eb3171 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -593,10 +593,6 @@ def _parsed_string_to_bounds(self, reso: Resolution, parsed: datetime): end = self._maybe_cast_for_get_loc(end) return start, end - def _can_partial_date_slice(self, reso: Resolution) -> bool: - # History of conversation GH#3452, GH#3931, GH#2369, GH#14826 - return reso > self._resolution_obj - def _deprecate_mismatched_indexing(self, key) -> None: # GH#36148 # we get here with isinstance(key, self._data._recognized_scalars) @@ -651,12 +647,8 @@ def get_loc(self, key, method=None, tolerance=None): except KeyError as err: if method is None: raise KeyError(key) from err - try: - key = self._maybe_cast_for_get_loc(key) - except ValueError as err: - # FIXME(dateutil#1180): we get here because parse_with_reso - # doesn't raise on "t2m" - raise KeyError(key) from err + + key = self._maybe_cast_for_get_loc(key) elif isinstance(key, timedelta): # GH#20464 @@ -682,7 +674,16 @@ def get_loc(self, key, method=None, tolerance=None): def _maybe_cast_for_get_loc(self, key) -> Timestamp: # needed to localize naive datetimes or dates (GH 35690) - key = Timestamp(key) + try: + key = Timestamp(key) + except ValueError as err: + # FIXME(dateutil#1180): we get here because parse_with_reso + # doesn't raise on "t2m" + if not isinstance(key, str): + # Not expected to be reached, but check to be sure + raise # pragma: no cover + raise KeyError(key) from err + if key.tzinfo is None: key = key.tz_localize(self.tz) else: @@ -691,6 +692,13 @@ def _maybe_cast_for_get_loc(self, key) -> Timestamp: @doc(DatetimeTimedeltaMixin._maybe_cast_slice_bound) def _maybe_cast_slice_bound(self, label, side: str, kind=lib.no_default): + + # GH#42855 handle date here instead of get_slice_bound + if isinstance(label, date) and not isinstance(label, datetime): + # Pandas supports slicing with dates, treated as datetimes at midnight. + # https://github.com/pandas-dev/pandas/issues/31501 + label = Timestamp(label).to_pydatetime() + label = super()._maybe_cast_slice_bound(label, side, kind=kind) self._deprecate_mismatched_indexing(label) return self._maybe_cast_for_get_loc(label) @@ -722,13 +730,6 @@ def slice_indexer(self, start=None, end=None, step=None, kind=lib.no_default): if isinstance(start, time) or isinstance(end, time): raise KeyError("Cannot mix time and non-time slice keys") - # Pandas supports slicing with dates, treated as datetimes at midnight. - # https://github.com/pandas-dev/pandas/issues/31501 - if isinstance(start, date) and not isinstance(start, datetime): - start = datetime.combine(start, time(0, 0)) - if isinstance(end, date) and not isinstance(end, datetime): - end = datetime.combine(end, time(0, 0)) - def check_str_or_none(point): return point is not None and not isinstance(point, str) @@ -768,15 +769,6 @@ def check_str_or_none(point): else: return indexer - @doc(Index.get_slice_bound) - def get_slice_bound( - self, label, side: Literal["left", "right"], kind=lib.no_default - ) -> int: - # GH#42855 handle date here instead of _maybe_cast_slice_bound - if isinstance(label, date) and not isinstance(label, datetime): - label = Timestamp(label).to_pydatetime() - return super().get_slice_bound(label, side=side, kind=kind) - # -------------------------------------------------------------------- @property diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 592e6e9fb703d..e3ab5e8624585 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -25,7 +25,10 @@ DtypeObj, npt, ) -from pandas.util._decorators import doc +from pandas.util._decorators import ( + cache_readonly, + doc, +) from pandas.util._exceptions import find_stack_level from pandas.core.dtypes.common import ( @@ -159,6 +162,12 @@ class PeriodIndex(DatetimeIndexOpsMixin): _engine_type = libindex.PeriodEngine _supports_partial_string_indexing = True + @cache_readonly + # Signature of "_resolution_obj" incompatible with supertype "DatetimeIndexOpsMixin" + def _resolution_obj(self) -> Resolution: # type: ignore[override] + # for compat with DatetimeIndex + return self.dtype._resolution_obj + # -------------------------------------------------------------------- # methods that dispatch to array and wrap result in Index # These are defined here instead of via inherit_names for mypy @@ -446,10 +455,10 @@ def get_loc(self, key, method=None, tolerance=None): # TODO: pass if method is not None, like DTI does? raise KeyError(key) from err - if reso == self.dtype._resolution_obj: - # the reso < self.dtype._resolution_obj case goes + if reso == self._resolution_obj: + # the reso < self._resolution_obj case goes # through _get_string_slice - key = Period(parsed, freq=self.freq) + key = self._cast_partial_indexing_scalar(key) loc = self.get_loc(key, method=method, tolerance=tolerance) # Recursing instead of falling through matters for the exception # message in test_get_loc3 (though not clear if that really matters) @@ -457,28 +466,14 @@ def get_loc(self, key, method=None, tolerance=None): elif method is None: raise KeyError(key) else: - key = Period(parsed, freq=self.freq) + key = self._cast_partial_indexing_scalar(parsed) elif isinstance(key, Period): - sfreq = self.freq - kfreq = key.freq - if not ( - sfreq.n == kfreq.n - # error: "BaseOffset" has no attribute "_period_dtype_code" - and sfreq._period_dtype_code # type: ignore[attr-defined] - # error: "BaseOffset" has no attribute "_period_dtype_code" - == kfreq._period_dtype_code # type: ignore[attr-defined] - ): - # GH#42247 For the subset of DateOffsets that can be Period freqs, - # checking these two attributes is sufficient to check equality, - # and much more performant than `self.freq == key.freq` - raise KeyError(key) + key = self._maybe_cast_for_get_loc(key) + elif isinstance(key, datetime): - try: - key = Period(key, freq=self.freq) - except ValueError as err: - # we cannot construct the Period - raise KeyError(orig_key) from err + key = self._cast_partial_indexing_scalar(key) + else: # in particular integer, which Period constructor would cast to string raise KeyError(key) @@ -488,10 +483,35 @@ def get_loc(self, key, method=None, tolerance=None): except KeyError as err: raise KeyError(orig_key) from err + def _maybe_cast_for_get_loc(self, key: Period) -> Period: + # name is a misnomer, chosen for compat with DatetimeIndex + sfreq = self.freq + kfreq = key.freq + if not ( + sfreq.n == kfreq.n + # error: "BaseOffset" has no attribute "_period_dtype_code" + and sfreq._period_dtype_code # type: ignore[attr-defined] + # error: "BaseOffset" has no attribute "_period_dtype_code" + == kfreq._period_dtype_code # type: ignore[attr-defined] + ): + # GH#42247 For the subset of DateOffsets that can be Period freqs, + # checking these two attributes is sufficient to check equality, + # and much more performant than `self.freq == key.freq` + raise KeyError(key) + return key + + def _cast_partial_indexing_scalar(self, label): + try: + key = Period(label, freq=self.freq) + except ValueError as err: + # we cannot construct the Period + raise KeyError(label) from err + return key + @doc(DatetimeIndexOpsMixin._maybe_cast_slice_bound) def _maybe_cast_slice_bound(self, label, side: str, kind=lib.no_default): if isinstance(label, datetime): - label = Period(label, freq=self.freq) + label = self._cast_partial_indexing_scalar(label) return super()._maybe_cast_slice_bound(label, side, kind=kind) @@ -499,11 +519,6 @@ def _parsed_string_to_bounds(self, reso: Resolution, parsed: datetime): iv = Period(parsed, freq=reso.attr_abbrev) return (iv.asfreq(self.freq, how="start"), iv.asfreq(self.freq, how="end")) - def _can_partial_date_slice(self, reso: Resolution) -> bool: - assert isinstance(reso, Resolution), (type(reso), reso) - # e.g. test_getitem_setitem_periodindex - return reso > self.dtype._resolution_obj - def period_range( start=None, end=None, periods: int | None = None, freq=None, name=None