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.
This commit is contained in:
Alexander Belopolsky 2016-03-06 14:58:43 -05:00
parent d07a1cb53b
commit a2998a63c8
6 changed files with 244 additions and 52 deletions

View File

@ -1134,7 +1134,7 @@ Instance methods:
``self.date().isocalendar()``.
.. method:: datetime.isoformat(sep='T')
.. method:: datetime.isoformat(sep='T', timespec='auto')
Return a string representing the date and time in ISO 8601 format,
YYYY-MM-DDTHH:MM:SS.mmmmmm or, if :attr:`microsecond` is 0,
@ -1155,6 +1155,37 @@ Instance methods:
>>> datetime(2002, 12, 25, tzinfo=TZ()).isoformat(' ')
'2002-12-25 00:00:00-06:39'
The optional argument *timespec* specifies the number of additional
components of the time to include (the default is ``'auto'``).
It can be one of the following:
- ``'auto'``: Same as ``'seconds'`` if :attr:`microsecond` is 0,
same as ``'microseconds'`` otherwise.
- ``'hours'``: Include the :attr:`hour` in the two-digit HH format.
- ``'minutes'``: Include :attr:`hour` and :attr:`minute` in HH:MM format.
- ``'seconds'``: Include :attr:`hour`, :attr:`minute`, and :attr:`second`
in HH:MM:SS format.
- ``'milliseconds'``: Include full time, but truncate fractional second
part to milliseconds. HH:MM:SS.sss format.
- ``'microseconds'``: Include full time in HH:MM:SS.mmmmmm format.
.. note::
Excluded time components are truncated, not rounded.
:exc:`ValueError` will be raised on an invalid *timespec* argument.
>>> from datetime import datetime
>>> datetime.now().isoformat(timespec='minutes')
'2002-12-25T00:00'
>>> dt = datetime(2015, 1, 1, 12, 30, 59, 0)
>>> dt.isoformat(timespec='microseconds')
'2015-01-01T12:30:59.000000'
.. versionadded:: 3.6
Added the *timespec* argument.
.. method:: datetime.__str__()
@ -1404,13 +1435,46 @@ Instance methods:
aware :class:`.time`, without conversion of the time data.
.. method:: time.isoformat()
.. method:: time.isoformat(timespec='auto')
Return a string representing the time in ISO 8601 format, HH:MM:SS.mmmmmm or, if
self.microsecond is 0, HH:MM:SS If :meth:`utcoffset` does not return ``None``, a
:attr:`microsecond` is 0, HH:MM:SS If :meth:`utcoffset` does not return ``None``, a
6-character string is appended, giving the UTC offset in (signed) hours and
minutes: HH:MM:SS.mmmmmm+HH:MM or, if self.microsecond is 0, HH:MM:SS+HH:MM
The optional argument *timespec* specifies the number of additional
components of the time to include (the default is ``'auto'``).
It can be one of the following:
- ``'auto'``: Same as ``'seconds'`` if :attr:`microsecond` is 0,
same as ``'microseconds'`` otherwise.
- ``'hours'``: Include the :attr:`hour` in the two-digit HH format.
- ``'minutes'``: Include :attr:`hour` and :attr:`minute` in HH:MM format.
- ``'seconds'``: Include :attr:`hour`, :attr:`minute`, and :attr:`second`
in HH:MM:SS format.
- ``'milliseconds'``: Include full time, but truncate fractional second
part to milliseconds. HH:MM:SS.sss format.
- ``'microseconds'``: Include full time in HH:MM:SS.mmmmmm format.
.. note::
Excluded time components are truncated, not rounded.
:exc:`ValueError` will be raised on an invalid *timespec* argument.
>>> from datetime import time
>>> time(hours=12, minute=34, second=56, microsecond=123456).isoformat(timespec='minutes')
'12:34'
>>> dt = time(hours=12, minute=34, second=56, microsecond=0)
>>> dt.isoformat(timespec='microseconds')
'12:34:56.000000'
>>> dt.isoformat(timespec='auto')
'12:34:56'
.. versionadded:: 3.6
Added the *timespec* argument.
.. method:: time.__str__()

View File

