Commit a2998a63 authored by Alexander Belopolsky's avatar Alexander Belopolsky

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.
parent d07a1cb5
...@@ -1134,7 +1134,7 @@ Instance methods: ...@@ -1134,7 +1134,7 @@ Instance methods:
``self.date().isocalendar()``. ``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, Return a string representing the date and time in ISO 8601 format,
YYYY-MM-DDTHH:MM:SS.mmmmmm or, if :attr:`microsecond` is 0, YYYY-MM-DDTHH:MM:SS.mmmmmm or, if :attr:`microsecond` is 0,
...@@ -1155,6 +1155,37 @@ Instance methods: ...@@ -1155,6 +1155,37 @@ Instance methods:
>>> datetime(2002, 12, 25, tzinfo=TZ()).isoformat(' ') >>> datetime(2002, 12, 25, tzinfo=TZ()).isoformat(' ')
'2002-12-25 00:00:00-06:39' '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__() .. method:: datetime.__str__()
...@@ -1404,13 +1435,46 @@ Instance methods: ...@@ -1404,13 +1435,46 @@ Instance methods:
aware :class:`.time`, without conversion of the time data. 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 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 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 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__() .. method:: time.__str__()
......
...@@ -152,12 +152,26 @@ def _build_struct_time(y, m, d, hh, mm, ss, dstflag): ...@@ -152,12 +152,26 @@ def _build_struct_time(y, m, d, hh, mm, ss, dstflag):
dnum = _days_before_month(y, m) + d dnum = _days_before_month(y, m) + d
return _time.struct_time((y, m, d, hh, mm, ss, wday, dnum, dstflag)) 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'):
# Skip trailing microseconds when us==0. specs = {
result = "%02d:%02d:%02d" % (hh, mm, ss) 'hours': '{:02d}',
if us: 'minutes': '{:02d}:{:02d}',
result += ".%06d" % us 'seconds': '{:02d}:{:02d}:{:02d}',
return result 'milliseconds': '{:02d}:{:02d}:{:02d}.{:03d}',
'microseconds': '{:02d}:{:02d}:{:02d}.{:06d}'
}
if timespec == 'auto':
# Skip trailing microseconds when us==0.
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. # Correctly substitute for %z and %Z escapes in strftime formats.
def _wrap_strftime(object, format, timetuple): def _wrap_strftime(object, format, timetuple):
...@@ -1194,14 +1208,17 @@ class time: ...@@ -1194,14 +1208,17 @@ class time:
s = s[:-1] + ", tzinfo=%r" % self._tzinfo + ")" s = s[:-1] + ", tzinfo=%r" % self._tzinfo + ")"
return s return s
def isoformat(self): def isoformat(self, timespec='auto'):
"""Return the time formatted according to ISO. """Return the time formatted according to ISO.
This is 'HH:MM:SS.mmmmmm+zz:zz', or 'HH:MM:SS+zz:zz' if The full format is 'HH:MM:SS.mmmmmm+zz:zz'. By default, the fractional
self.microsecond == 0. 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, s = _format_time(self._hour, self._minute, self._second,
self._microsecond) self._microsecond, timespec)
tz = self._tzstr() tz = self._tzstr()
if tz: if tz:
s += tz s += tz
...@@ -1550,21 +1567,25 @@ class datetime(date): ...@@ -1550,21 +1567,25 @@ class datetime(date):
self._hour, self._minute, self._second, self._hour, self._minute, self._second,
self._year) self._year)
def isoformat(self, sep='T'): def isoformat(self, sep='T', timespec='auto'):
"""Return the time formatted according to ISO. """Return the time formatted according to ISO.
This is 'YYYY-MM-DD HH:MM:SS.mmmmmm', or 'YYYY-MM-DD HH:MM:SS' if The full format looks like 'YYYY-MM-DD HH:MM:SS.mmmmmm'.
self.microsecond == 0. By default, the fractional part is omitted if self.microsecond == 0.
If self.tzinfo is not None, the UTC offset is also attached, giving 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 Optional argument sep specifies the separator between date and
time, default 'T'. 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) + s = ("%04d-%02d-%02d%c" % (self._year, self._month, self._day, sep) +
_format_time(self._hour, self._minute, self._second, _format_time(self._hour, self._minute, self._second,
self._microsecond)) self._microsecond, timespec))
off = self.utcoffset() off = self.utcoffset()
if off is not None: if off is not None:
if off.days < 0: if off.days < 0:
......
...@@ -1556,13 +1556,32 @@ class TestDateTime(TestDate): ...@@ -1556,13 +1556,32 @@ class TestDateTime(TestDate):
self.assertEqual(dt, dt2) self.assertEqual(dt, dt2)
def test_isoformat(self): def test_isoformat(self):
t = self.theclass(2, 3, 2, 4, 5, 1, 123) t = self.theclass(1, 2, 3, 4, 5, 1, 123)
self.assertEqual(t.isoformat(), "0002-03-02T04:05:01.000123") self.assertEqual(t.isoformat(), "0001-02-03T04:05:01.000123")
self.assertEqual(t.isoformat('T'), "0002-03-02T04:05:01.000123") self.assertEqual(t.isoformat('T'), "0001-02-03T04:05:01.000123")
self.assertEqual(t.isoformat(' '), "0002-03-02 04:05:01.000123") self.assertEqual(t.isoformat(' '), "0001-02-03 04:05:01.000123")
self.assertEqual(t.isoformat('\x00'), "0002-03-02\x0004: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. # 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) t = self.theclass(2, 3, 2)
self.assertEqual(t.isoformat(), "0002-03-02T00:00:00") self.assertEqual(t.isoformat(), "0002-03-02T00:00:00")
...@@ -2322,6 +2341,23 @@ class TestTime(HarmlessMixedComparison, unittest.TestCase): ...@@ -2322,6 +2341,23 @@ class TestTime(HarmlessMixedComparison, unittest.TestCase):
self.assertEqual(t.isoformat(), "00:00:00.100000") self.assertEqual(t.isoformat(), "00:00:00.100000")
self.assertEqual(t.isoformat(), str(t)) 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): def test_1653736(self):
# verify it doesn't accept extra keyword arguments # verify it doesn't accept extra keyword arguments
t = self.theclass(second=1) t = self.theclass(second=1)
......
...@@ -309,6 +309,7 @@ Laura Creighton ...@@ -309,6 +309,7 @@ Laura Creighton
Simon Cross Simon Cross
Felipe Cruz Felipe Cruz
Drew Csillag Drew Csillag
Alessandro Cucci
Joaquin Cuenca Abela Joaquin Cuenca Abela
John Cugini John Cugini
Tom Culliton Tom Culliton
......
...@@ -201,6 +201,9 @@ Core and Builtins ...@@ -201,6 +201,9 @@ Core and Builtins
Library 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 - Issue #2202: Fix UnboundLocalError in
AbstractDigestAuthHandler.get_algorithm_impls. Initial patch by Mathieu Dupuy. AbstractDigestAuthHandler.get_algorithm_impls. Initial patch by Mathieu Dupuy.
......
...@@ -3608,23 +3608,56 @@ time_str(PyDateTime_Time *self) ...@@ -3608,23 +3608,56 @@ time_str(PyDateTime_Time *self)
} }
static PyObject * static PyObject *
time_isoformat(PyDateTime_Time *self, PyObject *unused) time_isoformat(PyDateTime_Time *self, PyObject *args, PyObject *kw)
{ {
char buf[100]; char buf[100];
char *timespec = NULL;
static char *keywords[] = {"timespec", NULL};
PyObject *result; PyObject *result;
int us = TIME_GET_MICROSECOND(self); 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) if (!PyArg_ParseTupleAndKeywords(args, kw, "|s:isoformat", keywords, &timespec))
result = PyUnicode_FromFormat("%02d:%02d:%02d.%06d", return NULL;
TIME_GET_HOUR(self),
TIME_GET_MINUTE(self), if (timespec == NULL || strcmp(timespec, "auto") == 0) {
TIME_GET_SECOND(self), if (us == 0) {
us); /* seconds */
else given_spec = 2;
result = PyUnicode_FromFormat("%02d:%02d:%02d", }
TIME_GET_HOUR(self), else {
TIME_GET_MINUTE(self), /* microseconds */
TIME_GET_SECOND(self)); 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) if (result == NULL || !HASTZINFO(self) || self->tzinfo == Py_None)
return result; return result;
...@@ -3845,9 +3878,10 @@ time_reduce(PyDateTime_Time *self, PyObject *arg) ...@@ -3845,9 +3878,10 @@ time_reduce(PyDateTime_Time *self, PyObject *arg)
static PyMethodDef time_methods[] = { static PyMethodDef time_methods[] = {
{"isoformat", (PyCFunction)time_isoformat, METH_NOARGS, {"isoformat", (PyCFunction)time_isoformat, METH_VARARGS | METH_KEYWORDS,
PyDoc_STR("Return string in ISO 8601 format, HH:MM:SS[.mmmmmm]" PyDoc_STR("Return string in ISO 8601 format, [HH[:MM[:SS[.mmm[uuu]]]]]"
"[+HH:MM].")}, "[+HH:MM].\n\n"
"timespec specifies what components of the time to include.\n")},
{"strftime", (PyCFunction)time_strftime, METH_VARARGS | METH_KEYWORDS, {"strftime", (PyCFunction)time_strftime, METH_VARARGS | METH_KEYWORDS,
PyDoc_STR("format -> strftime() style string.")}, PyDoc_STR("format -> strftime() style string.")},
...@@ -4476,25 +4510,55 @@ static PyObject * ...@@ -4476,25 +4510,55 @@ static PyObject *
datetime_isoformat(PyDateTime_DateTime *self, PyObject *args, PyObject *kw) datetime_isoformat(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
{ {
int sep = 'T'; int sep = 'T';
static char *keywords[] = {"sep", NULL}; char *timespec = NULL;
static char *keywords[] = {"sep", "timespec", NULL};
char buffer[100]; char buffer[100];
PyObject *result; PyObject *result = NULL;
int us = DATE_GET_MICROSECOND(self); 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; 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_YEAR(self), GET_MONTH(self),
GET_DAY(self), (int)sep, GET_DAY(self), (int)sep,
DATE_GET_HOUR(self), DATE_GET_MINUTE(self), DATE_GET_HOUR(self), DATE_GET_MINUTE(self),
DATE_GET_SECOND(self), us); 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)) if (!result || !HASTZINFO(self))
return result; return result;
...@@ -5028,9 +5092,12 @@ static PyMethodDef datetime_methods[] = { ...@@ -5028,9 +5092,12 @@ static PyMethodDef datetime_methods[] = {
{"isoformat", (PyCFunction)datetime_isoformat, METH_VARARGS | METH_KEYWORDS, {"isoformat", (PyCFunction)datetime_isoformat, METH_VARARGS | METH_KEYWORDS,
PyDoc_STR("[sep] -> string in ISO 8601 format, " 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 " "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, {"utcoffset", (PyCFunction)datetime_utcoffset, METH_NOARGS,
PyDoc_STR("Return self.tzinfo.utcoffset(self).")}, PyDoc_STR("Return self.tzinfo.utcoffset(self).")},
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment