Skip to content

Commit 9a00692

Browse files
authored
REF: share parts of DTI and PI (#47038)
1 parent a335bf5 commit 9a00692

File tree

3 files changed

+68
-57
lines changed

3 files changed

+68
-57
lines changed

pandas/core/indexes/datetimelike.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,12 @@ def _summary(self, name=None) -> str:
210210
# --------------------------------------------------------------------
211211
# Indexing Methods
212212

213+
@final
213214
def _can_partial_date_slice(self, reso: Resolution) -> bool:
214-
raise NotImplementedError
215+
# e.g. test_getitem_setitem_periodindex
216+
# History of conversation GH#3452, GH#3931, GH#2369, GH#14826
217+
return reso > self._resolution_obj
218+
# NB: for DTI/PI, not TDI
215219

216220
def _parsed_string_to_bounds(self, reso: Resolution, parsed):
217221
raise NotImplementedError

pandas/core/indexes/datetimes.py

+19-27
Original file line numberDiff line numberDiff line change
@@ -593,10 +593,6 @@ def _parsed_string_to_bounds(self, reso: Resolution, parsed: datetime):
593593
end = self._maybe_cast_for_get_loc(end)
594594
return start, end
595595

596-
def _can_partial_date_slice(self, reso: Resolution) -> bool:
597-
# History of conversation GH#3452, GH#3931, GH#2369, GH#14826
598-
return reso > self._resolution_obj
599-
600596
def _deprecate_mismatched_indexing(self, key) -> None:
601597
# GH#36148
602598
# we get here with isinstance(key, self._data._recognized_scalars)
@@ -651,12 +647,8 @@ def get_loc(self, key, method=None, tolerance=None):
651647
except KeyError as err:
652648
if method is None:
653649
raise KeyError(key) from err
654-
try:
655-
key = self._maybe_cast_for_get_loc(key)
656-
except ValueError as err:
657-
# FIXME(dateutil#1180): we get here because parse_with_reso
658-
# doesn't raise on "t2m"
659-
raise KeyError(key) from err
650+
651+
key = self._maybe_cast_for_get_loc(key)
660652

661653
elif isinstance(key, timedelta):
662654
# GH#20464
@@ -682,7 +674,16 @@ def get_loc(self, key, method=None, tolerance=None):
682674

683675
def _maybe_cast_for_get_loc(self, key) -> Timestamp:
684676
# needed to localize naive datetimes or dates (GH 35690)
685-
key = Timestamp(key)
677+
try:
678+
key = Timestamp(key)
679+
except ValueError as err:
680+
# FIXME(dateutil#1180): we get here because parse_with_reso
681+
# doesn't raise on "t2m"
682+
if not isinstance(key, str):
683+
# Not expected to be reached, but check to be sure
684+
raise # pragma: no cover
685+
raise KeyError(key) from err
686+
686687
if key.tzinfo is None:
687688
key = key.tz_localize(self.tz)
688689
else:
@@ -691,6 +692,13 @@ def _maybe_cast_for_get_loc(self, key) -> Timestamp:
691692

692693
@doc(DatetimeTimedeltaMixin._maybe_cast_slice_bound)
693694
def _maybe_cast_slice_bound(self, label, side: str, kind=lib.no_default):
695+
696+
# GH#42855 handle date here instead of get_slice_bound
697+
if isinstance(label, date) and not isinstance(label, datetime):
698+
# Pandas supports slicing with dates, treated as datetimes at midnight.
699+
# https://github.com/pandas-dev/pandas/issues/31501
700+
label = Timestamp(label).to_pydatetime()
701+
694702
label = super()._maybe_cast_slice_bound(label, side, kind=kind)
695703
self._deprecate_mismatched_indexing(label)
696704
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):
722730
if isinstance(start, time) or isinstance(end, time):
723731
raise KeyError("Cannot mix time and non-time slice keys")
724732

725-
# Pandas supports slicing with dates, treated as datetimes at midnight.
726-
# https://github.com/pandas-dev/pandas/issues/31501
727-
if isinstance(start, date) and not isinstance(start, datetime):
728-
start = datetime.combine(start, time(0, 0))
729-
if isinstance(end, date) and not isinstance(end, datetime):
730-
end = datetime.combine(end, time(0, 0))
731-
732733
def check_str_or_none(point):
733734
return point is not None and not isinstance(point, str)
734735

@@ -768,15 +769,6 @@ def check_str_or_none(point):
768769
else:
769770
return indexer
770771

771-
@doc(Index.get_slice_bound)
772-
def get_slice_bound(
773-
self, label, side: Literal["left", "right"], kind=lib.no_default
774-
) -> int:
775-
# GH#42855 handle date here instead of _maybe_cast_slice_bound
776-
if isinstance(label, date) and not isinstance(label, datetime):
777-
label = Timestamp(label).to_pydatetime()
778-
return super().get_slice_bound(label, side=side, kind=kind)
779-
780772
# --------------------------------------------------------------------
781773

782774
@property

pandas/core/indexes/period.py

+44-29
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525
DtypeObj,
2626
npt,
2727
)
28-
from pandas.util._decorators import doc
28+
from pandas.util._decorators import (
29+
cache_readonly,
30+
doc,
31+
)
2932
from pandas.util._exceptions import find_stack_level
3033

