From d3b66835a95ca99d6d44b80e58194e1f5b8fd83c Mon Sep 17 00:00:00 2001 From: Alexander Belopolsky Date: Fri, 3 Feb 2023 21:49:10 +0400 Subject: [PATCH 1/3] Fix datetime.astimezone() method (gh-83861) WIP - test and pure python fix Resolves gh-83861 --- Lib/datetime.py | 5 +++++ Lib/test/datetimetester.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/Lib/datetime.py b/Lib/datetime.py index 637144637485bc..09a2d2d5381c34 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1965,6 +1965,11 @@ def replace(self, year=None, month=None, day=None, hour=None, def _local_timezone(self): if self.tzinfo is None: ts = self._mktime() + # Detect gap + ts2 = self.replace(fold=1-self.fold)._mktime() + if ts2 != ts: # This happens in a gap or a fold + if (ts2 > ts) == self.fold: + ts = ts2 else: ts = (self - _EPOCH) // timedelta(seconds=1) localtm = _time.localtime(ts) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 570f803918c1ef..477f16f1841f62 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -6212,6 +6212,10 @@ def test_system_transitions(self): ts1 = dt.replace(fold=1).timestamp() self.assertEqual(ts0, s0 + ss / 2) self.assertEqual(ts1, s0 - ss / 2) + # gh-83861 + utc0 = dt.astimezone(timezone.utc) + utc1 = dt.replace(fold=1).astimezone(timezone.utc) + self.assertEqual(utc0, utc1 + timedelta(0, ss)) finally: if TZ is None: del os.environ['TZ'] From b665d1048699d16ce223ddc632c3ae4a1b537f5e Mon Sep 17 00:00:00 2001 From: Alexander Belopolsky Date: Sun, 5 Feb 2023 11:50:06 +0400 Subject: [PATCH 2/3] WIP - add "naive" fix for C implementation --- Modules/_datetimemodule.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index eda8c5610ba659..f317dc14e15bf1 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -6153,17 +6153,31 @@ local_to_seconds(int year, int month, int day, static PyObject * local_timezone_from_local(PyDateTime_DateTime *local_dt) { - long long seconds; + long long seconds, seconds2; time_t timestamp; + int fold = DATE_GET_FOLD(local_dt); seconds = local_to_seconds(GET_YEAR(local_dt), GET_MONTH(local_dt), GET_DAY(local_dt), DATE_GET_HOUR(local_dt), DATE_GET_MINUTE(local_dt), DATE_GET_SECOND(local_dt), - DATE_GET_FOLD(local_dt)); + fold); if (seconds == -1) return NULL; + seconds2 = local_to_seconds(GET_YEAR(local_dt), + GET_MONTH(local_dt), + GET_DAY(local_dt), + DATE_GET_HOUR(local_dt), + DATE_GET_MINUTE(local_dt), + DATE_GET_SECOND(local_dt), + !fold); + if (seconds2 == -1) + return NULL; + /* Detect gap */ + if (seconds2 != seconds && (seconds2 > seconds) == fold) + seconds = seconds2; + /* XXX: add bounds check */ timestamp = seconds - epoch; return local_timezone_from_timestamp(timestamp); From 7634ca290e839d794a5845e4acd4df4a41d260a4 Mon Sep 17 00:00:00 2001 From: Alexander Belopolsky Date: Mon, 6 Feb 2023 16:45:45 +0400 Subject: [PATCH 3/3] Add a NEWS entry --- .../Library/2023-02-06-16-45-18.gh-issue-83861.mMbIU3.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2023-02-06-16-45-18.gh-issue-83861.mMbIU3.rst diff --git a/Misc/NEWS.d/next/Library/2023-02-06-16-45-18.gh-issue-83861.mMbIU3.rst b/Misc/NEWS.d/next/Library/2023-02-06-16-45-18.gh-issue-83861.mMbIU3.rst new file mode 100644 index 00000000000000..e85e7a4ff2e73a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-02-06-16-45-18.gh-issue-83861.mMbIU3.rst @@ -0,0 +1,4 @@ +Fix datetime.astimezone method return value when invoked on a naive datetime +instance that represents local time falling in a timezone transition gap. +PEP 495 requires that instances with fold=1 produce earlier times than those +with fold=0 in this case.