Skip to content

Commit a2998a6

Browse files
committed
Closes #19475: Added timespec to the datetime.isoformat() method.
Added an optional argument timespec to the datetime isoformat() method to choose the precision of the time component. Original patch by Alessandro Cucci.
1 parent d07a1cb commit a2998a6

6 files changed

Lines changed: 244 additions & 52 deletions

File tree

Doc/library/datetime.rst

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,7 +1134,7 @@ Instance methods:
11341134
``self.date().isocalendar()``.
11351135

11361136

1137-
.. method:: datetime.isoformat(sep='T')
1137+
.. method:: datetime.isoformat(sep='T', timespec='auto')
11381138

11391139
Return a string representing the date and time in ISO 8601 format,
11401140
YYYY-MM-DDTHH:MM:SS.mmmmmm or, if :attr:`microsecond` is 0,
@@ -1155,6 +1155,37 @@ Instance methods:
11551155
>>> datetime(2002, 12, 25, tzinfo=TZ()).isoformat(' ')
11561156
'2002-12-25 00:00:00-06:39'
11571157

1158+
The optional argument *timespec* specifies the number of additional
1159+
components of the time to include (the default is ``'auto'``).
1160+
It can be one of the following:
1161+
1162+
- ``'auto'``: Same as ``'seconds'`` if :attr:`microsecond` is 0,
1163+
same as ``'microseconds'`` otherwise.
1164+
- ``'hours'``: Include the :attr:`hour` in the two-digit HH format.
1165+
- ``'minutes'``: Include :attr:`hour` and :attr:`minute` in HH:MM format.
1166+
- ``'seconds'``: Include :attr:`hour`, :attr:`minute`, and :attr:`second`
1167+
in HH:MM:SS format.
1168+
- ``'milliseconds'``: Include full time, but truncate fractional second
1169+
part to milliseconds. HH:MM:SS.sss format.
1170+
- ``'microseconds'``: Include full time in HH:MM:SS.mmmmmm format.
1171+
1172+
.. note::
1173+
1174+
Excluded time components are truncated, not rounded.
1175+
1176+
:exc:`ValueError` will be raised on an invalid *timespec* argument.
1177+
1178+
1179+
>>> from datetime import datetime
1180+
>>> datetime.now().isoformat(timespec='minutes')
1181+
'2002-12-25T00:00'
1182+
>>> dt = datetime(2015, 1, 1, 12, 30, 59, 0)
1183+
>>> dt.isoformat(timespec='microseconds')
1184+
'2015-01-01T12:30:59.000000'
1185+
1186+
.. versionadded:: 3.6
1187+
Added the *timespec* argument.
1188+
11581189

11591190
.. method:: datetime.__str__()
11601191

@@ -1404,13 +1435,46 @@ Instance methods:
14041435
aware :class:`.time`, without conversion of the time data.
14051436

14061437

1407-
.. method:: time.isoformat()
1438+
.. method:: time.isoformat(timespec='auto')
14081439

14091440
Return a string representing the time in ISO 8601 format, HH:MM:SS.mmmmmm or, if
1410-
self.microsecond is 0, HH:MM:SS If :meth:`utcoffset` does not return ``None``, a
1441+
:attr:`microsecond` is 0, HH:MM:SS If :meth:`utcoffset` does not return ``None``, a
14111442
6-character string is appended, giving the UTC offset in (signed) hours and
14121443
minutes: HH:MM:SS.mmmmmm+HH:MM or, if self.microsecond is 0, HH:MM:SS+HH:MM
14131444

1445+
The optional argument *timespec* specifies the number of additional
1446+
components of the time to include (the default is ``'auto'``).
1447+
It can be one of the following:
1448+
1449+
- ``'auto'``: Same as ``'seconds'`` if :attr:`microsecond` is 0,
1450+
same as ``'microseconds'`` otherwise.
1451+
- ``'hours'``: Include the :attr:`hour` in the two-digit HH format.
1452+
- ``'minutes'``: Include :attr:`hour` and :attr:`minute` in HH:MM format.
1453+
- ``'seconds'``: Include :attr:`hour`, :attr:`minute`, and :attr:`second`
1454+
in HH:MM:SS format.
1455+
- ``'milliseconds'``: Include full time, but truncate fractional second
1456+
part to milliseconds. HH:MM:SS.sss format.
1457+
- ``'microseconds'``: Include full time in HH:MM:SS.mmmmmm format.
1458+
1459+
.. note::
1460+
1461+
Excluded time components are truncated, not rounded.
1462+
1463+
:exc:`ValueError` will be raised on an invalid *timespec* argument.
1464+
1465+
1466+
>>> from datetime import time
1467+
>>> time(hours=12, minute=34, second=56, microsecond=123456).isoformat(timespec='minutes')
1468+
'12:34'
1469+
>>> dt = time(hours=12, minute=34, second=56, microsecond=0)
1470+
>>> dt.isoformat(timespec='microseconds')
1471+
'12:34:56.000000'
1472+
>>> dt.isoformat(timespec='auto')
1473+
'12:34:56'
1474+
1475+
.. versionadded:: 3.6
1476+
Added the *timespec* argument.
1477+
14141478