@ -152,12 +152,26 @@ def _build_struct_time(y, m, d, hh, mm, ss, dstflag):
dnum = _days_before_month(y, m) + d
return _time.struct_time((y, m, d, hh, mm, ss, wday, dnum, dstflag))
def _format_time(hh, mm, ss, us):
def _format_time(hh, mm, ss, us, timespec='auto'):
specs = {
'hours': '{:02d}',
'minutes': '{:02d}:{:02d}',
'seconds': '{:02d}:{:02d}:{:02d}',
'milliseconds': '{:02d}:{:02d}:{:02d}.{:03d}',
'microseconds': '{:02d}:{:02d}:{:02d}.{:06d}'
}
if timespec == 'auto':
# Skip trailing microseconds when us==0.
result = "%02d:%02d:%02d" % (hh, mm, ss)
if us:
result += ".%06d" % us
return result
timespec = 'microseconds' if us else 'seconds'
elif timespec == 'milliseconds':
us //= 1000
try:
fmt = specs[timespec]
except KeyError:
raise ValueError('Unknown timespec value')
else:
return fmt.format(hh, mm, ss, us)
# Correctly substitute for %z and %Z escapes in strftime formats.
def _wrap_strftime(object, format, timetuple):
@ -1194,14 +1208,17 @@ class time:
s = s[:-1] + ", tzinfo=%r" % self._tzinfo + ")"
return s
def isoformat(self):
def isoformat(self, timespec='auto'):
"""Return the time formatted according to ISO.
This is 'HH:MM:SS.mmmmmm+zz:zz', or 'HH:MM:SS+zz:zz' if
self.microsecond == 0.
The full format is 'HH:MM:SS.mmmmmm+zz:zz'. By default, the fractional
part is omitted if self.microsecond == 0.
The optional argument timespec specifies the number of additional
terms of the time to include.
"""
s = _format_time(self._hour, self._minute, self._second,
self._microsecond)
self._microsecond, timespec)
tz = self._tzstr()
if tz:
s += tz
@ -1550,21 +1567,25 @@ class datetime(date):
self._hour, self._minute, self._second,
self._year)
def isoformat(self, sep='T'):
def isoformat(self, sep='T', timespec='auto'):
"""Return the time formatted according to ISO.
This is 'YYYY-MM-DD HH:MM:SS.mmmmmm', or 'YYYY-MM-DD HH:MM:SS' if
self.microsecond == 0.
The full format looks like 'YYYY-MM-DD HH:MM:SS.mmmmmm'.
By default, the fractional part is omitted if self.microsecond == 0.
If self.tzinfo is not None, the UTC offset is also attached, giving
'YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM' or 'YYYY-MM-DD HH:MM:SS+HH:MM'.
giving a full format of 'YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM'.
Optional argument sep specifies the separator between date and
time, default 'T'.
The optional argument timespec specifies the number of additional
terms of the time to include.
"""
s = ("%04d-%02d-%02d%c" % (self._year, self._month, self._day, sep) +
_format_time(self._hour, self._minute, self._second,
self._microsecond))
self._microsecond, timespec))
off = self.utcoffset()
if off is not None:
if off.days < 0:

View File

