diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 8445a28a51a5d..7d7dc7f0f17b5 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -525,6 +525,7 @@ Datetimelike API Changes - :class:`DateOffset` objects are now immutable. Attempting to alter one of these will now raise ``AttributeError`` (:issue:`21341`) - :class:`PeriodIndex` subtraction of another ``PeriodIndex`` will now return an object-dtype :class:`Index` of :class:`DateOffset` objects instead of raising a ``TypeError`` (:issue:`20049`) - :func:`cut` and :func:`qcut` now returns a :class:`DatetimeIndex` or :class:`TimedeltaIndex` bins when the input is datetime or timedelta dtype respectively and ``retbins=True`` (:issue:`19891`) +- :meth:`DatetimeIndex.to_period` and :meth:`Timestamp.to_period` will issue a warning when timezone information will be lost (:issue:`21333`) .. _whatsnew_0240.api.other: @@ -626,6 +627,8 @@ Datetimelike - Bug in :class:`DataFrame` with mixed dtypes including ``datetime64[ns]`` incorrectly raising ``TypeError`` on equality comparisons (:issue:`13128`,:issue:`22163`) - Bug in :meth:`DataFrame.eq` comparison against ``NaT`` incorrectly returning ``True`` or ``NaN`` (:issue:`15697`,:issue:`22163`) - Bug in :class:`DatetimeIndex` subtraction that incorrectly failed to raise `OverflowError` (:issue:`22492`, :issue:`22508`) +- Bug in :class:`DatetimeIndex` incorrectly allowing indexing with ``Timedelta`` object (:issue:`20464`) +- Timedelta ^^^^^^^^^ @@ -634,7 +637,7 @@ Timedelta - Bug in multiplying a :class:`Series` with numeric dtype against a ``timedelta`` object (:issue:`22390`) - Bug in :class:`Series` with numeric dtype when adding or subtracting an an array or ``Series`` with ``timedelta64`` dtype (:issue:`22390`) - Bug in :class:`Index` with numeric dtype when multiplying or dividing an array with dtype ``timedelta64`` (:issue:`22390`) -- +- Bug in :class:`TimedeltaIndex` incorrectly allowing indexing with ``Timestamp`` object (:issue:`20464`) - - diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 3ab1396c0fe38..52343593d1cc1 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -737,6 +737,12 @@ class Timestamp(_Timestamp): """ from pandas import Period + if self.tz is not None: + # GH#21333 + warnings.warn("Converting to Period representation will " + "drop timezone information.", + UserWarning) + if freq is None: freq = self.freq diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 629660c899a3f..f780b68a536a1 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -2,7 +2,7 @@ from __future__ import division import operator import warnings -from datetime import time, datetime +from datetime import time, datetime, timedelta import numpy as np from pytz import utc @@ -730,6 +730,10 @@ def to_period(self, freq=None): """ from pandas.core.indexes.period import PeriodIndex + if self.tz is not None: + warnings.warn("Converting to PeriodIndex representation will " + "drop timezone information.", UserWarning) + if freq is None: freq = self.freqstr or self.inferred_freq @@ -740,7 +744,7 @@ def to_period(self, freq=None): freq = get_period_alias(freq) - return PeriodIndex(self.values, name=self.name, freq=freq, tz=self.tz) + return PeriodIndex(self.values, name=self.name, freq=freq) def snap(self, freq='S'): """ @@ -1204,6 +1208,12 @@ def get_loc(self, key, method=None, tolerance=None): key = Timestamp(key, tz=self.tz) return Index.get_loc(self, key, method, tolerance) + elif isinstance(key, timedelta): + # GH#20464 + raise TypeError("Cannot index {cls} with {other}" + .format(cls=type(self).__name__, + other=type(key).__name__)) + if isinstance(key, time): if method is not None: raise NotImplementedError('cannot yet lookup inexact labels ' diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 063b578e512de..e0c78d6a1c518 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -1,5 +1,6 @@ """ implement the TimedeltaIndex """ import operator +from datetime import datetime import numpy as np from pandas.core.dtypes.common import ( @@ -487,7 +488,11 @@ def get_loc(self, key, method=None, tolerance=None): ------- loc : int """ - if is_list_like(key): + if is_list_like(key) or (isinstance(key, datetime) and key is not NaT): + # GH#20464 datetime check here is to ensure we don't allow + # datetime objects to be incorrectly treated as timedelta + # objects; NaT is a special case because it plays a double role + # as Not-A-Timedelta raise TypeError if isna(key): diff --git a/pandas/tests/indexes/datetimes/test_astype.py b/pandas/tests/indexes/datetimes/test_astype.py index 78b669de95598..be22d80a862e1 100644 --- a/pandas/tests/indexes/datetimes/test_astype.py +++ b/pandas/tests/indexes/datetimes/test_astype.py @@ -246,7 +246,9 @@ def setup_method(self, method): def test_to_period_millisecond(self): index = self.index - period = index.to_period(freq='L') + with tm.assert_produces_warning(UserWarning): + # warning that timezone info will be lost + period = index.to_period(freq='L') assert 2 == len(period) assert period[0] == Period('2007-01-01 10:11:12.123Z', 'L') assert period[1] == Period('2007-01-01 10:11:13.789Z', 'L') @@ -254,7 +256,9 @@ def test_to_period_millisecond(self): def test_to_period_microsecond(self): index = self.index - period = index.to_period(freq='U') + with tm.assert_produces_warning(UserWarning): + # warning that timezone info will be lost + period = index.to_period(freq='U') assert 2 == len(period) assert period[0] == Period('2007-01-01 10:11:12.123456Z', 'U') assert period[1] == Period('2007-01-01 10:11:13.789123Z', 'U') @@ -264,12 +268,20 @@ def test_to_period_microsecond(self): dateutil.tz.tzutc()]) def test_to_period_tz(self, tz): ts = date_range('1/1/2000', '2/1/2000', tz=tz) - result = ts.to_period()[0] - expected = ts[0].to_period() + + with tm.assert_produces_warning(UserWarning): + # GH#21333 warning that timezone info will be lost + result = ts.to_period()[0] + expected = ts[0].to_period() + assert result == expected expected = date_range('1/1/2000', '2/1/2000').to_period() - result = ts.to_period() + + with tm.assert_produces_warning(UserWarning): + # GH#21333 warning that timezone info will be lost + result = ts.to_period() + tm.assert_index_equal(result, expected) def test_to_period_nofreq(self): diff --git a/pandas/tests/indexes/datetimes/test_indexing.py b/pandas/tests/indexes/datetimes/test_indexing.py index 8cffa035721b0..601a7b13e370a 100644 --- a/pandas/tests/indexes/datetimes/test_indexing.py +++ b/pandas/tests/indexes/datetimes/test_indexing.py @@ -586,3 +586,17 @@ def test_reasonable_keyerror(self): with pytest.raises(KeyError) as excinfo: index.get_loc('1/1/2000') assert '2000' in str(excinfo.value) + + @pytest.mark.parametrize('key', [pd.Timedelta(0), + pd.Timedelta(1), + timedelta(0)]) + def test_timedelta_invalid_key(self, key): + # GH#20464 + dti = pd.date_range('1970-01-01', periods=10) + with pytest.raises(TypeError): + dti.get_loc(key) + + def test_get_loc_nat(self): + # GH#20464 + index = DatetimeIndex(['1/3/2000', 'NaT']) + assert index.get_loc(pd.NaT) == 1 diff --git a/pandas/tests/indexes/timedeltas/test_indexing.py b/pandas/tests/indexes/timedeltas/test_indexing.py index 08992188265bd..8ba2c81f429d8 100644 --- a/pandas/tests/indexes/timedeltas/test_indexing.py +++ b/pandas/tests/indexes/timedeltas/test_indexing.py @@ -1,4 +1,4 @@ -from datetime import timedelta +from datetime import datetime, timedelta import pytest import numpy as np @@ -41,6 +41,15 @@ def test_getitem(self): tm.assert_index_equal(result, expected) assert result.freq == expected.freq + @pytest.mark.parametrize('key', [pd.Timestamp('1970-01-01'), + pd.Timestamp('1970-01-02'), + datetime(1970, 1, 1)]) + def test_timestamp_invalid_key(self, key): + # GH#20464 + tdi = pd.timedelta_range(0, periods=10) + with pytest.raises(TypeError): + tdi.get_loc(key) + class TestWhere(object): # placeholder for symmetry with DatetimeIndex and PeriodIndex tests diff --git a/pandas/tests/scalar/timestamp/test_timestamp.py b/pandas/tests/scalar/timestamp/test_timestamp.py index 58146cae587fe..872c510094a4f 100644 --- a/pandas/tests/scalar/timestamp/test_timestamp.py +++ b/pandas/tests/scalar/timestamp/test_timestamp.py @@ -929,3 +929,11 @@ def test_to_datetime_bijective(self): with tm.assert_produces_warning(exp_warning, check_stacklevel=False): assert (Timestamp(Timestamp.min.to_pydatetime()).value / 1000 == Timestamp.min.value / 1000) + + def test_to_period_tz_warning(self): + # GH#21333 make sure a warning is issued when timezone + # info is lost + ts = Timestamp('2009-04-15 16:17:18', tz='US/Eastern') + with tm.assert_produces_warning(UserWarning): + # warning that timezone info will be lost + ts.to_period('D')