From 08e0d396eb4ee209fe25b236abffa1a638c7fb80 Mon Sep 17 00:00:00 2001 From: Anh Trinh Date: Fri, 12 Apr 2024 00:23:16 +0200 Subject: [PATCH 1/4] BUG: Timestamp ignoring explicit tz=None --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/_libs/tslibs/timestamps.pyx | 20 ++++++++++++++++--- .../scalar/timestamp/test_constructors.py | 9 +++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index e05cc87d1af14..66f6c10e36bdc 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -357,6 +357,7 @@ Categorical Datetimelike ^^^^^^^^^^^^ +- Bug in :class:`Timestamp` constructor failing to raise when ``tz=None`` is explicitly specified in conjunction with timezone-aware ``tzinfo`` or data (:issue:`48688`) - Bug in :func:`date_range` where the last valid timestamp would sometimes not be produced (:issue:`56134`) - Bug in :func:`date_range` where using a negative frequency value would not include all points between the start and end values (:issue:`56382`) - diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index d4cd90613ca5b..c09e3612942ab 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -1751,7 +1751,7 @@ class Timestamp(_Timestamp): tzinfo_type tzinfo=None, *, nanosecond=None, - tz=None, + tz=_no_input, unit=None, fold=None, ): @@ -1783,12 +1783,21 @@ class Timestamp(_Timestamp): _date_attributes = [year, month, day, hour, minute, second, microsecond, nanosecond] + explicit_tz_none = tz is None + if tz is _no_input: + tz = None + if tzinfo is not None: # GH#17690 tzinfo must be a datetime.tzinfo object, ensured # by the cython annotation. if tz is not None: raise ValueError("Can provide at most one of tz, tzinfo") + if explicit_tz_none: + raise ValueError( + "Passed data is timezone-aware, incompatible with 'tz=None'." + ) + # User passed tzinfo instead of tz; avoid silently ignoring tz, tzinfo = tzinfo, None @@ -1867,9 +1876,14 @@ class Timestamp(_Timestamp): hour or 0, minute or 0, second or 0, fold=fold or 0) unit = None - if getattr(ts_input, "tzinfo", None) is not None and tz is not None: - raise ValueError("Cannot pass a datetime or Timestamp with tzinfo with " + if getattr(ts_input, "tzinfo", None) is not None: + if tz is not None: + raise ValueError("Cannot pass a datetime or Timestamp with tzinfo with " "the tz parameter. Use tz_convert instead.") + if explicit_tz_none: + raise ValueError( + "Passed data is timezone-aware, incompatible with 'tz=None'." + ) tzobj = maybe_get_tz(tz) diff --git a/pandas/tests/scalar/timestamp/test_constructors.py b/pandas/tests/scalar/timestamp/test_constructors.py index bbda9d3ee7dce..e4545a7e4fcfd 100644 --- a/pandas/tests/scalar/timestamp/test_constructors.py +++ b/pandas/tests/scalar/timestamp/test_constructors.py @@ -1013,6 +1013,15 @@ def test_timestamp_constructed_by_date_and_tz(self, tz): assert result.hour == expected.hour assert result == expected + def test_explicit_tz_none(self): + # GH#48688 + msg = "Passed data is timezone-aware, incompatible with 'tz=None'" + with pytest.raises(ValueError, match=msg): + Timestamp(datetime(2022, 1, 1, tzinfo=timezone.utc), tz=None) + + with pytest.raises(ValueError, match=msg): + Timestamp("2022-01-01 00:00:00", tzinfo=timezone.utc, tz=None) + def test_constructor_ambiguous_dst(): # GH 24329 From aa2acbb6fbba78247d5e6a9b812a257e1cb57392 Mon Sep 17 00:00:00 2001 From: Anh Trinh Date: Fri, 12 Apr 2024 00:27:24 +0200 Subject: [PATCH 2/4] Fix lint error --- pandas/_libs/tslibs/timestamps.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index c09e3612942ab..96246f122fd42 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -1879,7 +1879,7 @@ class Timestamp(_Timestamp): if getattr(ts_input, "tzinfo", None) is not None: if tz is not None: raise ValueError("Cannot pass a datetime or Timestamp with tzinfo with " - "the tz parameter. Use tz_convert instead.") + "the tz parameter. Use tz_convert instead.") if explicit_tz_none: raise ValueError( "Passed data is timezone-aware, incompatible with 'tz=None'." From 496b3a9486aef67da5b7f1d06dc8978e0fcd0bfc Mon Sep 17 00:00:00 2001 From: Anh Trinh Date: Fri, 12 Apr 2024 00:40:03 +0200 Subject: [PATCH 3/4] Fix new edge case --- pandas/_libs/tslibs/timestamps.pyx | 5 +++++ pandas/tests/scalar/timestamp/test_constructors.py | 3 +++ pandas/tests/scalar/timestamp/test_formats.py | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 96246f122fd42..a5049b238e9b0 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -1897,6 +1897,11 @@ class Timestamp(_Timestamp): if ts.value == NPY_NAT: return NaT + if ts.tzinfo is not None and explicit_tz_none: + raise ValueError( + "Passed data is timezone-aware, incompatible with 'tz=None'." + ) + return create_timestamp_from_ts(ts.value, ts.dts, ts.tzinfo, ts.fold, ts.creso) def _round(self, freq, mode, ambiguous="raise", nonexistent="raise"): diff --git a/pandas/tests/scalar/timestamp/test_constructors.py b/pandas/tests/scalar/timestamp/test_constructors.py index e4545a7e4fcfd..621e199b2e510 100644 --- a/pandas/tests/scalar/timestamp/test_constructors.py +++ b/pandas/tests/scalar/timestamp/test_constructors.py @@ -1022,6 +1022,9 @@ def test_explicit_tz_none(self): with pytest.raises(ValueError, match=msg): Timestamp("2022-01-01 00:00:00", tzinfo=timezone.utc, tz=None) + with pytest.raises(ValueError, match=msg): + Timestamp("2022-01-01 00:00:00-0400", tz=None) + def test_constructor_ambiguous_dst(): # GH 24329 diff --git a/pandas/tests/scalar/timestamp/test_formats.py b/pandas/tests/scalar/timestamp/test_formats.py index b4493088acb31..e1299c272e5cc 100644 --- a/pandas/tests/scalar/timestamp/test_formats.py +++ b/pandas/tests/scalar/timestamp/test_formats.py @@ -118,7 +118,7 @@ def test_repr(self, date, freq, tz): def test_repr_utcoffset(self): # This can cause the tz field to be populated, but it's redundant to # include this information in the date-string. - date_with_utc_offset = Timestamp("2014-03-13 00:00:00-0400", tz=None) + date_with_utc_offset = Timestamp("2014-03-13 00:00:00-0400") assert "2014-03-13 00:00:00-0400" in repr(date_with_utc_offset) assert "tzoffset" not in repr(date_with_utc_offset) assert "UTC-04:00" in repr(date_with_utc_offset) From c69a62732da1d25fb626348e9ffa9e40b018200e Mon Sep 17 00:00:00 2001 From: Anh Trinh Date: Fri, 12 Apr 2024 01:07:05 +0200 Subject: [PATCH 4/4] Simplify --- pandas/_libs/tslibs/timestamps.pyx | 16 +++------------- pandas/tests/indexing/test_at.py | 6 +++++- .../tests/scalar/timestamp/test_constructors.py | 1 - 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index a5049b238e9b0..82daa6d942095 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -1793,11 +1793,6 @@ class Timestamp(_Timestamp): if tz is not None: raise ValueError("Can provide at most one of tz, tzinfo") - if explicit_tz_none: - raise ValueError( - "Passed data is timezone-aware, incompatible with 'tz=None'." - ) - # User passed tzinfo instead of tz; avoid silently ignoring tz, tzinfo = tzinfo, None @@ -1876,14 +1871,9 @@ class Timestamp(_Timestamp): hour or 0, minute or 0, second or 0, fold=fold or 0) unit = None - if getattr(ts_input, "tzinfo", None) is not None: - if tz is not None: - raise ValueError("Cannot pass a datetime or Timestamp with tzinfo with " - "the tz parameter. Use tz_convert instead.") - if explicit_tz_none: - raise ValueError( - "Passed data is timezone-aware, incompatible with 'tz=None'." - ) + if getattr(ts_input, "tzinfo", None) is not None and tz is not None: + raise ValueError("Cannot pass a datetime or Timestamp with tzinfo with " + "the tz parameter. Use tz_convert instead.") tzobj = maybe_get_tz(tz) diff --git a/pandas/tests/indexing/test_at.py b/pandas/tests/indexing/test_at.py index d78694018749c..217ca74bd7fbd 100644 --- a/pandas/tests/indexing/test_at.py +++ b/pandas/tests/indexing/test_at.py @@ -136,7 +136,11 @@ def test_at_datetime_index(self, row): class TestAtSetItemWithExpansion: def test_at_setitem_expansion_series_dt64tz_value(self, tz_naive_fixture): # GH#25506 - ts = Timestamp("2017-08-05 00:00:00+0100", tz=tz_naive_fixture) + ts = ( + Timestamp("2017-08-05 00:00:00+0100", tz=tz_naive_fixture) + if tz_naive_fixture is not None + else Timestamp("2017-08-05 00:00:00+0100") + ) result = Series(ts) result.at[1] = ts expected = Series([ts, ts]) diff --git a/pandas/tests/scalar/timestamp/test_constructors.py b/pandas/tests/scalar/timestamp/test_constructors.py index 621e199b2e510..4ebdea3733484 100644 --- a/pandas/tests/scalar/timestamp/test_constructors.py +++ b/pandas/tests/scalar/timestamp/test_constructors.py @@ -621,7 +621,6 @@ def test_constructor_with_stringoffset(self): ] timezones = [ - (None, 0), ("UTC", 0), (pytz.utc, 0), ("Asia/Tokyo", 9),