@ -1556,13 +1556,32 @@ class TestDateTime(TestDate):
self.assertEqual(dt, dt2)
def test_isoformat(self):
t = self.theclass(2, 3, 2, 4, 5, 1, 123)
self.assertEqual(t.isoformat(), "0002-03-02T04:05:01.000123")
self.assertEqual(t.isoformat('T'), "0002-03-02T04:05:01.000123")
self.assertEqual(t.isoformat(' '), "0002-03-02 04:05:01.000123")
self.assertEqual(t.isoformat('\x00'), "0002-03-02\x0004:05:01.000123")
t = self.theclass(1, 2, 3, 4, 5, 1, 123)
self.assertEqual(t.isoformat(), "0001-02-03T04:05:01.000123")
self.assertEqual(t.isoformat('T'), "0001-02-03T04:05:01.000123")
self.assertEqual(t.isoformat(' '), "0001-02-03 04:05:01.000123")
self.assertEqual(t.isoformat('\x00'), "0001-02-03\x0004:05:01.000123")
self.assertEqual(t.isoformat(timespec='hours'), "0001-02-03T04")
self.assertEqual(t.isoformat(timespec='minutes'), "0001-02-03T04:05")
self.assertEqual(t.isoformat(timespec='seconds'), "0001-02-03T04:05:01")
self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05:01.000")
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.assertRaises(ValueError, t.isoformat, timespec='foo')
# str is ISO format with the separator forced to a blank.
self.assertEqual(str(t), "0002-03-02 04:05:01.000123")
self.assertEqual(str(t), "0001-02-03 04:05:01.000123")
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")
t = self.theclass(1, 2, 3, 4, 5, 1, 999500)
self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05:01.999")
t = self.theclass(1, 2, 3, 4, 5, 1)
self.assertEqual(t.isoformat(timespec='auto'), "0001-02-03T04:05:01")
self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05:01.000")
self.assertEqual(t.isoformat(timespec='microseconds'), "0001-02-03T04:05:01.000000")
t = self.theclass(2, 3, 2)
self.assertEqual(t.isoformat(), "0002-03-02T00:00:00")
@ -2322,6 +2341,23 @@ class TestTime(HarmlessMixedComparison, unittest.TestCase):
self.assertEqual(t.isoformat(), "00:00:00.100000")
self.assertEqual(t.isoformat(), str(t))
t = self.theclass(hour=12, minute=34, second=56, microsecond=123456)
self.assertEqual(t.isoformat(timespec='hours'), "12")
self.assertEqual(t.isoformat(timespec='minutes'), "12:34")
self.assertEqual(t.isoformat(timespec='seconds'), "12:34:56")
self.assertEqual(t.isoformat(timespec='milliseconds'), "12:34:56.123")
self.assertEqual(t.isoformat(timespec='microseconds'), "12:34:56.123456")
self.assertEqual(t.isoformat(timespec='auto'), "12:34:56.123456")
self.assertRaises(ValueError, t.isoformat, timespec='monkey')
t = self.theclass(hour=12, minute=34, second=56, microsecond=999500)
self.assertEqual(t.isoformat(timespec='milliseconds'), "12:34:56.999")
t = self.theclass(hour=12, minute=34, second=56, microsecond=0)
self.assertEqual(t.isoformat(timespec='milliseconds'), "12:34:56.000")
self.assertEqual(t.isoformat(timespec='microseconds'), "12:34:56.000000")
self.assertEqual(t.isoformat(timespec='auto'), "12:34:56")
def test_1653736(self):
# verify it doesn't accept extra keyword arguments
t = self.theclass(second=1)

View File

@ -309,6 +309,7 @@ Laura Creighton
Simon Cross
Felipe Cruz
Drew Csillag
Alessandro Cucci
Joaquin Cuenca Abela
John Cugini
Tom Culliton

View File

@ -201,6 +201,9 @@ Core and Builtins
Library
-------
- Issue #19475: Added an optional argument timespec to the datetime
isoformat() method to choose the precision of the time component.
- Issue #2202: Fix UnboundLocalError in
AbstractDigestAuthHandler.get_algorithm_impls. Initial patch by Mathieu Dupuy.

View File

