Skip to content

Commit f7c3f59

Browse files
godlygeekStanFromIreland
authored andcommitted
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.
1 parent b8367e7 commit f7c3f59

File tree

4 files changed

+131
-14
lines changed

4 files changed

+131
-14
lines changed

Doc/library/datetime.rst

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1536,7 +1536,7 @@ Instance methods:
15361536
and ``weekday``. The same as ``self.date().isocalendar()``.
15371537

15381538

1539-
.. method:: datetime.isoformat(sep='T', timespec='auto')
1539+
.. method:: datetime.isoformat(sep='T', timespec='auto', use_utc_designator=False)
15401540

15411541
Return a string representing the date and time in ISO 8601 format:
15421542

@@ -1600,9 +1600,14 @@ Instance methods:
16001600
>>> dt.isoformat(timespec='microseconds')
16011601
'2015-01-01T12:30:59.000000'
16021602

1603-
.. versionchanged:: 3.6
1604-
Added the *timespec* parameter.
1603+
If the optional argument *use_utc_designator* is set to :const:`True` and
1604+
:meth:`tzname` returns exactly ``"UTC"``, then "Z" will be given as the UTC
1605+
offset in the formatted string.
16051606

1607+
.. versionadded:: 3.6
1608+
Added the *timespec* argument.
1609+
.. versionadded:: 3.11
1610+
Added the *use_utc_designator* argument.
16061611

16071612
.. method:: datetime.__str__()
16081613

@@ -1954,7 +1959,7 @@ Instance methods:
19541959
Added the *fold* parameter.
19551960

19561961

1957-
.. method:: time.isoformat(timespec='auto')
1962+
.. method:: time.isoformat(timespec='auto', use_utc_designator=False)
19581963

19591964
Return a string representing the time in ISO 8601 format, one of:
19601965

@@ -1983,20 +1988,32 @@ Instance methods:
19831988

19841989
:exc:`ValueError` will be raised on an invalid *timespec* argument.
19851990

1991+
If the optional argument *use_utc_designator* is set to :const:`True` and
1992+
:meth:`tzname` returns exactly ``"UTC"``, then "Z" will be given as the UTC
1993+
offset in the formatted string.
1994+
19861995
Example::
19871996

1988-
>>> from datetime import time
1997+
>>> from datetime import time, timezone
19891998
>>> time(hour=12, minute=34, second=56, microsecond=123456).isoformat(timespec='minutes')
19901999
'12:34'
19912000
>>> dt = time(hour=12, minute=34, second=56, microsecond=0)
19922001
>>> dt.isoformat(timespec='microseconds')
19932002
'12:34:56.000000'
19942003
>>> dt.isoformat(timespec='auto')
19952004
'12:34:56'
2005+
>>> dt = time(12, 30, 59, tzinfo=timezone.utc)
2006+
>>> dt.isoformat()
2007+
'12:30:59+00:00'
2008+
>>> dt.isoformat(use_utc_designator=True)
2009+
'12:30:59Z'
19962010

19972011
.. versionchanged:: 3.6
19982012
Added the *timespec* parameter.
19992013

2014+
.. versionadded:: 3.11
2015+
Added the *use_utc_designator* argument.
2016+
20002017

20012018
.. method:: time.__str__()
20022019

Lib/test/datetimetester.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2237,6 +2237,8 @@ def test_isoformat(self):
22372237
self.assertEqual(t.isoformat(timespec='microseconds'), "0001-02-03T04:05:01.000123")
22382238
self.assertEqual(t.isoformat(timespec='auto'), "0001-02-03T04:05:01.000123")
22392239
self.assertEqual(t.isoformat(sep=' ', timespec='minutes'), "0001-02-03 04:05")
2240+
self.assertEqual(t.isoformat(use_utc_designator=False), "0001-02-03T04:05:01.000123")
2241+
self.assertEqual(t.isoformat(use_utc_designator=True), "0001-02-03T04:05:01.000123")
22402242
self.assertRaises(ValueError, t.isoformat, timespec='foo')
22412243
# bpo-34482: Check that surrogates are handled properly.
22422244
self.assertRaises(ValueError, t.isoformat, timespec='\ud800')
@@ -2245,6 +2247,8 @@ def test_isoformat(self):
22452247

22462248
t = self.theclass(1, 2, 3, 4, 5, 1, 999500, tzinfo=timezone.utc)
22472249
self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05:01.999+00:00")
2250+
self.assertEqual(t.isoformat(use_utc_designator=False), "0001-02-03T04:05:01.999500+00:00")
2251+
self.assertEqual(t.isoformat(use_utc_designator=True), "0001-02-03T04:05:01.999500Z")
22482252

