From eedf0d5cc2801cc3849ec9abb4e8b3f509d91ee9 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Wed, 1 Nov 2023 05:18:37 -0700 Subject: [PATCH] BUG: Timestamp(ambiguous, tz=from_pytz) not raising (#55712) --- doc/source/whatsnew/v2.2.0.rst | 1 + pandas/_libs/tslibs/conversion.pyx | 3 +- .../indexes/datetimes/test_constructors.py | 30 +++++++++++++++++++ pandas/tests/tseries/offsets/test_dst.py | 26 ++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst index 99b6310a80f83..16d279bb0d52c 100644 --- a/doc/source/whatsnew/v2.2.0.rst +++ b/doc/source/whatsnew/v2.2.0.rst @@ -342,6 +342,7 @@ Timedelta Timezones ^^^^^^^^^ - Bug in :class:`AbstractHolidayCalendar` where timezone data was not propagated when computing holiday observances (:issue:`54580`) +- Bug in :class:`Timestamp` construction with an ambiguous value and a ``pytz`` timezone failing to raise ``pytz.AmbiguousTimeError`` (:issue:`55657`) - Numeric diff --git a/pandas/_libs/tslibs/conversion.pyx b/pandas/_libs/tslibs/conversion.pyx index dc1cc906c9d07..68aaaafd208d4 100644 --- a/pandas/_libs/tslibs/conversion.pyx +++ b/pandas/_libs/tslibs/conversion.pyx @@ -645,7 +645,8 @@ cdef datetime _localize_pydatetime(datetime dt, tzinfo tz): """ try: # datetime.replace with pytz may be incorrect result - return tz.localize(dt) + # TODO: try to respect `fold` attribute + return tz.localize(dt, is_dst=None) except AttributeError: return dt.replace(tzinfo=tz) diff --git a/pandas/tests/indexes/datetimes/test_constructors.py b/pandas/tests/indexes/datetimes/test_constructors.py index 90ddc9b5f618a..22353da57de73 100644 --- a/pandas/tests/indexes/datetimes/test_constructors.py +++ b/pandas/tests/indexes/datetimes/test_constructors.py @@ -1013,6 +1013,36 @@ def test_dti_convert_datetime_list(self, tzstr): dr2 = DatetimeIndex(list(dr), name="foo", freq="D") tm.assert_index_equal(dr, dr2) + def test_dti_ambiguous_matches_timestamp(self): + # GH#47471 check that we get the same raising behavior in the DTI + # constructor and Timestamp constructor + dtstr = "2013-11-03 01:59:59.999999" + dtobj = Timestamp(dtstr).to_pydatetime() + + tz = pytz.timezone("US/Eastern") + with pytest.raises(pytz.AmbiguousTimeError, match=dtstr): + Timestamp(dtstr, tz=tz) + with pytest.raises(pytz.AmbiguousTimeError, match=dtstr): + Timestamp(dtobj, tz=tz) + with pytest.raises(pytz.AmbiguousTimeError, match=dtstr): + DatetimeIndex([dtstr], tz=tz) + with pytest.raises(pytz.AmbiguousTimeError, match=dtstr): + DatetimeIndex([dtobj], tz=tz) + + tz2 = gettz("US/Eastern") + with pytest.raises(pytz.AmbiguousTimeError, match=dtstr): + Timestamp(dtstr, tz=tz2) + # FIXME: The Timestamp constructor here behaves differently than all + # the other cases bc with dateutil/zoneinfo tzinfos we implicitly + # get fold=0. Having this raise is not important, but having the + # behavior be consistent across cases is. + # with pytest.raises(pytz.AmbiguousTimeError, match=dtstr): + # Timestamp(dtobj, tz=tz2) + with pytest.raises(pytz.AmbiguousTimeError, match=dtstr): + DatetimeIndex([dtstr], tz=tz2) + with pytest.raises(pytz.AmbiguousTimeError, match=dtstr): + DatetimeIndex([dtobj], tz=tz2) + @pytest.mark.parametrize("tz", [None, "UTC", "US/Pacific"]) def test_dti_constructor_with_non_nano_dtype(self, tz): # GH#55756, GH#54620 diff --git a/pandas/tests/tseries/offsets/test_dst.py b/pandas/tests/tseries/offsets/test_dst.py index ea4855baa87e1..b22dc0b330817 100644 --- a/pandas/tests/tseries/offsets/test_dst.py +++ b/pandas/tests/tseries/offsets/test_dst.py @@ -29,7 +29,10 @@ YearBegin, YearEnd, ) +from pandas.errors import PerformanceWarning +from pandas import DatetimeIndex +import pandas._testing as tm from pandas.util.version import Version # error: Module has no attribute "__version__" @@ -83,6 +86,29 @@ def _test_all_offsets(self, n, **kwds): def _test_offset(self, offset_name, offset_n, tstart, expected_utc_offset): offset = DateOffset(**{offset_name: offset_n}) + if ( + offset_name in ["hour", "minute", "second", "microsecond"] + and offset_n == 1 + and tstart == Timestamp("2013-11-03 01:59:59.999999-0500", tz="US/Eastern") + ): + # This addition results in an ambiguous wall time + err_msg = { + "hour": "2013-11-03 01:59:59.999999", + "minute": "2013-11-03 01:01:59.999999", + "second": "2013-11-03 01:59:01.999999", + "microsecond": "2013-11-03 01:59:59.000001", + }[offset_name] + with pytest.raises(pytz.AmbiguousTimeError, match=err_msg): + tstart + offset + # While we're here, let's check that we get the same behavior in a + # vectorized path + dti = DatetimeIndex([tstart]) + warn_msg = "Non-vectorized DateOffset" + with pytest.raises(pytz.AmbiguousTimeError, match=err_msg): + with tm.assert_produces_warning(PerformanceWarning, match=warn_msg): + dti + offset + return + t = tstart + offset if expected_utc_offset is not None: assert get_utc_offset_hours(t) == expected_utc_offset