diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 1af7d6be750102..4c0a0f405c784d 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 + offset in the formatted string. + .. versionchanged:: 3.6 + Added the *timespec* argument. + .. versionchanged:: next + 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,9 +2002,14 @@ Instance methods: '12:34:56.000000' >>> dt.isoformat(timespec='auto') '12:34:56' + >>> dt = time(12, 30, 59, tzinfo=timezone.utc) + >>> dt.isoformat(use_utc_designator=True) + '12:30:59Z' .. versionchanged:: 3.6 Added the *timespec* parameter. + .. versionchanged:: next + Added the *use_utc_designator* argument. .. method:: time.__str__() diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 26bcd1e491d78c..33e713ebcbd2a8 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.tzinfo is timezone.utc or 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 diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 84eb872f964ba1..b969e737a87601 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,13 @@ def test_isoformat_timezone(self): with self.subTest(tzi=tzi): assert t.isoformat() == exp + t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, + tzinfo=timezone.utc) + 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(use_utc_designator=True), "12:34:56.123456Z") + 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..ae6486929e5504 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,7 +1712,8 @@ static PyObject *delta_negative(PyObject *op); */ static int format_utcoffset(char *buf, size_t buflen, const char *sep, - PyObject *tzinfo, PyObject *tzinfoarg) + int use_utc_designator, + PyObject *tzinfo, PyObject *tzinfoarg) { PyObject *offset; int hours, minutes, seconds, microseconds; @@ -1718,6 +1721,15 @@ 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)) == 0) { + PyOS_snprintf(buf, buflen, "Z"); + return 0; + } + } + offset = call_utcoffset(tzinfo, tzinfoarg); if (offset == NULL) return -1; @@ -1770,6 +1782,7 @@ make_somezreplacement(PyObject *object, char *sep, PyObject *tzinfoarg) if (format_utcoffset(buf, sizeof(buf), sep, + 0, tzinfo, tzinfoarg) < 0) return NULL; @@ -4758,7 +4771,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 +4786,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 +4826,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 +6229,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 +6245,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 +6286,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; }