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:
parent
d07a1cb53b
commit
a2998a63c8
|
@ -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__()
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -309,6 +309,7 @@ Laura Creighton
|
|||
Simon Cross
|
||||
Felipe Cruz
|
||||
Drew Csillag
|
||||
Alessandro Cucci
|
||||
Joaquin Cuenca Abela
|
||||
John Cugini
|
||||
Tom Culliton
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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, ×pec))
|
||||
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, ×pec))
|
||||
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).")},
|
||||
|
|
Loading…
Reference in New Issue