diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst index 5be9155b3ff0b..afc0046ec6822 100644 --- a/doc/source/whatsnew/v1.2.0.rst +++ b/doc/source/whatsnew/v1.2.0.rst @@ -289,6 +289,7 @@ Deprecations - Deprecated :meth:`Index.is_all_dates` (:issue:`27744`) - Deprecated automatic alignment on comparison operations between :class:`DataFrame` and :class:`Series`, do ``frame, ser = frame.align(ser, axis=1, copy=False)`` before e.g. ``frame == ser`` (:issue:`28759`) - :meth:`Rolling.count` with ``min_periods=None`` will default to the size of the window in a future version (:issue:`31302`) +- Deprecated slice-indexing on timezone-aware :class:`DatetimeIndex` with naive ``datetime`` objects, to match scalar indexing behavior (:issue:`36148`) - :meth:`Index.ravel` returning a ``np.ndarray`` is deprecated, in the future this will return a view on the same index (:issue:`19956`) .. --------------------------------------------------------------------------- diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 67b71ce63a6e3..62a3688216247 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -604,6 +604,28 @@ def _validate_partial_date_slice(self, reso: Resolution): # _parsed_string_to_bounds allows it. raise KeyError + def _deprecate_mismatched_indexing(self, key): + # GH#36148 + # we get here with isinstance(key, self._data._recognized_scalars) + try: + self._data._assert_tzawareness_compat(key) + except TypeError: + if self.tz is None: + msg = ( + "Indexing a timezone-naive DatetimeIndex with a " + "timezone-aware datetime is deprecated and will " + "raise KeyError in a future version. " + "Use a timezone-naive object instead." + ) + else: + msg = ( + "Indexing a timezone-aware DatetimeIndex with a " + "timezone-naive datetime is deprecated and will " + "raise KeyError in a future version. " + "Use a timezone-aware object instead." + ) + warnings.warn(msg, FutureWarning, stacklevel=5) + def get_loc(self, key, method=None, tolerance=None): """ Get integer location for requested label @@ -621,6 +643,7 @@ def get_loc(self, key, method=None, tolerance=None): if isinstance(key, self._data._recognized_scalars): # needed to localize naive datetimes + self._deprecate_mismatched_indexing(key) key = self._maybe_cast_for_get_loc(key) elif isinstance(key, str): @@ -702,6 +725,8 @@ def _maybe_cast_slice_bound(self, label, side: str, kind): if self._is_strictly_monotonic_decreasing and len(self) > 1: return upper if side == "left" else lower return lower if side == "left" else upper + elif isinstance(label, (self._data._recognized_scalars, date)): + self._deprecate_mismatched_indexing(label) return self._maybe_cast_for_get_loc(label) def _get_string_slice(self, key: str, use_lhs: bool = True, use_rhs: bool = True): diff --git a/pandas/tests/indexes/datetimes/test_indexing.py b/pandas/tests/indexes/datetimes/test_indexing.py index 539d9cb8f06a7..cb12365f38605 100644 --- a/pandas/tests/indexes/datetimes/test_indexing.py +++ b/pandas/tests/indexes/datetimes/test_indexing.py @@ -675,10 +675,14 @@ def test_get_slice_bounds_datetime_within( self, box, kind, side, expected, tz_aware_fixture ): # GH 35690 - index = bdate_range("2000-01-03", "2000-02-11").tz_localize(tz_aware_fixture) - result = index.get_slice_bound( - box(year=2000, month=1, day=7), kind=kind, side=side - ) + tz = tz_aware_fixture + index = bdate_range("2000-01-03", "2000-02-11").tz_localize(tz) + key = box(year=2000, month=1, day=7) + + warn = None if tz is None else FutureWarning + with tm.assert_produces_warning(warn, check_stacklevel=False): + # GH#36148 will require tzawareness-compat + result = index.get_slice_bound(key, kind=kind, side=side) assert result == expected @pytest.mark.parametrize("box", [date, datetime, Timestamp]) @@ -689,19 +693,27 @@ def test_get_slice_bounds_datetime_outside( self, box, kind, side, year, expected, tz_aware_fixture ): # GH 35690 - index = bdate_range("2000-01-03", "2000-02-11").tz_localize(tz_aware_fixture) - result = index.get_slice_bound( - box(year=year, month=1, day=7), kind=kind, side=side - ) + tz = tz_aware_fixture + index = bdate_range("2000-01-03", "2000-02-11").tz_localize(tz) + key = box(year=year, month=1, day=7) + + warn = None if tz is None else FutureWarning + with tm.assert_produces_warning(warn, check_stacklevel=False): + # GH#36148 will require tzawareness-compat + result = index.get_slice_bound(key, kind=kind, side=side) assert result == expected @pytest.mark.parametrize("box", [date, datetime, Timestamp]) @pytest.mark.parametrize("kind", ["getitem", "loc", None]) def test_slice_datetime_locs(self, box, kind, tz_aware_fixture): # GH 34077 - index = DatetimeIndex(["2010-01-01", "2010-01-03"]).tz_localize( - tz_aware_fixture - ) - result = index.slice_locs(box(2010, 1, 1), box(2010, 1, 2)) + tz = tz_aware_fixture + index = DatetimeIndex(["2010-01-01", "2010-01-03"]).tz_localize(tz) + key = box(2010, 1, 1) + + warn = None if tz is None else FutureWarning + with tm.assert_produces_warning(warn, check_stacklevel=False): + # GH#36148 will require tzawareness-compat + result = index.slice_locs(key, box(2010, 1, 2)) expected = (0, 1) assert result == expected diff --git a/pandas/tests/series/indexing/test_datetime.py b/pandas/tests/series/indexing/test_datetime.py index 0389099a195d0..1801d13e75565 100644 --- a/pandas/tests/series/indexing/test_datetime.py +++ b/pandas/tests/series/indexing/test_datetime.py @@ -237,23 +237,38 @@ def test_getitem_setitem_datetimeindex(): expected = ts[4:8] tm.assert_series_equal(result, expected) - # repeat all the above with naive datetimes - result = ts[datetime(1990, 1, 1, 4)] + # But we do not give datetimes a pass on tzawareness compat + # TODO: do the same with Timestamps and dt64 + msg = "Cannot compare tz-naive and tz-aware datetime-like objects" + naive = datetime(1990, 1, 1, 4) + with tm.assert_produces_warning(FutureWarning): + # GH#36148 will require tzawareness compat + result = ts[naive] expected = ts[4] assert result == expected result = ts.copy() - result[datetime(1990, 1, 1, 4)] = 0 - result[datetime(1990, 1, 1, 4)] = ts[4] + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + # GH#36148 will require tzawareness compat + result[datetime(1990, 1, 1, 4)] = 0 + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + # GH#36148 will require tzawareness compat + result[datetime(1990, 1, 1, 4)] = ts[4] tm.assert_series_equal(result, ts) - result = ts[datetime(1990, 1, 1, 4) : datetime(1990, 1, 1, 7)] + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + # GH#36148 will require tzawareness compat + result = ts[datetime(1990, 1, 1, 4) : datetime(1990, 1, 1, 7)] expected = ts[4:8] tm.assert_series_equal(result, expected) result = ts.copy() - result[datetime(1990, 1, 1, 4) : datetime(1990, 1, 1, 7)] = 0 - result[datetime(1990, 1, 1, 4) : datetime(1990, 1, 1, 7)] = ts[4:8] + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + # GH#36148 will require tzawareness compat + result[datetime(1990, 1, 1, 4) : datetime(1990, 1, 1, 7)] = 0 + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + # GH#36148 will require tzawareness compat + result[datetime(1990, 1, 1, 4) : datetime(1990, 1, 1, 7)] = ts[4:8] tm.assert_series_equal(result, ts) lb = datetime(1990, 1, 1, 4) diff --git a/pandas/tests/series/methods/test_truncate.py b/pandas/tests/series/methods/test_truncate.py index 45592f8d99b93..858b1d6b4df8c 100644 --- a/pandas/tests/series/methods/test_truncate.py +++ b/pandas/tests/series/methods/test_truncate.py @@ -101,7 +101,13 @@ def test_truncate_datetimeindex_tz(self): # GH 9243 idx = date_range("4/1/2005", "4/30/2005", freq="D", tz="US/Pacific") s = Series(range(len(idx)), index=idx) - result = s.truncate(datetime(2005, 4, 2), datetime(2005, 4, 4)) + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + # GH#36148 in the future will require tzawareness compat + s.truncate(datetime(2005, 4, 2), datetime(2005, 4, 4)) + + lb = idx[1] + ub = idx[3] + result = s.truncate(lb.to_pydatetime(), ub.to_pydatetime()) expected = Series([1, 2, 3], index=idx[1:4]) tm.assert_series_equal(result, expected)