14151479
.. method:: time.__str__()
14161480

Lib/datetime.py

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,26 @@ def _build_struct_time(y, m, d, hh, mm, ss, dstflag):
152152
dnum = _days_before_month(y, m) + d
153153
return _time.struct_time((y, m, d, hh, mm, ss, wday, dnum, dstflag))
154154

155-
def _format_time(hh, mm, ss, us):
156-
# Skip trailing microseconds when us==0.
157-
result = "%02d:%02d:%02d" % (hh, mm, ss)
158-
if us:
159-
result += ".%06d" % us
160-
return result
155+
def _format_time(hh, mm, ss, us, timespec='auto'):
156+
specs = {
157+
'hours': '{:02d}',
158+
'minutes': '{:02d}:{:02d}',
159+
'seconds': '{:02d}:{:02d}:{:02d}',
160+
'milliseconds': '{:02d}:{:02d}:{:02d}.{:03d}',
161+
'microseconds': '{:02d}:{:02d}:{:02d}.{:06d}'
162+
}
163+
164+
if timespec == 'auto':
165+
# Skip trailing microseconds when us==0.
166+
timespec = 'microseconds' if us else 'seconds'
167+
elif timespec == 'milliseconds':
168+
us //= 1000
169+
try:
170+
fmt = specs[timespec]
171+
except KeyError:
172+
raise ValueError('Unknown timespec value')
173+
else:
174+
return fmt.format(hh, mm, ss, us)
161175

162176
# Correctly substitute for %z and %Z escapes in strftime formats.
163177
def _wrap_strftime(object, format, timetuple):
@@ -1194,14 +1208,17 @@ def __repr__(self):
11941208
s = s[:-1] + ", tzinfo=%r" % self._tzinfo + ")"
11951209
return s
11961210

1197-
def isoformat(self):
1211+
def isoformat(self, timespec='auto'):
11981212
"""Return the time formatted according to ISO.
11991213
1200-
This is 'HH:MM:SS.mmmmmm+zz:zz', or 'HH:MM:SS+zz:zz' if
1201-
self.microsecond == 0.
1214+
The full format is 'HH:MM:SS.mmmmmm+zz:zz'. By default, the fractional
1215+
part is omitted if self.microsecond == 0.
1216+
1217+
The optional argument timespec specifies the number of additional
1218+
terms of the time to include.
12021219
"""
12031220
s = _format_time(self._hour, self._minute, self._second,
1204-
self._microsecond)
1221+
self._microsecond, timespec)
12051222
tz = self._tzstr()
12061223
if tz:
12071224
s += tz
@@ -1550,21 +1567,25 @@ def ctime(self):
15501567
self._hour, self._minute, self._second,
15511568
self._year)
15521569

1553-
def isoformat(self, sep='T'):
1570+
def isoformat(self, sep='T', timespec='auto'):
15541571
"""Return the time formatted according to ISO.
15551572
1556-
This is 'YYYY-MM-DD HH:MM:SS.mmmmmm', or 'YYYY-MM-DD HH:MM:SS' if
1557-
self.microsecond == 0.
1573+
The full format looks like 'YYYY-MM-DD HH:MM:SS.mmmmmm'.
1574+
By default, the fractional part is omitted if self.microsecond == 0.
15581575
15591576
If self.tzinfo is not None, the UTC offset is also attached, giving
1560-
'YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM' or 'YYYY-MM-DD HH:MM:SS+HH:MM'.
1577+
giving a full format of 'YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM'.
15611578
15621579
Optional argument sep specifies the separator between date and
15631580
time, default 'T'.
1581+
1582+
The optional argument timespec specifies the number of additional
1583+
terms of the time to include.
15641584
"""
15651585
s = ("%04d-%02d-%02d%c" % (self._year, self._month, self._day, sep) +
15661586
_format_time(self._hour, self._minute, self._second,
1567-
self._microsecond))
1587+
self._microsecond, timespec))
1588+
15681589
off = self.utcoffset()
15691590
if off is not None:
15701591
if off.days < 0:

Lib/test/datetimetester.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,13 +1556,32 @@ def test_roundtrip(self):
15561556
self.assertEqual(dt, dt2)
15571557

