[Python-checkins] gh-83861: Fix datetime.astimezone() method (GH-101545)

miss-islington webhook-mailer at python.org
Wed Apr 19 17:02:36 EDT 2023


https://github.com/python/cpython/commit/2b1260c55763a952c57b92fe0f274b6ee79efd05
commit: 2b1260c55763a952c57b92fe0f274b6ee79efd05
branch: main
author: Alexander Belopolsky <abalkin at users.noreply.github.com>
committer: miss-islington <31488909+miss-islington at users.noreply.github.com>
date: 2023-04-19T14:02:29-07:00
summary:

gh-83861: Fix datetime.astimezone() method (GH-101545)

files:
A Misc/NEWS.d/next/Library/2023-02-06-16-45-18.gh-issue-83861.mMbIU3.rst
M Lib/datetime.py
M Lib/test/datetimetester.py
M Modules/_datetimemodule.c

diff --git a/Lib/datetime.py b/Lib/datetime.py
index 637144637485..09a2d2d5381c 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 570f803918c1..477f16f1841f 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']
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 000000000000..e85e7a4ff2e7
--- /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.
diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c
index eda8c5610ba6..f317dc14e15b 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);



More information about the Python-checkins mailing list