3134
from pandas.core.dtypes.common import (
@@ -159,6 +162,12 @@ class PeriodIndex(DatetimeIndexOpsMixin):
159162
_engine_type = libindex.PeriodEngine
160163
_supports_partial_string_indexing = True
161164

165+
@cache_readonly
166+
# Signature of "_resolution_obj" incompatible with supertype "DatetimeIndexOpsMixin"
167+
def _resolution_obj(self) -> Resolution: # type: ignore[override]
168+
# for compat with DatetimeIndex
169+
return self.dtype._resolution_obj
170+
162171
# --------------------------------------------------------------------
163172
# methods that dispatch to array and wrap result in Index
164173
# These are defined here instead of via inherit_names for mypy
@@ -446,39 +455,25 @@ def get_loc(self, key, method=None, tolerance=None):
446455
# TODO: pass if method is not None, like DTI does?
447456
raise KeyError(key) from err
448457

449-
if reso == self.dtype._resolution_obj:
450-
# the reso < self.dtype._resolution_obj case goes
458+
if reso == self._resolution_obj:
459+
# the reso < self._resolution_obj case goes
451460
# through _get_string_slice
452-
key = Period(parsed, freq=self.freq)
461+
key = self._cast_partial_indexing_scalar(key)
453462
loc = self.get_loc(key, method=method, tolerance=tolerance)
454463
# Recursing instead of falling through matters for the exception
455464
# message in test_get_loc3 (though not clear if that really matters)
456465
return loc
457466
elif method is None:
458467
raise KeyError(key)
459468
else:
460-
key = Period(parsed, freq=self.freq)
469+
key = self._cast_partial_indexing_scalar(parsed)
461470

462471
elif isinstance(key, Period):
463-
sfreq = self.freq
464-
kfreq = key.freq
465-
if not (
466-
sfreq.n == kfreq.n
467-
# error: "BaseOffset" has no attribute "_period_dtype_code"
468-
and sfreq._period_dtype_code # type: ignore[attr-defined]
469-
# error: "BaseOffset" has no attribute "_period_dtype_code"
470-
== kfreq._period_dtype_code # type: ignore[attr-defined]
471-
):
472-
# GH#42247 For the subset of DateOffsets that can be Period freqs,
473-
# checking these two attributes is sufficient to check equality,
474-
# and much more performant than `self.freq == key.freq`
475-
raise KeyError(key)
472+
key = self._maybe_cast_for_get_loc(key)
473+
476474
elif isinstance(key, datetime):
477-
try:
478-
key = Period(key, freq=self.freq)
479-
except ValueError as err:
480-
# we cannot construct the Period
481-
raise KeyError(orig_key) from err
475+
key = self._cast_partial_indexing_scalar(key)
476+
482477
else:
483478
# in particular integer, which Period constructor would cast to string
484479
raise KeyError(key)
@@ -488,22 +483,42 @@ def get_loc(self, key, method=None, tolerance=None):
488483
except KeyError as err:
489484
raise KeyError(orig_key) from err
490485

486+
def _maybe_cast_for_get_loc(self, key: Period) -> Period:
487+
# name is a misnomer, chosen for compat with DatetimeIndex
488+
sfreq = self.freq
489+
kfreq = key.freq
490+
if not (
491+
sfreq.n == kfreq.n
492+
# error: "BaseOffset" has no attribute "_period_dtype_code"
493+
and sfreq._period_dtype_code # type: ignore[attr-defined]
494+
# error: "BaseOffset" has no attribute "_period_dtype_code"
495+
== kfreq._period_dtype_code # type: ignore[attr-defined]
496+
):
497+
# GH#42247 For the subset of DateOffsets that can be Period freqs,
498+
# checking these two attributes is sufficient to check equality,
499+
# and much more performant than `self.freq == key.freq`
500+
raise KeyError(key)
501+
return key
502+
503+
def _cast_partial_indexing_scalar(self, label):
504+
try:
505+
key = Period(label, freq=self.freq)
506+
except ValueError as err:
507+
# we cannot construct the Period
508+
raise KeyError(label) from err
509+
return key
510+
491511
@doc(DatetimeIndexOpsMixin._maybe_cast_slice_bound)
492512
def _maybe_cast_slice_bound(self, label, side: str, kind=lib.no_default):
493513
if isinstance(label, datetime):
494-
label = Period(label, freq=self.freq)
514+
label = self._cast_partial_indexing_scalar(label)
495515

496516
return super()._maybe_cast_slice_bound(label, side, kind=kind)
497517

498518
def _parsed_string_to_bounds(self, reso: Resolution, parsed: datetime):
499519
iv = Period(parsed, freq=reso.attr_abbrev)
500520
return (iv.asfreq(self.freq, how="start"), iv.asfreq(self.freq, how="end"))
501521

502-
def _can_partial_date_slice(self, reso: Resolution) -> bool:
503-
assert isinstance(reso, Resolution), (type(reso), reso)
504-
# e.g. test_getitem_setitem_periodindex
505-
return reso > self.dtype._resolution_obj
506-
507522

508523
def period_range(
509524
start=None, end=None, periods: int | None = None, freq=None, name=None

0 commit comments

Comments
 (0)