Skip to content

Commit f1512af

Browse files
Backport PR pandas-dev#41821: BUG: wrong exception when slicing on TimedeltaIndex (pandas-dev#42079)
Co-authored-by: jbrockmendel <[email protected]>
1 parent 04b8b32 commit f1512af

File tree

5 files changed

+70
-12
lines changed

5 files changed

+70
-12
lines changed

doc/source/whatsnew/v1.3.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,7 @@ Indexing
10251025
- Bug in :meth:`PeriodIndex.get_loc` failing to raise a ``KeyError`` when given a :class:`Period` with a mismatched ``freq`` (:issue:`41670`)
10261026
- Bug ``.loc.__getitem__`` with a :class:`UInt64Index` and negative-integer keys raising ``OverflowError`` instead of ``KeyError`` in some cases, wrapping around to positive integers in others (:issue:`41777`)
10271027
- Bug in :meth:`Index.get_indexer` failing to raise ``ValueError`` in some cases with invalid ``method``, ``limit``, or ``tolerance`` arguments (:issue:`41918`)
1028+
- Bug when slicing a :class:`Series` or :class:`DataFrame` with a :class:`TimedeltaIndex` when passing an invalid string raising ``ValueError`` instead of a ``TypeError`` (:issue:`41821`)
10281029

10291030
Missing
10301031
^^^^^^^

pandas/core/indexes/datetimelike.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
Resolution,
2727
Tick,
2828
)
29-
from pandas._typing import Callable
29+
from pandas._typing import (
30+
Callable,
31+
final,
32+
)
3033
from pandas.compat.numpy import function as nv
3134
from pandas.util._decorators import (
3235
Appender,
@@ -399,6 +402,7 @@ def _validate_partial_date_slice(self, reso: Resolution):
399402
def _parsed_string_to_bounds(self, reso: Resolution, parsed: datetime):
400403
raise NotImplementedError
401404

405+
@final
402406
def _partial_date_slice(
403407
self,
404408
reso: Resolution,

pandas/core/indexes/period.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232

3333
from pandas.core.dtypes.common import (
3434
is_datetime64_any_dtype,
35-
is_float,
3635
is_integer,
3736
is_scalar,
3837
pandas_dtype,
@@ -493,13 +492,14 @@ def _maybe_cast_slice_bound(self, label, side: str, kind=lib.no_default):
493492
elif isinstance(label, str):
494493
try:
495494
parsed, reso_str = parse_time_string(label, self.freq)
496-
reso = Resolution.from_attrname(reso_str)
497-
bounds = self._parsed_string_to_bounds(reso, parsed)
498-
return bounds[0 if side == "left" else 1]
499495
except ValueError as err:
500496
# string cannot be parsed as datetime-like
501497
raise self._invalid_indexer("slice", label) from err
502-
elif is_integer(label) or is_float(label):
498+
499+
reso = Resolution.from_attrname(reso_str)
500+
lower, upper = self._parsed_string_to_bounds(reso, parsed)
501+
return lower if side == "left" else upper
502+
elif not isinstance(label, self._data._recognized_scalars):
503503
raise self._invalid_indexer("slice", label)
504504

505505
return label

pandas/core/indexes/timedeltas.py

+10-6
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,16 @@ def _maybe_cast_slice_bound(self, label, side: str, kind=lib.no_default):
198198
self._deprecated_arg(kind, "kind", "_maybe_cast_slice_bound")
199199

200200
if isinstance(label, str):
201-
parsed = Timedelta(label)
202-
lbound = parsed.round(parsed.resolution_string)
203-
if side == "left":
204-
return lbound
205-
else:
206-
return lbound + to_offset(parsed.resolution_string) - Timedelta(1, "ns")
201+
try:
202+
parsed = Timedelta(label)
203+
except ValueError as err:
204+
# e.g. 'unit abbreviation w/o a number'
205+
raise self._invalid_indexer("slice", label) from err
206+
207+
# The next two lines are analogous to DTI/PI._parsed_str_to_bounds
208+
lower = parsed.round(parsed.resolution_string)
209+
upper = lower + to_offset(parsed.resolution_string) - Timedelta(1, "ns")
210+
return lower if side == "left" else upper
207211
elif not isinstance(label, self._data._recognized_scalars):
208212
raise self._invalid_indexer("slice", label)
209213

pandas/tests/indexes/timedeltas/test_indexing.py

+49
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,52 @@ def test_take_fill_value(self):
291291
msg = "index -5 is out of bounds for (axis 0 with )?size 3"
292292
with pytest.raises(IndexError, match=msg):
293293
idx.take(np.array([1, -5]))
294+
295+
296+
class TestMaybeCastSliceBound:
297+
@pytest.fixture(params=["increasing", "decreasing", None])
298+
def monotonic(self, request):
299+
return request.param
300+
301+
@pytest.fixture
302+
def tdi(self, monotonic):
303+
tdi = timedelta_range("1 Day", periods=10)
304+
if monotonic == "decreasing":
305+
tdi = tdi[::-1]
306+
elif monotonic is None:
307+
taker = np.arange(10, dtype=np.intp)
308+
np.random.shuffle(taker)
309+
tdi = tdi.take(taker)
310+
return tdi
311+
312+
def test_maybe_cast_slice_bound_invalid_str(self, tdi):
313+
# test the low-level _maybe_cast_slice_bound and that we get the
314+
# expected exception+message all the way up the stack
315+
msg = (
316+
"cannot do slice indexing on TimedeltaIndex with these "
317+
r"indexers \[foo\] of type str"
318+
)
319+
with pytest.raises(TypeError, match=msg):
320+
tdi._maybe_cast_slice_bound("foo", side="left")
321+
with pytest.raises(TypeError, match=msg):
322+
tdi.get_slice_bound("foo", side="left")
323+
with pytest.raises(TypeError, match=msg):
324+
tdi.slice_locs("foo", None, None)
325+
326+
def test_slice_invalid_str_with_timedeltaindex(
327+
self, tdi, frame_or_series, indexer_sl
328+
):
329+
obj = frame_or_series(range(10), index=tdi)
330+
331+
msg = (
332+
"cannot do slice indexing on TimedeltaIndex with these "
333+
r"indexers \[foo\] of type str"
334+
)
335+
with pytest.raises(TypeError, match=msg):
336+
indexer_sl(obj)["foo":]
337+
with pytest.raises(TypeError, match=msg):
338+
indexer_sl(obj)["foo":-1]
339+
with pytest.raises(TypeError, match=msg):
340+
indexer_sl(obj)[:"foo"]
341+
with pytest.raises(TypeError, match=msg):
342+
indexer_sl(obj)[tdi[0] : "foo"]

0 commit comments

Comments
 (0)