22492253
t = self.theclass(1, 2, 3, 4, 5, 1, 999500)
22502254
self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05:01.999")
@@ -2264,6 +2268,8 @@ def test_isoformat(self):
22642268
tz = FixedOffset(timedelta(seconds=16), 'XXX')
22652269
t = self.theclass(2, 3, 2, tzinfo=tz)
22662270
self.assertEqual(t.isoformat(), "0002-03-02T00:00:00+00:00:16")
2271+
self.assertEqual(t.isoformat(use_utc_designator=False), "0002-03-02T00:00:00+00:00:16")
2272+
self.assertEqual(t.isoformat(use_utc_designator=True), "0002-03-02T00:00:00+00:00:16")
22672273

22682274
def test_isoformat_timezone(self):
22692275
tzoffsets = [
@@ -3836,6 +3842,74 @@ def test_isoformat_timezone(self):
38363842
with self.subTest(tzi=tzi):
38373843
assert t.isoformat() == exp
38383844

3845+
def test_isoformat_utc_designator(self):
3846+
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456)
3847+
self.assertEqual(t.isoformat(), "12:34:56.123456")
3848+
self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456")
3849+
self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456")
3850+
3851+
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456,
3852+
tzinfo=timezone.utc)
3853+
self.assertEqual(t.isoformat(), "12:34:56.123456+00:00")
3854+
self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00")
3855+
self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z")
3856+
3857+
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456,
3858+
tzinfo=timezone(timedelta(0)))
3859+
self.assertEqual(t.isoformat(), "12:34:56.123456+00:00")
3860+
self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00")
3861+
self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z")
3862+
3863+
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456,
3864+
tzinfo=timezone(timedelta(0), "UTC"))
3865+
self.assertEqual(t.isoformat(), "12:34:56.123456+00:00")
3866+
self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00")
3867+
self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z")
3868+
3869+
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456,
3870+
tzinfo=timezone(timedelta(0), "GMT"))
3871+
self.assertEqual(t.isoformat(), "12:34:56.123456+00:00")
3872+
self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00")
3873+
self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456+00:00")
3874+
3875+
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456,
3876+
tzinfo=timezone(timedelta(hours=5), "UTC"))
3877+
self.assertEqual(t.isoformat(), "12:34:56.123456+05:00")
3878+
self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+05:00")
3879+
self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z")
3880+
3881+
class UnnamedTimezone(tzinfo):
3882+
def utcoffset(self, dt):
3883+
return timedelta(0)
3884+
3885+
def dst(self, dt):
3886+
return timedelta(0)
3887+
3888+
def tzname(self, dt):
3889+
return None
3890+
3891+
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456,
3892+
tzinfo=UnnamedTimezone())
3893+
self.assertEqual(t.isoformat(), "12:34:56.123456+00:00")
3894+
self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00")
3895+
self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456+00:00")
3896+
3897+
class NonStringNamedTimezone(tzinfo):
3898+
def utcoffset(self, dt):
3899+
return timedelta(0)
3900+
3901+
def dst(self, dt):
3902+
return timedelta(0)
3903+
3904+
def tzname(self, dt):
3905+
return 42
3906+
3907+
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456,
3908+
tzinfo=UnnamedTimezone())
3909+
self.assertEqual(t.isoformat(), "12:34:56.123456+00:00")
3910+
self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00")
3911+
self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456+00:00")
3912+
38393913
def test_1653736(self):
38403914
# verify it doesn't accept extra keyword arguments
38413915
t = self.theclass(second=1)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add *use_utc_designator* as an optional parameter to
2+
:meth:`datetime.datetime.isoformat` and :meth:`datetime.time.isoformat`. If
3+
it's set to true, the UTC offset will be formatted as "Z" rather than "+00:00"
4+
if the object is associated with a timezone named exactly ``"UTC"``.

Modules/_datetimemodule.c

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1699,8 +1699,10 @@ format_ctime(PyObject *date, int hours, int minutes, int seconds)
16991699
static PyObject *delta_negative(PyObject *op);
17001700

