]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-152079: Fix `_datetime.fromisoformat()` mishandling a sub-second tz offset...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Thu, 25 Jun 2026 09:41:13 +0000 (11:41 +0200)
committerGitHub <noreply@github.com>
Thu, 25 Jun 2026 09:41:13 +0000 (09:41 +0000)
(cherry picked from commit 6f9c76d8d86997012acfa09fed05396aa9349bbf)

Co-authored-by: tonghuaroot (童话) <tonghuaroot@gmail.com>
Co-authored-by: Stan Ulbrych <stan@python.org>
Lib/test/datetimetester.py
Misc/NEWS.d/next/Library/2026-06-24-12-00-00.gh-issue-152079.f1tzus.rst [new file with mode: 0644]
Modules/_datetimemodule.c

index 6749fad35a64d91edf769c8493a501c74b125c35..3e7aebe8ecf9bfd19a859d22304de2c1c9e48329 100644 (file)
@@ -3416,6 +3416,32 @@ class TestDateTime(TestDate):
 
         self.assertIs(dt.tzinfo, timezone.utc)
 
+    def test_fromisoformat_utc_subsecond_offset(self):
+        # A UTC offset whose whole-second part is zero but with a non-zero
+        # microsecond part must be preserved, not collapsed to UTC.
+        for us in (1, -1, 999999, -999999):
+            with self.subTest(microseconds=us):
+                tz = timezone(timedelta(microseconds=us))
+                dt = self.theclass(2020, 6, 15, 12, 34, 56, tzinfo=tz)
+                rt = self.theclass.fromisoformat(dt.isoformat())
+                self.assertEqual(rt.utcoffset(), timedelta(microseconds=us))
+                self.assertEqual(rt, dt)
+                self.assertIsNot(rt.tzinfo, timezone.utc)
+
+        tz = timezone(timedelta(hours=5, minutes=30, seconds=15,
+                                microseconds=123456))
+        dt = self.theclass(2020, 6, 15, 12, 34, 56, tzinfo=tz)
+        rt = self.theclass.fromisoformat(dt.isoformat())
+        self.assertEqual(rt.utcoffset(), tz.utcoffset(None))
+        self.assertEqual(rt, dt)
+
+        for tstr in ('2020-06-15T12:34:56+00:00',
+                     '2020-06-15T12:34:56+00:00:00.000000',
+                     '2020-06-15T12:34:56Z'):
+            with self.subTest(tstr=tstr):
+                self.assertIs(self.theclass.fromisoformat(tstr).tzinfo,
+                              timezone.utc)
+
     def test_fromisoformat_subclass(self):
         class DateTimeSubclass(self.theclass):
             pass
diff --git a/Misc/NEWS.d/next/Library/2026-06-24-12-00-00.gh-issue-152079.f1tzus.rst b/Misc/NEWS.d/next/Library/2026-06-24-12-00-00.gh-issue-152079.f1tzus.rst
new file mode 100644 (file)
index 0000000..492d007
--- /dev/null
@@ -0,0 +1,3 @@
+Fix :meth:`datetime.datetime.fromisoformat` in the C implementation dropping
+the sub-second part of a UTC offset whose whole-second part is zero, matching
+the pure-Python implementation.
index 8df5913dfaee3d80d4574fb586f5b29cdb35ce2f..e0d1f57c259c429ab81c9ba68c2a7f082e52c3cd 100644 (file)
@@ -1637,8 +1637,8 @@ tzinfo_from_isoformat_results(int rv, int tzoffset, int tz_useconds)
 {
     PyObject *tzinfo;
     if (rv == 1) {
-        // Create a timezone from offset in seconds (0 returns UTC)
-        if (tzoffset == 0) {
+        // Create a timezone from the offset (a zero offset returns UTC)
+        if (tzoffset == 0 && tz_useconds == 0) {
             return Py_NewRef(CONST_UTC(NO_STATE));
         }