From 897e5bfc1829925544bf34c33b4ec6e0073d95c2 Mon Sep 17 00:00:00 2001 From: Brock Date: Fri, 4 Jun 2021 17:44:19 -0700 Subject: [PATCH 1/2] BUG: wrong exception when slicing on TimedeltaIndex --- pandas/core/indexes/datetimelike.py | 6 ++- pandas/core/indexes/period.py | 10 ++-- pandas/core/indexes/timedeltas.py | 16 +++--- .../tests/indexes/timedeltas/test_indexing.py | 49 +++++++++++++++++++ 4 files changed, 69 insertions(+), 12 deletions(-) diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 19167677257f7..945e5905972ec 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -26,7 +26,10 @@ Resolution, Tick, ) -from pandas._typing import Callable +from pandas._typing import ( + Callable, + final, +) from pandas.compat.numpy import function as nv from pandas.util._decorators import ( Appender, @@ -400,6 +403,7 @@ def _validate_partial_date_slice(self, reso: Resolution): def _parsed_string_to_bounds(self, reso: Resolution, parsed: datetime): raise NotImplementedError + @final def _partial_date_slice( self, reso: Resolution, diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index c1104b80a0a7a..ae8126846d03a 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -32,7 +32,6 @@ from pandas.core.dtypes.common import ( is_datetime64_any_dtype, - is_float, is_integer, is_scalar, pandas_dtype, @@ -493,13 +492,14 @@ def _maybe_cast_slice_bound(self, label, side: str, kind=lib.no_default): elif isinstance(label, str): try: parsed, reso_str = parse_time_string(label, self.freq) - reso = Resolution.from_attrname(reso_str) - bounds = self._parsed_string_to_bounds(reso, parsed) - return bounds[0 if side == "left" else 1] except ValueError as err: # string cannot be parsed as datetime-like raise self._invalid_indexer("slice", label) from err - elif is_integer(label) or is_float(label): + + reso = Resolution.from_attrname(reso_str) + lower, upper = self._parsed_string_to_bounds(reso, parsed) + return lower if side == "left" else upper + elif not isinstance(label, self._data._recognized_scalars): raise self._invalid_indexer("slice", label) return label diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 85cbea39b9b98..950aea5a22b5c 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -203,12 +203,16 @@ def _maybe_cast_slice_bound(self, label, side: str, kind=lib.no_default): self._deprecated_arg(kind, "kind", "_maybe_cast_slice_bound") if isinstance(label, str): - parsed = Timedelta(label) - lbound = parsed.round(parsed.resolution_string) - if side == "left": - return lbound - else: - return lbound + to_offset(parsed.resolution_string) - Timedelta(1, "ns") + try: + parsed = Timedelta(label) + except ValueError as err: + # e.g. 'unit abbreviation w/o a number' + raise self._invalid_indexer("slice", label) from err + + # The next two lines are analogous to DTI/PI._parsed_str_to_bounds + lower = parsed.round(parsed.resolution_string) + upper = lower + to_offset(parsed.resolution_string) - Timedelta(1, "ns") + return lower if side == "left" else upper elif not isinstance(label, self._data._recognized_scalars): raise self._invalid_indexer("slice", label) diff --git a/pandas/tests/indexes/timedeltas/test_indexing.py b/pandas/tests/indexes/timedeltas/test_indexing.py index 5f0101eb4478c..ec41956371164 100644 --- a/pandas/tests/indexes/timedeltas/test_indexing.py +++ b/pandas/tests/indexes/timedeltas/test_indexing.py @@ -291,3 +291,52 @@ def test_take_fill_value(self): msg = "index -5 is out of bounds for (axis 0 with )?size 3" with pytest.raises(IndexError, match=msg): idx.take(np.array([1, -5])) + + +class TestMaybeCastSliceBound: + @pytest.fixture(params=["increasing", "decreasing", None]) + def monotonic(self, request): + return request.param + + @pytest.fixture + def tdi(self, monotonic): + tdi = timedelta_range("1 Day", periods=10) + if monotonic == "decreasing": + tdi = tdi[::-1] + elif monotonic is None: + taker = np.arange(10, dtype=np.intp) + np.random.shuffle(taker) + tdi = tdi.take(taker) + return tdi + + def test_maybe_cast_slice_bound_invalid_str(self, tdi): + # test the low-level _maybe_cast_slice_bound and that we get the + # expected exception+message all the way up the stack + msg = ( + "cannot do slice indexing on TimedeltaIndex with these " + r"indexers \[foo\] of type str" + ) + with pytest.raises(TypeError, match=msg): + tdi._maybe_cast_slice_bound("foo", side="left") + with pytest.raises(TypeError, match=msg): + tdi.get_slice_bound("foo", side="left") + with pytest.raises(TypeError, match=msg): + tdi.slice_locs("foo", None, None) + + def test_slice_invalid_str_with_timedeltaindex( + self, tdi, frame_or_series, indexer_sl + ): + obj = frame_or_series(range(10), index=tdi) + + msg = ( + "cannot do slice indexing on TimedeltaIndex with these " + r"indexers \[foo\] of type str" + ) + with pytest.raises(TypeError, match=msg): + indexer_sl(obj)["foo":] + with pytest.raises(TypeError, match=msg): + indexer_sl(obj)["foo":-1] + with pytest.raises(TypeError, match=msg): + indexer_sl(obj)[:"foo"] + with pytest.raises(TypeError, match=msg): + indexer_sl(obj)[tdi[0] : "foo"] From dad75119cb92603908b21cc26576afd816ecfbaf Mon Sep 17 00:00:00 2001 From: Brock Date: Mon, 7 Jun 2021 12:42:06 -0700 Subject: [PATCH 2/2] whatsnew --- doc/source/whatsnew/v1.3.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 8b413808503ad..a8455d4e97617 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -953,6 +953,7 @@ Indexing - Bug in :meth:`DataFrame.__setitem__` raising a ``TypeError`` when using a ``str`` subclass as the column name with a :class:`DatetimeIndex` (:issue:`37366`) - Bug in :meth:`PeriodIndex.get_loc` failing to raise a ``KeyError`` when given a :class:`Period` with a mismatched ``freq`` (:issue:`41670`) - 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`) +- 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`) Missing ^^^^^^^