17011701
/* Add formatted UTC offset string to buf. buf has no more than
1702-
* buflen bytes remaining. The UTC offset is gotten by calling
1703-
* tzinfo.uctoffset(tzinfoarg). If that returns None, \0 is stored into
1702+
* buflen bytes remaining. If use_utc_designator is true,
1703+
* tzinfo.tzname(tzinfoarg) will be called, and if it returns "UTC",
1704+
* only "Z\0" will be added. Otherwise, the UTC offset is gotten by calling
1705+
* tzinfo.utcoffset(tzinfoarg). If that returns None, \0 is stored into
17041706
* *buf, and that's all. Else the returned value is checked for sanity (an
17051707
* integer in range), and if that's OK it's converted to an hours & minutes
17061708
* string of the form
@@ -1710,6 +1712,7 @@ static PyObject *delta_negative(PyObject *op);
17101712
*/
17111713
static int
17121714
format_utcoffset(char *buf, size_t buflen, const char *sep,
1715+
int use_utc_designator,
17131716
PyObject *tzinfo, PyObject *tzinfoarg)
17141717
{
17151718
PyObject *offset;
@@ -1718,6 +1721,20 @@ format_utcoffset(char *buf, size_t buflen, const char *sep,
17181721

17191722
assert(buflen >= 1);
17201723

1724+
if (use_utc_designator) {
1725+
PyObject* name = PyObject_CallMethod(tzinfo, "tzname", "O", tzinfoarg);
1726+
if (name == NULL)
1727+
return -1;
1728+
int tz_is_utc = (PyUnicode_Check(name) &&
1729+
0 == strcmp("UTC", PyUnicode_AsUTF8(name)));
1730+
Py_DECREF(name);
1731+
1732+
if (tz_is_utc) {
1733+
PyOS_snprintf(buf, buflen, "Z");
1734+
return 0;
1735+
}
1736+
}
1737+
17211738
offset = call_utcoffset(tzinfo, tzinfoarg);
17221739
if (offset == NULL)
17231740
return -1;
@@ -4758,7 +4775,8 @@ time_isoformat(PyObject *op, PyObject *args, PyObject *kw)
47584775
{
47594776
char buf[100];
47604777
const char *timespec = NULL;
4761-
static char *keywords[] = {"timespec", NULL};
4778+
int use_utc_designator = 0;
4779+
static char *keywords[] = {"timespec", "use_utc_designator", NULL};
47624780
PyDateTime_Time *self = PyTime_CAST(op);
47634781

47644782
PyObject *result;
@@ -4772,7 +4790,8 @@ time_isoformat(PyObject *op, PyObject *args, PyObject *kw)
47724790
};
47734791
size_t given_spec;
47744792

4775-
if (!PyArg_ParseTupleAndKeywords(args, kw, "|s:isoformat", keywords, &timespec))
4793+
if (!PyArg_ParseTupleAndKeywords(args, kw, "|sp:isoformat", keywords,
4794+
&timespec, &use_utc_designator))
47764795
return NULL;
47774796

47784797
if (timespec == NULL || strcmp(timespec, "auto") == 0) {
@@ -4811,8 +4830,8 @@ time_isoformat(PyObject *op, PyObject *args, PyObject *kw)
48114830
return result;
48124831

48134832
/* We need to append the UTC offset. */
4814-
if (format_utcoffset(buf, sizeof(buf), ":", self->tzinfo,
4815-
Py_None) < 0) {
4833+
if (format_utcoffset(buf, sizeof(buf), ":", use_utc_designator,
4834+
self->tzinfo, Py_None) < 0) {
48164835
Py_DECREF(result);
48174836
return NULL;
48184837
}
@@ -6214,7 +6233,8 @@ datetime_isoformat(PyObject *op, PyObject *args, PyObject *kw)
62146233
{
62156234
int sep = 'T';
62166235
char *timespec = NULL;
6217-
static char *keywords[] = {"sep", "timespec", NULL};
6236+
int use_utc_designator = 0;
6237+
static char *keywords[] = {"sep", "timespec", "use_utc_designator", NULL};
62186238
char buffer[100];
62196239
PyDateTime_DateTime *self = PyDateTime_CAST(op);
62206240

@@ -6229,7 +6249,8 @@ datetime_isoformat(PyObject *op, PyObject *args, PyObject *kw)
62296249
};
62306250
size_t given_spec;
62316251

6232-
if (!PyArg_ParseTupleAndKeywords(args, kw, "|Cs:isoformat", keywords, &sep, &timespec))
6252+
if (!PyArg_ParseTupleAndKeywords(args, kw, "|Csp:isoformat", keywords,
6253+
&sep, &timespec, &use_utc_designator))
62336254
return NULL;
62346255

62356256
if (timespec == NULL || strcmp(timespec, "auto") == 0) {
@@ -6269,7 +6290,8 @@ datetime_isoformat(PyObject *op, PyObject *args, PyObject *kw)
62696290
return result;
62706291

62716292
/* We need to append the UTC offset. */
6272-
if (format_utcoffset(buffer, sizeof(buffer), ":", self->tzinfo, op) < 0) {
6293+
if (format_utcoffset(buffer, sizeof(buffer), ":", use_utc_designator,
6294+
self->tzinfo, (PyObject *)self) < 0) {
62736295
Py_DECREF(result);
62746296
return NULL;
62756297
}

0 commit comments

Comments
 (0)