@ -3608,23 +3608,56 @@ time_str(PyDateTime_Time *self)
}
static PyObject *
time_isoformat(PyDateTime_Time *self, PyObject *unused)
time_isoformat(PyDateTime_Time *self, PyObject *args, PyObject *kw)
{
char buf[100];
char *timespec = NULL;
static char *keywords[] = {"timespec", NULL};
PyObject *result;
int us = TIME_GET_MICROSECOND(self);
static char *specs[][2] = {
{"hours", "%02d"},
{"minutes", "%02d:%02d"},
{"seconds", "%02d:%02d:%02d"},
{"milliseconds", "%02d:%02d:%02d.%03d"},
{"microseconds", "%02d:%02d:%02d.%06d"},
};
size_t given_spec;
if (us)
result = PyUnicode_FromFormat("%02d:%02d:%02d.%06d",
TIME_GET_HOUR(self),
TIME_GET_MINUTE(self),
TIME_GET_SECOND(self),
us);
else
result = PyUnicode_FromFormat("%02d:%02d:%02d",
TIME_GET_HOUR(self),
TIME_GET_MINUTE(self),
TIME_GET_SECOND(self));
if (!PyArg_ParseTupleAndKeywords(args, kw, "|s:isoformat", keywords, &timespec))
return NULL;
if (timespec == NULL || strcmp(timespec, "auto") == 0) {
if (us == 0) {
/* seconds */
given_spec = 2;
}
else {
/* microseconds */
given_spec = 4;
}
}
else {
for (given_spec = 0; given_spec < Py_ARRAY_LENGTH(specs); given_spec++) {
if (strcmp(timespec, specs[given_spec][0]) == 0) {
if (given_spec == 3) {
/* milliseconds */
us = us / 1000;
}
break;
}
}
}
if (given_spec == Py_ARRAY_LENGTH(specs)) {
PyErr_Format(PyExc_ValueError, "Unknown timespec value");
return NULL;
}
else {
result = PyUnicode_FromFormat(specs[given_spec][1],
TIME_GET_HOUR(self), TIME_GET_MINUTE(self),
TIME_GET_SECOND(self), us);
}
if (result == NULL || !HASTZINFO(self) || self->tzinfo == Py_None)
return result;
@ -3845,9 +3878,10 @@ time_reduce(PyDateTime_Time *self, PyObject *arg)
static PyMethodDef time_methods[] = {
{"isoformat", (PyCFunction)time_isoformat, METH_NOARGS,
PyDoc_STR("Return string in ISO 8601 format, HH:MM:SS[.mmmmmm]"
"[+HH:MM].")},
{"isoformat", (PyCFunction)time_isoformat, METH_VARARGS | METH_KEYWORDS,
PyDoc_STR("Return string in ISO 8601 format, [HH[:MM[:SS[.mmm[uuu]]]]]"
"[+HH:MM].\n\n"
"timespec specifies what components of the time to include.\n")},
{"strftime", (PyCFunction)time_strftime, METH_VARARGS | METH_KEYWORDS,
PyDoc_STR("format -> strftime() style string.")},
@ -4476,25 +4510,55 @@ static PyObject *
datetime_isoformat(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
{
int sep = 'T';
static char *keywords[] = {"sep", NULL};
char *timespec = NULL;
static char *keywords[] = {"sep", "timespec", NULL};
char buffer[100];
PyObject *result;
PyObject *result = NULL;
int us = DATE_GET_MICROSECOND(self);
static char *specs[][2] = {
{"hours", "%04d-%02d-%02d%c%02d"},
{"minutes", "%04d-%02d-%02d%c%02d:%02d"},
{"seconds", "%04d-%02d-%02d%c%02d:%02d:%02d"},
{"milliseconds", "%04d-%02d-%02d%c%02d:%02d:%02d.%03d"},
{"microseconds", "%04d-%02d-%02d%c%02d:%02d:%02d.%06d"},
};
size_t given_spec;
if (!PyArg_ParseTupleAndKeywords(args, kw, "|C:isoformat", keywords, &sep))
if (!PyArg_ParseTupleAndKeywords(args, kw, "|Cs:isoformat", keywords, &sep, &timespec))
return NULL;
if (us)
result = PyUnicode_FromFormat("%04d-%02d-%02d%c%02d:%02d:%02d.%06d",
if (timespec == NULL || strcmp(timespec, "auto") == 0) {
if (us == 0) {
/* seconds */
given_spec = 2;
}
else {
/* microseconds */
given_spec = 4;
}
}
else {
for (given_spec = 0; given_spec < Py_ARRAY_LENGTH(specs); given_spec++) {
if (strcmp(timespec, specs[given_spec][0]) == 0) {
if (given_spec == 3) {
us = us / 1000;
}
break;
}
}
}
if (given_spec == Py_ARRAY_LENGTH(specs)) {
PyErr_Format(PyExc_ValueError, "Unknown timespec value");
return NULL;
}
else {
result = PyUnicode_FromFormat(specs[given_spec][1],
GET_YEAR(self), GET_MONTH(self),
GET_DAY(self), (int)sep,
DATE_GET_HOUR(self), DATE_GET_MINUTE(self),
DATE_GET_SECOND(self), us);
else
result = PyUnicode_FromFormat("%04d-%02d-%02d%c%02d:%02d:%02d",
GET_YEAR(self), GET_MONTH(self),
GET_DAY(self), (int)sep,
DATE_GET_HOUR(self), DATE_GET_MINUTE(self),
DATE_GET_SECOND(self));
}
if (!result || !HASTZINFO(self))
return result;
@ -5028,9 +5092,12 @@ static PyMethodDef datetime_methods[] = {
{"isoformat", (PyCFunction)datetime_isoformat, METH_VARARGS | METH_KEYWORDS,
PyDoc_STR("[sep] -> string in ISO 8601 format, "
"YYYY-MM-DDTHH:MM:SS[.mmmmmm][+HH:MM].\n\n"
"YYYY-MM-DDT[HH[:MM[:SS[.mmm[uuu]]]]][+HH:MM].\n"
"sep is used to separate the year from the time, and "
"defaults to 'T'.")},
"defaults to 'T'.\n"
"timespec specifies what components of the time to include"
" (allowed values are 'auto', 'hours', 'minutes', 'seconds',"
" 'milliseconds', and 'microseconds').\n")},
{"utcoffset", (PyCFunction)datetime_utcoffset, METH_NOARGS,
PyDoc_STR("Return self.tzinfo.utcoffset(self).")},