From d0c657a86d615295fabe8dae4aff0ee1e606491d Mon Sep 17 00:00:00 2001 From: Brock Date: Wed, 25 Nov 2020 19:24:21 -0800 Subject: [PATCH] BUG: raise consistent exception on slicing failure --- pandas/core/indexes/base.py | 8 +++--- pandas/core/indexes/datetimes.py | 8 ++++-- pandas/core/indexes/period.py | 5 ++-- pandas/core/indexes/timedeltas.py | 2 +- .../indexes/period/test_partial_slicing.py | 28 +++++++++++++++++++ 5 files changed, 41 insertions(+), 10 deletions(-) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index b5900ead246f3..b583bb72558f0 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -3433,11 +3433,11 @@ def _convert_list_indexer(self, keyarr): return None @final - def _invalid_indexer(self, form: str_t, key): + def _invalid_indexer(self, form: str_t, key) -> TypeError: """ Consistent invalid indexer message. """ - raise TypeError( + return TypeError( f"cannot do {form} indexing on {type(self).__name__} with these " f"indexers [{key}] of type {type(key).__name__}" ) @@ -5230,7 +5230,7 @@ def _validate_indexer(self, form: str_t, key, kind: str_t): elif is_integer(key): pass else: - self._invalid_indexer(form, key) + raise self._invalid_indexer(form, key) def _maybe_cast_slice_bound(self, label, side: str_t, kind): """ @@ -5259,7 +5259,7 @@ def _maybe_cast_slice_bound(self, label, side: str_t, kind): # datetimelike Indexes # reject them, if index does not contain label if (is_float(label) or is_integer(label)) and label not in self.values: - self._invalid_indexer("slice", label) + raise self._invalid_indexer("slice", label) return label diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index b39a36d95d27b..f6eeb121b1ac0 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -717,7 +717,11 @@ def _maybe_cast_slice_bound(self, label, side: str, kind): if isinstance(label, str): freq = getattr(self, "freqstr", getattr(self, "inferred_freq", None)) - parsed, reso = parsing.parse_time_string(label, freq) + try: + parsed, reso = parsing.parse_time_string(label, freq) + except parsing.DateParseError as err: + raise self._invalid_indexer("slice", label) from err + reso = Resolution.from_attrname(reso) lower, upper = self._parsed_string_to_bounds(reso, parsed) # lower, upper form the half-open interval: @@ -732,7 +736,7 @@ def _maybe_cast_slice_bound(self, label, side: str, kind): elif isinstance(label, (self._data._recognized_scalars, date)): self._deprecate_mismatched_indexing(label) else: - self._invalid_indexer("slice", label) + raise self._invalid_indexer("slice", label) return self._maybe_cast_for_get_loc(label) diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index e25119162368f..959c79de53f4c 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -579,10 +579,9 @@ def _maybe_cast_slice_bound(self, label, side: str, kind: str): return bounds[0 if side == "left" else 1] except ValueError as err: # string cannot be parsed as datetime-like - # TODO: we need tests for this case - raise KeyError(label) from err + raise self._invalid_indexer("slice", label) from err elif is_integer(label) or is_float(label): - self._invalid_indexer("slice", label) + raise self._invalid_indexer("slice", label) return label diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index f44a1701bfa9b..fcab3e1f6a0a4 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -223,7 +223,7 @@ def _maybe_cast_slice_bound(self, label, side: str, kind): else: return lbound + to_offset(parsed.resolution_string) - Timedelta(1, "ns") elif not isinstance(label, self._data._recognized_scalars): - self._invalid_indexer("slice", label) + raise self._invalid_indexer("slice", label) return label diff --git a/pandas/tests/indexes/period/test_partial_slicing.py b/pandas/tests/indexes/period/test_partial_slicing.py index 660c32d44a7aa..fb5705e2b7673 100644 --- a/pandas/tests/indexes/period/test_partial_slicing.py +++ b/pandas/tests/indexes/period/test_partial_slicing.py @@ -90,6 +90,34 @@ def test_range_slice_outofbounds(self, make_range): tm.assert_frame_equal(df["2013-06":"2013-09"], empty) tm.assert_frame_equal(df["2013-11":"2013-12"], empty) + @pytest.mark.parametrize("make_range", [date_range, period_range]) + def test_maybe_cast_slice_bound(self, make_range, frame_or_series): + idx = make_range(start="2013/10/01", freq="D", periods=10) + + obj = DataFrame(dict(units=[100 + i for i in range(10)]), index=idx) + if frame_or_series is not DataFrame: + obj = obj["units"] + + msg = ( + f"cannot do slice indexing on {type(idx).__name__} with " + r"these indexers \[foo\] of type str" + ) + + # Check the lower-level calls are raising where expected. + with pytest.raises(TypeError, match=msg): + idx._maybe_cast_slice_bound("foo", "left", "loc") + with pytest.raises(TypeError, match=msg): + idx.get_slice_bound("foo", "left", "loc") + + with pytest.raises(TypeError, match=msg): + obj["2013/09/30":"foo"] + with pytest.raises(TypeError, match=msg): + obj["foo":"2013/09/30"] + with pytest.raises(TypeError, match=msg): + obj.loc["2013/09/30":"foo"] + with pytest.raises(TypeError, match=msg): + obj.loc["foo":"2013/09/30"] + def test_partial_slice_doesnt_require_monotonicity(self): # See also: DatetimeIndex test ofm the same name dti = date_range("2014-01-01", periods=30, freq="30D")