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.
diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst
index a822842..d73e97b 100644
--- a/Doc/library/datetime.rst
+++ b/Doc/library/datetime.rst
@@ -1134,7 +1134,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,
@@ -1155,6 +1155,37 @@
       >>> 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 @@
    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__()
 
diff --git a/Lib/datetime.py b/Lib/datetime.py
index 3206923..d4b7fc4 100644
--- a/Lib/datetime.py
+++ b/Lib/datetime.py
@@ -152,12 +152,26 @@
     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}',
+        '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.
 def _wrap_strftime(object, format, timetuple):
@@ -1194,14 +1208,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 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 @@
             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:
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index 68c18bd..b49942a 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -1556,13 +1556,32 @@
             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 @@
         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)
diff --git a/Misc/ACKS b/Misc/ACKS
index 5e6fee7..adc6848 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -309,6 +309,7 @@
 Simon Cross
 Felipe Cruz
 Drew Csillag
+Alessandro Cucci
 Joaquin Cuenca Abela
 John Cugini
 Tom Culliton
diff --git a/Misc/NEWS b/Misc/NEWS
index 9c94db3..d90422d 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -201,6 +201,9 @@
 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.
 
diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c
index 4c8f3f6..347a9d9 100644
--- a/Modules/_datetimemodule.c
+++ b/Modules/_datetimemodule.c
@@ -3608,23 +3608,56 @@
 }
 
 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 @@
 
 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 @@
 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 @@
 
     {"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).")},