15581558
def test_isoformat(self):
1559-
t = self.theclass(2, 3, 2, 4, 5, 1, 123)
1560-
self.assertEqual(t.isoformat(), "0002-03-02T04:05:01.000123")
1561-
self.assertEqual(t.isoformat('T'), "0002-03-02T04:05:01.000123")
1562-
self.assertEqual(t.isoformat(' '), "0002-03-02 04:05:01.000123")
1563-
self.assertEqual(t.isoformat('\x00'), "0002-03-02\x0004:05:01.000123")
1559+
t = self.theclass(1, 2, 3, 4, 5, 1, 123)
1560+
self.assertEqual(t.isoformat(), "0001-02-03T04:05:01.000123")
1561+
self.assertEqual(t.isoformat('T'), "0001-02-03T04:05:01.000123")
1562+
self.assertEqual(t.isoformat(' '), "0001-02-03 04:05:01.000123")
1563+
self.assertEqual(t.isoformat('\x00'), "0001-02-03\x0004:05:01.000123")
1564+
self.assertEqual(t.isoformat(timespec='hours'), "0001-02-03T04")
1565+
self.assertEqual(t.isoformat(timespec='minutes'), "0001-02-03T04:05")
1566+
self.assertEqual(t.isoformat(timespec='seconds'), "0001-02-03T04:05:01")
1567+
self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05:01.000")
1568+
self.assertEqual(t.isoformat(timespec='microseconds'), "0001-02-03T04:05:01.000123")
1569+
self.assertEqual(t.isoformat(timespec='auto'), "0001-02-03T04:05:01.000123")
1570+
self.assertEqual(t.isoformat(sep=' ', timespec='minutes'), "0001-02-03 04:05")
1571+
self.assertRaises(ValueError, t.isoformat, timespec='foo')
15641572
# str is ISO format with the separator forced to a blank.
1565-
self.assertEqual(str(t), "0002-03-02 04:05:01.000123")
1573+
self.assertEqual(str(t), "0001-02-03 04:05:01.000123")
1574+
1575+
t = self.theclass(1, 2, 3, 4, 5, 1, 999500, tzinfo=timezone.utc)
1576+
self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05:01.999+00:00")
1577+
1578+
t = self.theclass(1, 2, 3, 4, 5, 1, 999500)
1579+
self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05:01.999")
1580+
1581+
t = self.theclass(1, 2, 3, 4, 5, 1)
1582+
self.assertEqual(t.isoformat(timespec='auto'), "0001-02-03T04:05:01")
1583+
self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05:01.000")
1584+
self.assertEqual(t.isoformat(timespec='microseconds'), "0001-02-03T04:05:01.000000")
15661585

15671586
t = self.theclass(2, 3, 2)
15681587
self.assertEqual(t.isoformat(), "0002-03-02T00:00:00")
@@ -2322,6 +2341,23 @@ def test_isoformat(self):
23222341
self.assertEqual(t.isoformat(), "00:00:00.100000")
23232342
self.assertEqual(t.isoformat(), str(t))
23242343

2344+
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456)
2345+
self.assertEqual(t.isoformat(timespec='hours'), "12")
2346+
self.assertEqual(t.isoformat(timespec='minutes'), "12:34")
2347+
self.assertEqual(t.isoformat(timespec='seconds'), "12:34:56")
2348+
self.assertEqual(t.isoformat(timespec='milliseconds'), "12:34:56.123")
2349+
self.assertEqual(t.isoformat(timespec='microseconds'), "12:34:56.123456")
2350+
self.assertEqual(t.isoformat(timespec='auto'), "12:34:56.123456")
2351+
self.assertRaises(ValueError, t.isoformat, timespec='monkey')
2352+
2353+
t = self.theclass(hour=12, minute=34, second=56, microsecond=999500)
2354+
self.assertEqual(t.isoformat(timespec='milliseconds'), "12:34:56.999")
2355+
2356+
t = self.theclass(hour=12, minute=34, second=56, microsecond=0)
2357+
self.assertEqual(t.isoformat(timespec='milliseconds'), "12:34:56.000")
2358+
self.assertEqual(t.isoformat(timespec='microseconds'), "12:34:56.000000")
2359+
self.assertEqual(t.isoformat(timespec='auto'), "12:34:56")
2360+
23252361
def test_1653736(self):
23262362
# verify it doesn't accept extra keyword arguments
23272363
t = self.theclass(second=1)

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ Laura Creighton
309309
Simon Cross
310310
Felipe Cruz
311311
Drew Csillag
312+
Alessandro Cucci
312313
Joaquin Cuenca Abela
313314
John Cugini
314315
Tom Culliton

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ Core and Builtins
201201
Library
202202
-------
203203

204+
- Issue #19475: Added an optional argument timespec to the datetime
205+
isoformat() method to choose the precision of the time component.
206+
204207
- Issue #2202: Fix UnboundLocalError in
205208
AbstractDigestAuthHandler.get_algorithm_impls. Initial patch by Mathieu Dupuy.
206209

0 commit comments

Comments
 (0)