diff -r e7ed7db521e9 Doc/library/datetime.rst --- a/Doc/library/datetime.rst Fri Dec 18 19:37:02 2015 +0200 +++ b/Doc/library/datetime.rst Fri Jan 01 13:48:34 2016 +0100 @@ -1133,7 +1133,7 @@ ``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, @@ -1154,6 +1154,32 @@ >>> datetime(2002, 12, 25, tzinfo=TZ()).isoformat(' ') '2002-12-25 00:00:00-06:39' + The optional argument *timespec* specifies the number of additional + terms of the time to include (the default is ``'auto'``). + It can be one of the following: + + - ``'auto'``: Append microseconds except if microsecond is 0. + - ``'hours'``: Append the hour of the day to the date. + - ``'minutes'``: Append the hours and minutes. + - ``'seconds'``: Append the hours, minutes and seconds. + - ``'microseconds'``: Append the hours, minutes, seconds and microseconds. + + :exc:`ValueError` will be raised on invalid timespec argument. + + + >>> from datetime import datetime + >>> datetime.now().isoformat(timespec='minutes') + '2002-12-25T00:00' + >>> dt = datetime(2015, 1, 1, 12, 30, 59, 123456) + >>> dt.isoformat(timespec='microseconds') + '2015-01-01T12:30:59.123456' + >>> dt = datetime(2015, 1, 1, 12, 30, 59, 0) + >>> dt.isoformat(timespec='microseconds') + '2015-01-01T12:30:59.000000' + + .. versionadded:: 3.6 + *timespec* argument was added. + .. method:: datetime.__str__() @@ -1402,13 +1428,41 @@ 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 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 + terms of the time to include (the default is ``'auto'``). + It can be one of the following: + + - ``'auto'``: Append microseconds except if microsecond is 0. + - ``'hours'``: Append the hour of the day to the date. + - ``'minutes'``: Append the hours and minutes. + - ``'seconds'``: Append the hours, minutes and seconds. + - ``'microseconds'``: Append the hours, minutes, seconds and microseconds. + + :exc:`ValueError` will be raised on 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=123456) + >>> dt.isoformat(timespec='microseconds') + '12:34:56.123456' + >>> 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 + *timespec* argument was added. + .. method:: time.__str__() diff -r e7ed7db521e9 Lib/datetime.py --- a/Lib/datetime.py Fri Dec 18 19:37:02 2015 +0200 +++ b/Lib/datetime.py Fri Jan 01 13:48:34 2016 +0100 @@ -152,12 +152,21 @@ 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): - # Skip trailing microseconds when us==0. - result = "%02d:%02d:%02d" % (hh, mm, ss) - if us: - result += ".%06d" % us - return result +def _format_time(hh, mm, ss, us, timespec='auto'): + specs = { + 'hours': '{:02d}', + 'minutes': '{:02d}:{:02d}', + 'seconds': '{:02d}:{:02d}:{:02d}', + 'microseconds': '{:02d}:{:02d}:{:02d}.{:06d}' + } + + if timespec == 'auto': + # Skip trailing microseconds when us==0. + timespec = 'microseconds' if us else 'seconds' + + if timespec not in specs: + raise ValueError('Unknown timespec value') + return specs[timespec].format(hh, mm, ss, us) # Correctly substitute for %z and %Z escapes in strftime formats. def _wrap_strftime(object, format, timetuple): @@ -1193,14 +1202,17 @@ 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 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 @@ -1549,7 +1561,7 @@ 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 @@ -1560,10 +1572,14 @@ 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: diff -r e7ed7db521e9 Lib/test/datetimetester.py --- a/Lib/test/datetimetester.py Fri Dec 18 19:37:02 2015 +0200 +++ b/Lib/test/datetimetester.py Fri Jan 01 13:48:34 2016 +0100 @@ -1556,13 +1556,24 @@ 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='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='monkey') # 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) + self.assertEqual(t.isoformat(timespec='auto'), "0001-02-03T04:05:01") + 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 +2333,18 @@ 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='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=0) + 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) diff -r e7ed7db521e9 Modules/_datetimemodule.c --- a/Modules/_datetimemodule.c Fri Dec 18 19:37:02 2015 +0200 +++ b/Modules/_datetimemodule.c Fri Jan 01 13:48:34 2016 +0100 @@ -3612,23 +3612,49 @@ } static PyObject * -time_isoformat(PyDateTime_Time *self, PyObject *unused) +time_isoformat(PyDateTime_Time *self, PyObject *args, PyObject *kw) { char buf[100]; + char *timespec = "auto"; + static char *keywords[] = {"timespec", NULL}; PyObject *result; int us = TIME_GET_MICROSECOND(self); - - 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)); + static char *specs[4][2] = { + {"hours", "%02d"}, + {"minutes", "%02d:%02d"}, + {"seconds", "%02d:%02d:%02d"}, + {"microseconds", "%02d:%02d:%02d.%06d"}, + }; + int spec_found = 0, given_spec, sp; + + if (!PyArg_ParseTupleAndKeywords(args, kw, "|s:isoformat", keywords, ×pec)) + return NULL; + + if (strcmp(timespec, "auto") == 0) { + if (us == 0) { + timespec = "seconds"; + } + else { + timespec = "microseconds"; + } + } + + for (sp = 0; sp < 4; sp++) { + if (strcmp(timespec, specs[sp][0]) == 0) { + spec_found = 1; + given_spec = sp; + } + } + + if (!spec_found) { + 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; @@ -3849,9 +3875,11 @@ 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.[mmmmmm]" + "[+HH:MM].\n" + "The optional argument timespec specifies the number of " + "additional terms of the time to include.\n")}, {"strftime", (PyCFunction)time_strftime, METH_VARARGS | METH_KEYWORDS, PyDoc_STR("format -> strftime() style string.")}, @@ -4488,25 +4516,51 @@ datetime_isoformat(PyDateTime_DateTime *self, PyObject *args, PyObject *kw) { int sep = 'T'; - static char *keywords[] = {"sep", NULL}; + char *timespec = "auto"; + static char *keywords[] = {"sep", "timespec", NULL}; char buffer[100]; - PyObject *result; + PyObject *result = NULL; int us = DATE_GET_MICROSECOND(self); - - if (!PyArg_ParseTupleAndKeywords(args, kw, "|C:isoformat", keywords, &sep)) + static char *specs[4][2] = { + {"hours", "%04d-%02d-%02d%c%02d"}, + {"minutes", "%04d-%02d-%02d%c%02d:%02d"}, + {"seconds", "%04d-%02d-%02d%c%02d:%02d:%02d"}, + {"microseconds", "%04d-%02d-%02d%c%02d:%02d:%02d.%06d"}, + }; + int sp; + char *format; + int givenspec = 0; + + 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 (strcmp(timespec, "auto") == 0) { + if (us == 0) { + timespec = "seconds"; + } + else { + timespec = "microseconds"; + } + } + + for (sp = 0; sp < 4; sp++) { + if (strcmp(timespec, specs[sp][0]) == 0) { + givenspec = 1; + format = specs[sp][1]; + } + } + + if (!givenspec) { + PyErr_Format(PyExc_ValueError, "Unknown timespec value"); + return NULL; + } + else { + result = PyUnicode_FromFormat(format, 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; @@ -5040,9 +5094,11 @@ {"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-DDTHH:MM:SS[.mmmmmm][+HH:MM].\n" "sep is used to separate the year from the time, and " - "defaults to 'T'.")}, + "defaults to 'T'.\n" + "The optional argument timespec specifies the number of " + "additional terms of the time to include.\n")}, {"utcoffset", (PyCFunction)datetime_utcoffset, METH_NOARGS, PyDoc_STR("Return self.tzinfo.utcoffset(self).")},