From f7c3f590cc2c268c224e5bc16b37bd20d60109f1 Mon Sep 17 00:00:00 2001 From: Matt Wozniski Date: Sat, 22 Mar 2025 19:12:16 +0000 Subject: [PATCH 1/4] Allow datetime.isoformat to use "Z" UTC designator Conflicts resolved and NEWS updated. Add an optional parameter to datetime.isoformat() and time.isoformat() called use_utc_designator. If provided and True and the object is associated with a tzinfo and its tzname is exactly "UTC", the formatted UTC offset will be "Z" rather than an offset from UTC. --- Doc/library/datetime.rst | 27 +++++-- Lib/test/datetimetester.py | 74 +++++++++++++++++++ ...-03-22-00-00-00.gh-issue-90772.Rgtd_1Y.rst | 4 + Modules/_datetimemodule.c | 40 +++++++--- 4 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-03-22-00-00-00.gh-issue-90772.Rgtd_1Y.rst diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 1af7d6be750102..e355d5b3443caa 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -1536,7 +1536,7 @@ Instance methods: and ``weekday``. The same as ``self.date().isocalendar()``. -.. method:: datetime.isoformat(sep='T', timespec='auto') +.. method:: datetime.isoformat(sep='T', timespec='auto', use_utc_designator=False) Return a string representing the date and time in ISO 8601 format: @@ -1600,9 +1600,14 @@ Instance methods: >>> dt.isoformat(timespec='microseconds') '2015-01-01T12:30:59.000000' - .. versionchanged:: 3.6 - Added the *timespec* parameter. + If the optional argument *use_utc_designator* is set to :const:`True` and + :meth:`tzname` returns exactly ``"UTC"``, then "Z" will be given as the UTC + offset in the formatted string. + .. versionadded:: 3.6 + Added the *timespec* argument. + .. versionadded:: 3.11 + Added the *use_utc_designator* argument. .. method:: datetime.__str__() @@ -1954,7 +1959,7 @@ Instance methods: Added the *fold* parameter. -.. method:: time.isoformat(timespec='auto') +.. method:: time.isoformat(timespec='auto', use_utc_designator=False) Return a string representing the time in ISO 8601 format, one of: @@ -1983,9 +1988,13 @@ Instance methods: :exc:`ValueError` will be raised on an invalid *timespec* argument. + If the optional argument *use_utc_designator* is set to :const:`True` and + :meth:`tzname` returns exactly ``"UTC"``, then "Z" will be given as the UTC + offset in the formatted string. + Example:: - >>> from datetime import time + >>> from datetime import time, timezone >>> time(hour=12, minute=34, second=56, microsecond=123456).isoformat(timespec='minutes') '12:34' >>> dt = time(hour=12, minute=34, second=56, microsecond=0) @@ -1993,10 +2002,18 @@ Instance methods: '12:34:56.000000' >>> dt.isoformat(timespec='auto') '12:34:56' + >>> dt = time(12, 30, 59, tzinfo=timezone.utc) + >>> dt.isoformat() + '12:30:59+00:00' + >>> dt.isoformat(use_utc_designator=True) + '12:30:59Z' .. versionchanged:: 3.6 Added the *timespec* parameter. + .. versionadded:: 3.11 + Added the *use_utc_designator* argument. + .. method:: time.__str__() diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 84eb872f964ba1..0cfe89087aa9ea 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2237,6 +2237,8 @@ def test_isoformat(self): self.assertEqual(t.isoformat(timespec='microseconds'), "0001-02-03T04:05:01.000123") self.assertEqual(t.isoformat(timespec='auto'), "0001-02-03T04:05:01.000123") self.assertEqual(t.isoformat(sep=' ', timespec='minutes'), "0001-02-03 04:05") + self.assertEqual(t.isoformat(use_utc_designator=False), "0001-02-03T04:05:01.000123") + self.assertEqual(t.isoformat(use_utc_designator=True), "0001-02-03T04:05:01.000123") self.assertRaises(ValueError, t.isoformat, timespec='foo') # bpo-34482: Check that surrogates are handled properly. self.assertRaises(ValueError, t.isoformat, timespec='\ud800') @@ -2245,6 +2247,8 @@ def test_isoformat(self): t = self.theclass(1, 2, 3, 4, 5, 1, 999500, tzinfo=timezone.utc) self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05:01.999+00:00") + self.assertEqual(t.isoformat(use_utc_designator=False), "0001-02-03T04:05:01.999500+00:00") + self.assertEqual(t.isoformat(use_utc_designator=True), "0001-02-03T04:05:01.999500Z") t = self.theclass(1, 2, 3, 4, 5, 1, 999500) self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05:01.999") @@ -2264,6 +2268,8 @@ def test_isoformat(self): tz = FixedOffset(timedelta(seconds=16), 'XXX') t = self.theclass(2, 3, 2, tzinfo=tz) self.assertEqual(t.isoformat(), "0002-03-02T00:00:00+00:00:16") + self.assertEqual(t.isoformat(use_utc_designator=False), "0002-03-02T00:00:00+00:00:16") + self.assertEqual(t.isoformat(use_utc_designator=True), "0002-03-02T00:00:00+00:00:16") def test_isoformat_timezone(self): tzoffsets = [ @@ -3836,6 +3842,74 @@ def test_isoformat_timezone(self): with self.subTest(tzi=tzi): assert t.isoformat() == exp + def test_isoformat_utc_designator(self): + t = self.theclass(hour=12, minute=34, second=56, microsecond=123456) + self.assertEqual(t.isoformat(), "12:34:56.123456") + self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456") + self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456") + + t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, + tzinfo=timezone.utc) + self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z") + + t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, + tzinfo=timezone(timedelta(0))) + self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z") + + t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, + tzinfo=timezone(timedelta(0), "UTC")) + self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z") + + t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, + tzinfo=timezone(timedelta(0), "GMT")) + self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456+00:00") + + t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, + tzinfo=timezone(timedelta(hours=5), "UTC")) + self.assertEqual(t.isoformat(), "12:34:56.123456+05:00") + self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+05:00") + self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z") + + class UnnamedTimezone(tzinfo): + def utcoffset(self, dt): + return timedelta(0) + + def dst(self, dt): + return timedelta(0) + + def tzname(self, dt): + return None + + t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, + tzinfo=UnnamedTimezone()) + self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456+00:00") + + class NonStringNamedTimezone(tzinfo): + def utcoffset(self, dt): + return timedelta(0) + + def dst(self, dt): + return timedelta(0) + + def tzname(self, dt): + return 42 + + t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, + tzinfo=UnnamedTimezone()) + self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456+00:00") + def test_1653736(self): # verify it doesn't accept extra keyword arguments t = self.theclass(second=1) diff --git a/Misc/NEWS.d/next/Library/2025-03-22-00-00-00.gh-issue-90772.Rgtd_1Y.rst b/Misc/NEWS.d/next/Library/2025-03-22-00-00-00.gh-issue-90772.Rgtd_1Y.rst new file mode 100644 index 00000000000000..eb5940805bb52f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-03-22-00-00-00.gh-issue-90772.Rgtd_1Y.rst @@ -0,0 +1,4 @@ +Add *use_utc_designator* as an optional parameter to +:meth:`datetime.datetime.isoformat` and :meth:`datetime.time.isoformat`. If +it's set to true, the UTC offset will be formatted as "Z" rather than "+00:00" +if the object is associated with a timezone named exactly ``"UTC"``. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 70b59d67f56bda..60d16044d17e0d 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1699,8 +1699,10 @@ format_ctime(PyObject *date, int hours, int minutes, int seconds) static PyObject *delta_negative(PyObject *op); /* Add formatted UTC offset string to buf. buf has no more than - * buflen bytes remaining. The UTC offset is gotten by calling - * tzinfo.uctoffset(tzinfoarg). If that returns None, \0 is stored into + * buflen bytes remaining. If use_utc_designator is true, + * tzinfo.tzname(tzinfoarg) will be called, and if it returns "UTC", + * only "Z\0" will be added. Otherwise, the UTC offset is gotten by calling + * tzinfo.utcoffset(tzinfoarg). If that returns None, \0 is stored into * *buf, and that's all. Else the returned value is checked for sanity (an * integer in range), and if that's OK it's converted to an hours & minutes * string of the form @@ -1710,6 +1712,7 @@ static PyObject *delta_negative(PyObject *op); */ static int format_utcoffset(char *buf, size_t buflen, const char *sep, + int use_utc_designator, PyObject *tzinfo, PyObject *tzinfoarg) { PyObject *offset; @@ -1718,6 +1721,20 @@ format_utcoffset(char *buf, size_t buflen, const char *sep, assert(buflen >= 1); + if (use_utc_designator) { + PyObject* name = PyObject_CallMethod(tzinfo, "tzname", "O", tzinfoarg); + if (name == NULL) + return -1; + int tz_is_utc = (PyUnicode_Check(name) && + 0 == strcmp("UTC", PyUnicode_AsUTF8(name))); + Py_DECREF(name); + + if (tz_is_utc) { + PyOS_snprintf(buf, buflen, "Z"); + return 0; + } + } + offset = call_utcoffset(tzinfo, tzinfoarg); if (offset == NULL) return -1; @@ -4758,7 +4775,8 @@ time_isoformat(PyObject *op, PyObject *args, PyObject *kw) { char buf[100]; const char *timespec = NULL; - static char *keywords[] = {"timespec", NULL}; + int use_utc_designator = 0; + static char *keywords[] = {"timespec", "use_utc_designator", NULL}; PyDateTime_Time *self = PyTime_CAST(op); PyObject *result; @@ -4772,7 +4790,8 @@ time_isoformat(PyObject *op, PyObject *args, PyObject *kw) }; size_t given_spec; - if (!PyArg_ParseTupleAndKeywords(args, kw, "|s:isoformat", keywords, ×pec)) + if (!PyArg_ParseTupleAndKeywords(args, kw, "|sp:isoformat", keywords, + ×pec, &use_utc_designator)) return NULL; if (timespec == NULL || strcmp(timespec, "auto") == 0) { @@ -4811,8 +4830,8 @@ time_isoformat(PyObject *op, PyObject *args, PyObject *kw) return result; /* We need to append the UTC offset. */ - if (format_utcoffset(buf, sizeof(buf), ":", self->tzinfo, - Py_None) < 0) { + if (format_utcoffset(buf, sizeof(buf), ":", use_utc_designator, + self->tzinfo, Py_None) < 0) { Py_DECREF(result); return NULL; } @@ -6214,7 +6233,8 @@ datetime_isoformat(PyObject *op, PyObject *args, PyObject *kw) { int sep = 'T'; char *timespec = NULL; - static char *keywords[] = {"sep", "timespec", NULL}; + int use_utc_designator = 0; + static char *keywords[] = {"sep", "timespec", "use_utc_designator", NULL}; char buffer[100]; PyDateTime_DateTime *self = PyDateTime_CAST(op); @@ -6229,7 +6249,8 @@ datetime_isoformat(PyObject *op, PyObject *args, PyObject *kw) }; size_t given_spec; - if (!PyArg_ParseTupleAndKeywords(args, kw, "|Cs:isoformat", keywords, &sep, ×pec)) + if (!PyArg_ParseTupleAndKeywords(args, kw, "|Csp:isoformat", keywords, + &sep, ×pec, &use_utc_designator)) return NULL; if (timespec == NULL || strcmp(timespec, "auto") == 0) { @@ -6269,7 +6290,8 @@ datetime_isoformat(PyObject *op, PyObject *args, PyObject *kw) return result; /* We need to append the UTC offset. */ - if (format_utcoffset(buffer, sizeof(buffer), ":", self->tzinfo, op) < 0) { + if (format_utcoffset(buffer, sizeof(buffer), ":", use_utc_designator, + self->tzinfo, (PyObject *)self) < 0) { Py_DECREF(result); return NULL; } From c72d219648ae60f450ee5875d773dc453671d28e Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sat, 22 Mar 2025 19:24:25 +0000 Subject: [PATCH 2/4] Apply Python changes and update docs --- Doc/library/datetime.rst | 7 ++----- Lib/_pydatetime.py | 29 +++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index e355d5b3443caa..37276ac19606d6 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -1606,7 +1606,7 @@ Instance methods: .. versionadded:: 3.6 Added the *timespec* argument. - .. versionadded:: 3.11 + .. versionadded:: next Added the *use_utc_designator* argument. .. method:: datetime.__str__() @@ -2003,15 +2003,12 @@ Instance methods: >>> dt.isoformat(timespec='auto') '12:34:56' >>> dt = time(12, 30, 59, tzinfo=timezone.utc) - >>> dt.isoformat() - '12:30:59+00:00' >>> dt.isoformat(use_utc_designator=True) '12:30:59Z' .. versionchanged:: 3.6 Added the *timespec* parameter. - - .. versionadded:: 3.11 + .. versionadded:: next Added the *use_utc_designator* argument. diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 26bcd1e491d78c..bbc8d8c395a38c 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -1598,7 +1598,7 @@ def __repr__(self): s = s[:-1] + ", fold=1)" return s - def isoformat(self, timespec='auto'): + def isoformat(self, timespec='auto', use_utc_designator=False): """Return the time formatted according to ISO. The full format is 'HH:MM:SS.mmmmmm+zz:zz'. By default, the fractional @@ -1607,12 +1607,19 @@ def isoformat(self, timespec='auto'): The optional argument timespec specifies the number of additional terms of the time to include. Valid options are 'auto', 'hours', 'minutes', 'seconds', 'milliseconds' and 'microseconds'. + + The UTC offset will be replaced with 'Z' if use_utc_designator + is True and self.tzname() is exactly 'UTC'. """ s = _format_time(self._hour, self._minute, self._second, self._microsecond, timespec) - tz = self._tzstr() - if tz: - s += tz + + if use_utc_designator and self.tzname() == 'UTC': + s += 'Z' + else: + tz = self._tzstr() + if tz: + s += tz return s __str__ = isoformat @@ -2128,7 +2135,7 @@ def ctime(self): self._hour, self._minute, self._second, self._year) - def isoformat(self, sep='T', timespec='auto'): + def isoformat(self, sep='T', timespec='auto', use_utc_designator=False): """Return the time formatted according to ISO. The full format looks like 'YYYY-MM-DD HH:MM:SS.mmmmmm'. @@ -2143,15 +2150,21 @@ def isoformat(self, sep='T', timespec='auto'): The optional argument timespec specifies the number of additional terms of the time to include. Valid options are 'auto', 'hours', 'minutes', 'seconds', 'milliseconds' and 'microseconds'. + + The UTC offset will be replaced with 'Z' if use_utc_designator + is True and self.tzname() is exactly 'UTC'. """ s = ("%04d-%02d-%02d%c" % (self._year, self._month, self._day, sep) + _format_time(self._hour, self._minute, self._second, self._microsecond, timespec)) off = self.utcoffset() - tz = _format_offset(off) - if tz: - s += tz + if use_utc_designator and (self.tzinfo is timezone.utc or self.tzname() == 'UTC'): + s += 'Z' + else: + tz = _format_offset(off) + if tz: + s += tz return s From 8cc0d759ca22d6b0f3c690f0d212a9e23c56506b Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sat, 22 Mar 2025 19:45:01 +0000 Subject: [PATCH 3/4] Fix stuff --- Doc/library/datetime.rst | 8 ++++---- Lib/_pydatetime.py | 2 +- Modules/_datetimemodule.c | 19 +++++++------------ 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 37276ac19606d6..4c0a0f405c784d 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -1601,12 +1601,12 @@ Instance methods: '2015-01-01T12:30:59.000000' If the optional argument *use_utc_designator* is set to :const:`True` and - :meth:`tzname` returns exactly ``"UTC"``, then "Z" will be given as the UTC + :meth:`tzname` returns exactly ``'UTC'``, then 'Z' will be given as the offset in the formatted string. - .. versionadded:: 3.6 + .. versionchanged:: 3.6 Added the *timespec* argument. - .. versionadded:: next + .. versionchanged:: next Added the *use_utc_designator* argument. .. method:: datetime.__str__() @@ -2008,7 +2008,7 @@ Instance methods: .. versionchanged:: 3.6 Added the *timespec* parameter. - .. versionadded:: next + .. versionchanged:: next Added the *use_utc_designator* argument. diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index bbc8d8c395a38c..33e713ebcbd2a8 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -1614,7 +1614,7 @@ def isoformat(self, timespec='auto', use_utc_designator=False): s = _format_time(self._hour, self._minute, self._second, self._microsecond, timespec) - if use_utc_designator and self.tzname() == 'UTC': + if use_utc_designator and (self.tzinfo is timezone.utc or self.tzname() == 'UTC'): s += 'Z' else: tz = self._tzstr() diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 60d16044d17e0d..d54c571e6c094a 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1700,8 +1700,8 @@ static PyObject *delta_negative(PyObject *op); /* Add formatted UTC offset string to buf. buf has no more than * buflen bytes remaining. If use_utc_designator is true, - * tzinfo.tzname(tzinfoarg) will be called, and if it returns "UTC", - * only "Z\0" will be added. Otherwise, the UTC offset is gotten by calling + * tzinfo.tzname(tzinfoarg) will be called, and if it returns 'UTC', + * only 'Z\0' will be added. Otherwise, the UTC offset is gotten by calling * tzinfo.utcoffset(tzinfoarg). If that returns None, \0 is stored into * *buf, and that's all. Else the returned value is checked for sanity (an * integer in range), and if that's OK it's converted to an hours & minutes @@ -1712,8 +1712,8 @@ static PyObject *delta_negative(PyObject *op); */ static int format_utcoffset(char *buf, size_t buflen, const char *sep, - int use_utc_designator, - PyObject *tzinfo, PyObject *tzinfoarg) + int use_utc_designator, + PyObject *tzinfo, PyObject *tzinfoarg) { PyObject *offset; int hours, minutes, seconds, microseconds; @@ -1722,14 +1722,8 @@ format_utcoffset(char *buf, size_t buflen, const char *sep, assert(buflen >= 1); if (use_utc_designator) { - PyObject* name = PyObject_CallMethod(tzinfo, "tzname", "O", tzinfoarg); - if (name == NULL) - return -1; - int tz_is_utc = (PyUnicode_Check(name) && - 0 == strcmp("UTC", PyUnicode_AsUTF8(name))); - Py_DECREF(name); - - if (tz_is_utc) { + PyObject *name = PyObject_CallMethod(tzinfo, "tzname", "O", tzinfoarg); + if (PyUnicode_Check(name) && strcmp("UTC", PyUnicode_AsUTF8(name))) { PyOS_snprintf(buf, buflen, "Z"); return 0; } @@ -1787,6 +1781,7 @@ make_somezreplacement(PyObject *object, char *sep, PyObject *tzinfoarg) if (format_utcoffset(buf, sizeof(buf), sep, + 0, tzinfo, tzinfoarg) < 0) return NULL; From 959436e6c781992f8436aa6489fabb28f723125e Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sat, 22 Mar 2025 19:57:58 +0000 Subject: [PATCH 4/4] Simplify tests --- Lib/test/datetimetester.py | 61 -------------------------------------- Modules/_datetimemodule.c | 5 ++-- 2 files changed, 3 insertions(+), 63 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 0cfe89087aa9ea..b969e737a87601 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3842,74 +3842,13 @@ def test_isoformat_timezone(self): with self.subTest(tzi=tzi): assert t.isoformat() == exp - def test_isoformat_utc_designator(self): - t = self.theclass(hour=12, minute=34, second=56, microsecond=123456) - self.assertEqual(t.isoformat(), "12:34:56.123456") - self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456") - self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456") - t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, tzinfo=timezone.utc) - self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") - self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z") - - t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, - tzinfo=timezone(timedelta(0))) - self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") - self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") - self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z") - t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, tzinfo=timezone(timedelta(0), "UTC")) - self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") - self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") - self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z") - - t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, - tzinfo=timezone(timedelta(0), "GMT")) - self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") - self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") - self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456+00:00") - - t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, - tzinfo=timezone(timedelta(hours=5), "UTC")) - self.assertEqual(t.isoformat(), "12:34:56.123456+05:00") - self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+05:00") self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z") - class UnnamedTimezone(tzinfo): - def utcoffset(self, dt): - return timedelta(0) - - def dst(self, dt): - return timedelta(0) - - def tzname(self, dt): - return None - - t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, - tzinfo=UnnamedTimezone()) - self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") - self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") - self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456+00:00") - - class NonStringNamedTimezone(tzinfo): - def utcoffset(self, dt): - return timedelta(0) - - def dst(self, dt): - return timedelta(0) - - def tzname(self, dt): - return 42 - - t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, - tzinfo=UnnamedTimezone()) - self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") - self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") - self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456+00:00") - def test_1653736(self): # verify it doesn't accept extra keyword arguments t = self.theclass(second=1) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index d54c571e6c094a..ae6486929e5504 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1722,8 +1722,9 @@ format_utcoffset(char *buf, size_t buflen, const char *sep, assert(buflen >= 1); if (use_utc_designator) { - PyObject *name = PyObject_CallMethod(tzinfo, "tzname", "O", tzinfoarg); - if (PyUnicode_Check(name) && strcmp("UTC", PyUnicode_AsUTF8(name))) { + PyObject* name = PyObject_CallMethod(tzinfo, "tzname", "O", tzinfoarg); + + if (PyUnicode_Check(name) && strcmp("UTC", PyUnicode_AsUTF8(name)) == 0) { PyOS_snprintf(buf, buflen, "Z"); return 0; }