diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 48e7080da6c525..9bd8a9c60d4c3d 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -791,13 +791,25 @@ Instance methods: .. versionchanged:: 3.9 Result changed from a tuple to a :term:`named tuple`. -.. method:: date.isoformat() - Return a string representing the date in ISO 8601 format, ``YYYY-MM-DD``:: +.. method:: date.isoformat(basic=False) + + Return a string representing the date in: + + - ISO 8601 extended format ``YYYY-MM-DD`` (the default), or + - ISO 8601 basic format ``YYYYMMDD`` via the *basic* argument. + + Examples: >>> from datetime import date >>> date(2002, 12, 4).isoformat() '2002-12-04' + >>> date(2002, 12, 4).isoformat(basic=True) + '20021204' + + .. versionchanged:: next + Added the *basic* parameter. + .. method:: date.__str__() @@ -1566,9 +1578,9 @@ Instance methods: and ``weekday``. The same as ``self.date().isocalendar()``. -.. method:: datetime.isoformat(sep='T', timespec='auto') +.. method:: datetime.isoformat(sep='T', timespec='auto', basic=False) - Return a string representing the date and time in ISO 8601 format: + Return a string representing the date and time in ISO 8601 extended format: - ``YYYY-MM-DDTHH:MM:SS.ffffff``, if :attr:`microsecond` is not 0 - ``YYYY-MM-DDTHH:MM:SS``, if :attr:`microsecond` is 0 @@ -1580,13 +1592,20 @@ Instance methods: is not 0 - ``YYYY-MM-DDTHH:MM:SS+HH:MM[:SS[.ffffff]]``, if :attr:`microsecond` is 0 + If *basic* is true, this uses the ISO 8601 basic format for the date, + time and offset components. + Examples:: >>> from datetime import datetime, timezone >>> datetime(2019, 5, 18, 15, 17, 8, 132263).isoformat() '2019-05-18T15:17:08.132263' + >>> datetime(2019, 5, 18, 15, 17, 8, 132263).isoformat(basic=True) + '20190518T151708.132263' >>> datetime(2019, 5, 18, 15, 17, tzinfo=timezone.utc).isoformat() '2019-05-18T15:17:00+00:00' + >>> datetime(2019, 5, 18, 15, 17, tzinfo=timezone.utc).isoformat(basic=True) + '20190518T151700+0000' The optional argument *sep* (default ``'T'``) is a one-character separator, placed between the date and time portions of the result. For example:: @@ -1633,6 +1652,9 @@ Instance methods: .. versionchanged:: 3.6 Added the *timespec* parameter. + .. versionadded:: next + Added the *basic* parameter. + .. method:: datetime.__str__() @@ -1984,15 +2006,17 @@ Instance methods: Added the *fold* parameter. -.. method:: time.isoformat(timespec='auto') +.. method:: time.isoformat(timespec='auto', basic=False) - Return a string representing the time in ISO 8601 format, one of: + Return a string representing the time in ISO 8601 (extended) format, one of: - ``HH:MM:SS.ffffff``, if :attr:`microsecond` is not 0 - ``HH:MM:SS``, if :attr:`microsecond` is 0 - ``HH:MM:SS.ffffff+HH:MM[:SS[.ffffff]]``, if :meth:`utcoffset` does not return ``None`` - ``HH:MM:SS+HH:MM[:SS[.ffffff]]``, if :attr:`microsecond` is 0 and :meth:`utcoffset` does not return ``None`` + If *basic* is true, this uses the ISO 8601 basic format which omits the colons. + 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: @@ -2027,6 +2051,9 @@ Instance methods: .. versionchanged:: 3.6 Added the *timespec* parameter. + .. versionchanged:: next + Added the *basic* parameter. + .. method:: time.__str__() diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 11f08031ec54f2..a2c94669b9bd61 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -485,6 +485,14 @@ dataclasses type names. +datetime +-------- + +* Add the :meth:`~datetime.date.strptime` method to the + :class:`datetime.date` and :class:`datetime.time` classes. + (Contributed by Wannes Boeykens in :gh:`41431`.) + + dbm --- diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index e625bf2fef1912..cae7c7fcd7e206 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1584,6 +1584,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(autocommit)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(backtick)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(base)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(basic)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(before)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(big)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(binary_form)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 771f0f8cb4ad87..4f3415659ee629 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -307,6 +307,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(autocommit) STRUCT_FOR_ID(backtick) STRUCT_FOR_ID(base) + STRUCT_FOR_ID(basic) STRUCT_FOR_ID(before) STRUCT_FOR_ID(big) STRUCT_FOR_ID(binary_form) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 499a2569b9a06c..76b0ab7bf8897c 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1582,6 +1582,7 @@ extern "C" { INIT_ID(autocommit), \ INIT_ID(backtick), \ INIT_ID(base), \ + INIT_ID(basic), \ INIT_ID(before), \ INIT_ID(big), \ INIT_ID(binary_form), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 1375f46018f943..c3316fcda91455 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -1008,6 +1008,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(basic); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(before); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index b6d68f2372850a..07b9f792051323 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -163,14 +163,23 @@ 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, timespec='auto'): - specs = { - 'hours': '{:02d}', - 'minutes': '{:02d}:{:02d}', - 'seconds': '{:02d}:{:02d}:{:02d}', - 'milliseconds': '{:02d}:{:02d}:{:02d}.{:03d}', - 'microseconds': '{:02d}:{:02d}:{:02d}.{:06d}' - } +def _format_time(hh, mm, ss, us, timespec='auto', basic=False): + if basic: + specs = { + 'hours': '{:02d}', + 'minutes': '{:02d}{:02d}', + 'seconds': '{:02d}{:02d}{:02d}', + 'milliseconds': '{:02d}{:02d}{:02d}.{:03d}', + 'microseconds': '{:02d}{:02d}{:02d}.{:06d}' + } + else: + 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. @@ -1125,16 +1134,18 @@ def __format__(self, fmt): return self.strftime(fmt) return str(self) - def isoformat(self): - """Return the date formatted according to ISO. + def isoformat(self, basic=False): + """Return the date formatted according to ISO 8601. - This is 'YYYY-MM-DD'. + This is 'YYYY-MM-DD' or 'YYYYMMDD' if *basic* is true. References: - https://www.w3.org/TR/NOTE-datetime - https://www.cl.cam.ac.uk/~mgk25/iso-time.html """ - return "%04d-%02d-%02d" % (self._year, self._month, self._day) + if basic: + return f"{self._year:04d}{self._month:02d}{self._day:02d}" + return f"{self._year:04d}-{self._month:02d}-{self._day:02d}" __str__ = isoformat @@ -1587,10 +1598,13 @@ def __hash__(self): # Conversion to string - def _tzstr(self): - """Return formatted timezone offset (+xx:xx) or an empty string.""" + def _tzstr(self, basic): + """Return formatted timezone offset (+xx:xx) or an empty string. + The colon separator is omitted if *basic* is true. + """ off = self.utcoffset() - return _format_offset(off) + sep = '' if basic else ':' + return _format_offset(off, sep) def __repr__(self): """Convert to formal string, for repr().""" @@ -1611,19 +1625,21 @@ def __repr__(self): s = s[:-1] + ", fold=1)" return s - def isoformat(self, timespec='auto'): + def isoformat(self, timespec='auto', basic=False): """Return the time formatted according to ISO. The full format is 'HH:MM:SS.mmmmmm+zz:zz'. By default, the fractional part is omitted if self.microsecond == 0. + If *basic* is true, separators ':' are omitted. + The optional argument timespec specifies the number of additional terms of the time to include. Valid options are 'auto', 'hours', 'minutes', 'seconds', 'milliseconds' and 'microseconds'. """ s = _format_time(self._hour, self._minute, self._second, - self._microsecond, timespec) - tz = self._tzstr() + self._microsecond, timespec, basic) + tz = self._tzstr(basic) if tz: s += tz return s @@ -2150,6 +2166,14 @@ def astimezone(self, tz=None): # Ways to produce a string. + def _tzstr(self, basic): + """Return formatted timezone offset (+xx:xx) or an empty string. + The colon separator is omitted if *basic* is true. + """ + off = self.utcoffset() + sep = '' if basic else ':' + return _format_offset(off, sep) + def ctime(self): "Return ctime() style string." weekday = self.toordinal() % 7 or 7 @@ -2160,12 +2184,14 @@ def ctime(self): self._hour, self._minute, self._second, self._year) - def isoformat(self, sep='T', timespec='auto'): + def isoformat(self, sep='T', timespec='auto', basic=False): """Return the time formatted according to ISO. The full format looks like 'YYYY-MM-DD HH:MM:SS.mmmmmm'. By default, the fractional part is omitted if self.microsecond == 0. + If *basic* is true, separators ':' and '-' are omitted. + If self.tzinfo is not None, the UTC offset is also attached, giving a full format of 'YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM'. @@ -2176,12 +2202,12 @@ def isoformat(self, sep='T', timespec='auto'): terms of the time to include. Valid options are 'auto', 'hours', 'minutes', 'seconds', 'milliseconds' and 'microseconds'. """ - s = ("%04d-%02d-%02d%c" % (self._year, self._month, self._day, sep) + + fmt = "%04d%02d%02d%c" if basic else "%04d-%02d-%02d%c" + s = (fmt % (self._year, self._month, self._day, sep) + _format_time(self._hour, self._minute, self._second, - self._microsecond, timespec)) + self._microsecond, timespec, basic)) - off = self.utcoffset() - tz = _format_offset(off) + tz = self._tzstr(basic) if tz: s += tz diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index ace56aab7aceba..56702705991ff0 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1578,6 +1578,7 @@ def test_iso_long_years(self): def test_isoformat(self): t = self.theclass(2, 3, 2) self.assertEqual(t.isoformat(), "0002-03-02") + self.assertEqual(t.isoformat(basic=True), "00020302") def test_ctime(self): t = self.theclass(2002, 3, 2) @@ -2245,76 +2246,117 @@ def test_roundtrip(self): def test_isoformat(self): t = self.theclass(1, 2, 3, 4, 5, 1, 123) self.assertEqual(t.isoformat(), "0001-02-03T04:05:01.000123") + self.assertEqual(t.isoformat(basic=True), "00010203T040501.000123") + self.assertEqual(t.isoformat('T'), "0001-02-03T04:05:01.000123") + self.assertEqual(t.isoformat('T', basic=True), "00010203T040501.000123") + self.assertEqual(t.isoformat(' '), "0001-02-03 04:05:01.000123") + self.assertEqual(t.isoformat(' ', basic=True), "00010203 040501.000123") + self.assertEqual(t.isoformat('\x00'), "0001-02-03\x0004:05:01.000123") + self.assertEqual(t.isoformat('\x00', basic=True), "00010203\x00040501.000123") + # bpo-34482: Check that surrogates are handled properly. - self.assertEqual(t.isoformat('\ud800'), - "0001-02-03\ud80004:05:01.000123") + self.assertEqual(t.isoformat('\ud800'), "0001-02-03\ud80004:05:01.000123") + self.assertEqual(t.isoformat('\ud800', basic=True), "00010203\ud800040501.000123") + self.assertEqual(t.isoformat(timespec='hours'), "0001-02-03T04") + self.assertEqual(t.isoformat(timespec='hours', basic=True), "00010203T04") + self.assertEqual(t.isoformat(timespec='minutes'), "0001-02-03T04:05") + self.assertEqual(t.isoformat(timespec='minutes', basic=True), "00010203T0405") + self.assertEqual(t.isoformat(timespec='seconds'), "0001-02-03T04:05:01") + self.assertEqual(t.isoformat(timespec='seconds', basic=True), "00010203T040501") + self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05:01.000") + self.assertEqual(t.isoformat(timespec='milliseconds', basic=True), "00010203T040501.000") + self.assertEqual(t.isoformat(timespec='microseconds'), "0001-02-03T04:05:01.000123") + self.assertEqual(t.isoformat(timespec='microseconds', basic=True), "00010203T040501.000123") + self.assertEqual(t.isoformat(timespec='auto'), "0001-02-03T04:05:01.000123") + self.assertEqual(t.isoformat(timespec='auto', basic=True), "00010203T040501.000123") + self.assertEqual(t.isoformat(sep=' ', timespec='minutes'), "0001-02-03 04:05") + self.assertEqual(t.isoformat(sep=' ', timespec='minutes', basic=True), "00010203 0405") + self.assertRaises(ValueError, t.isoformat, timespec='foo') + self.assertRaises(ValueError, t.isoformat, timespec='foo', basic=True) # bpo-34482: Check that surrogates are handled properly. self.assertRaises(ValueError, t.isoformat, timespec='\ud800') + self.assertRaises(ValueError, t.isoformat, timespec='\ud800', basic=True) # str is ISO format with the separator forced to a blank. 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") + self.assertEqual(t.isoformat(timespec='milliseconds', basic=True), "00010203T040501.999+0000") t = self.theclass(1, 2, 3, 4, 5, 1, 999500) self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05:01.999") + self.assertEqual(t.isoformat(timespec='milliseconds', basic=True), "00010203T040501.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='auto', basic=True), "00010203T040501") self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05:01.000") + self.assertEqual(t.isoformat(timespec='milliseconds', basic=True), "00010203T040501.000") self.assertEqual(t.isoformat(timespec='microseconds'), "0001-02-03T04:05:01.000000") + self.assertEqual(t.isoformat(timespec='microseconds', basic=True), "00010203T040501.000000") t = self.theclass(2, 3, 2) self.assertEqual(t.isoformat(), "0002-03-02T00:00:00") + self.assertEqual(t.isoformat(basic=True), "00020302T000000") self.assertEqual(t.isoformat('T'), "0002-03-02T00:00:00") + self.assertEqual(t.isoformat('T', basic=True), "00020302T000000") self.assertEqual(t.isoformat(' '), "0002-03-02 00:00:00") + self.assertEqual(t.isoformat(' ', basic=True), "00020302 000000") # str is ISO format with the separator forced to a blank. self.assertEqual(str(t), "0002-03-02 00:00:00") # ISO format with timezone tz = FixedOffset(timedelta(seconds=16), 'XXX') t = self.theclass(2, 3, 2, tzinfo=tz) self.assertEqual(t.isoformat(), "0002-03-02T00:00:00+00:00:16") + self.assertEqual(t.isoformat(basic=True), "00020302T000000+000016") def test_isoformat_timezone(self): tzoffsets = [ - ('05:00', timedelta(hours=5)), - ('02:00', timedelta(hours=2)), - ('06:27', timedelta(hours=6, minutes=27)), - ('12:32:30', timedelta(hours=12, minutes=32, seconds=30)), - ('02:04:09.123456', timedelta(hours=2, minutes=4, seconds=9, microseconds=123456)) + (('05:00', '0500'), timedelta(hours=5)), + (('02:00', '0200'), timedelta(hours=2)), + (('06:27', '0627'), timedelta(hours=6, minutes=27)), + (('12:32:30', '123230'), timedelta(hours=12, minutes=32, seconds=30)), + (('02:04:09.123456', '020409.123456'), + timedelta(hours=2, minutes=4, seconds=9, microseconds=123456)) ] tzinfos = [ - ('', None), - ('+00:00', timezone.utc), - ('+00:00', timezone(timedelta(0))), + (('', ''), None), + (('+00:00', '+0000'), timezone.utc), + (('+00:00', '+0000'), timezone(timedelta(0))), ] tzinfos += [ - (prefix + expected, timezone(sign * td)) - for expected, td in tzoffsets + ((prefix + expected_extended, prefix + expected_basic), timezone(sign * td)) + for (expected_extended, expected_basic), td in tzoffsets for prefix, sign in [('-', -1), ('+', 1)] ] dt_base = self.theclass(2016, 4, 1, 12, 37, 9) - exp_base = '2016-04-01T12:37:09' + exp_base_ext = '2016-04-01T12:37:09' + exp_base_basic = '20160401T123709' - for exp_tz, tzi in tzinfos: + for (exp_tz_ext, exp_tz_basic), tzi in tzinfos: dt = dt_base.replace(tzinfo=tzi) - exp = exp_base + exp_tz - with self.subTest(tzi=tzi): + with self.subTest(tzi=tzi, basic=False): + exp = exp_base_ext + exp_tz_ext self.assertEqual(dt.isoformat(), exp) + self.assertEqual(dt.isoformat(basic=False), exp) + + with self.subTest(tzi=tzi, basic=True): + exp = exp_base_basic + exp_tz_basic + self.assertEqual(dt.isoformat(basic=True), exp) def test_format(self): dt = self.theclass(2007, 9, 10, 4, 5, 1, 123) @@ -4568,10 +4610,15 @@ def test_zones(self): self.assertEqual(str(t5), "00:00:00.000040+00:00") self.assertEqual(t1.isoformat(), "07:47:00-05:00") + self.assertEqual(t1.isoformat(basic=True), "074700-0500") self.assertEqual(t2.isoformat(), "12:47:00+00:00") + self.assertEqual(t2.isoformat(basic=True), "124700+0000") self.assertEqual(t3.isoformat(), "13:47:00+01:00") + self.assertEqual(t3.isoformat(basic=True), "134700+0100") self.assertEqual(t4.isoformat(), "00:00:00.000040") + self.assertEqual(t4.isoformat(basic=True), "000000.000040") self.assertEqual(t5.isoformat(), "00:00:00.000040+00:00") + self.assertEqual(t5.isoformat(basic=True), "000000.000040+0000") d = 'datetime.time' self.assertEqual(repr(t1), d + "(7, 47, tzinfo=est)") @@ -5502,25 +5549,71 @@ def utcoffset(self, dt): self.assertRaises(OverflowError, huge.utctimetuple) def test_tzinfo_isoformat(self): + offsets = [ + (("+00:00", "+0000"), 0), + (("+03:40", "+0340"), 220), + (("-03:51", "-0351"), -231), + (("", ""), None), + ] + zero = FixedOffset(0, "+00:00") plus = FixedOffset(220, "+03:40") minus = FixedOffset(-231, "-03:51") unknown = FixedOffset(None, "") cls = self.theclass - datestr = '0001-02-03' + datestr_ext = '0001-02-03' + datestr_basic = '00010203' + for (name_ext, name_basic), value in offsets: + for us in 0, 987001: + timestr_suffix = (us and '.987001' or '') + + offset_ext = FixedOffset(value, name_ext) + d = cls(1, 2, 3, 4, 5, 59, us, tzinfo=offset_ext) + ofsstr_ext = offset_ext is not None and d.tzname() or '' + timestr_ext = '04:05:59' + timestr_suffix + tailstr_ext = timestr_ext + ofsstr_ext + + iso = d.isoformat() + self.assertEqual(iso, d.isoformat(basic=False)) + self.assertEqual(iso, datestr_ext + 'T' + tailstr_ext) + self.assertEqual(iso, d.isoformat('T')) + self.assertEqual(d.isoformat('k'), datestr_ext + 'k' + tailstr_ext) + self.assertEqual(d.isoformat('\u1234'), datestr_ext + '\u1234' + tailstr_ext) + self.assertEqual(str(d), datestr_ext + ' ' + tailstr_ext) + + offset_basic = FixedOffset(value, name_basic) + d = cls(1, 2, 3, 4, 5, 59, us, tzinfo=offset_basic) + ofsstr_basic = offset_basic is not None and d.tzname() or '' + timestr_basic = '040559' + timestr_suffix + tailstr_basic = timestr_basic + ofsstr_basic + + iso = d.isoformat(basic=True) + self.assertEqual(iso, datestr_basic + 'T' + tailstr_basic) + self.assertEqual(iso, d.isoformat('T', basic=True)) + self.assertEqual(d.isoformat('k', basic=True), datestr_basic + 'k' + tailstr_basic) + self.assertEqual(d.isoformat('\u1234', basic=True), datestr_basic + '\u1234' + tailstr_basic) + + def test_tzinfo_isoformat_basic(self): + zero = FixedOffset(0, "+0000") + plus = FixedOffset(220, "+0340") + minus = FixedOffset(-231, "-0351") + unknown = FixedOffset(None, "") + + cls = self.theclass + datestr = '00010203' for ofs in None, zero, plus, minus, unknown: for us in 0, 987001: d = cls(1, 2, 3, 4, 5, 59, us, tzinfo=ofs) - timestr = '04:05:59' + (us and '.987001' or '') + timestr_suffix = us and '.987001' or '' + timestr = '040559' + timestr_suffix ofsstr = ofs is not None and d.tzname() or '' tailstr = timestr + ofsstr - iso = d.isoformat() + iso = d.isoformat(basic=True) self.assertEqual(iso, datestr + 'T' + tailstr) - self.assertEqual(iso, d.isoformat('T')) - self.assertEqual(d.isoformat('k'), datestr + 'k' + tailstr) - self.assertEqual(d.isoformat('\u1234'), datestr + '\u1234' + tailstr) - self.assertEqual(str(d), datestr + ' ' + tailstr) + self.assertEqual(iso, d.isoformat('T', basic=True)) + self.assertEqual(d.isoformat('k', basic=True), datestr + 'k' + tailstr) + self.assertEqual(d.isoformat('\u1234', basic=True), datestr + '\u1234' + tailstr) def test_replace(self): cls = self.theclass diff --git a/Misc/NEWS.d/next/Library/2024-06-15-13-08-56.gh-issue-118948.s5aW4U.rst b/Misc/NEWS.d/next/Library/2024-06-15-13-08-56.gh-issue-118948.s5aW4U.rst new file mode 100644 index 00000000000000..a041261309c828 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-06-15-13-08-56.gh-issue-118948.s5aW4U.rst @@ -0,0 +1,4 @@ +The :meth:`date.isoformat `, +:meth:`datetime.isoformat ` and +:meth:`time.isoformat ` methods now +support ISO 8601 basic format. Patch by Bénédikt Tran. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 46c4f57984b0df..4ba8b6a49771d3 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3595,12 +3595,22 @@ date_repr(PyObject *op) GET_YEAR(self), GET_MONTH(self), GET_DAY(self)); } +/*[clinic input] +datetime.date.isoformat + + basic: bool = False + +Return string in ISO 8601 format, YYYY-MM-DD. + +If basic is true, uses the basic format, YYYYMMDD. +[clinic start generated code]*/ + static PyObject * -date_isoformat(PyObject *op, PyObject *Py_UNUSED(dummy)) +datetime_date_isoformat_impl(PyDateTime_Date *self, int basic) +/*[clinic end generated code: output=c458fbf6d05e16f2 input=1bd448614fd107d0]*/ { - PyDateTime_Date *self = PyDate_CAST(op); - return PyUnicode_FromFormat("%04d-%02d-%02d", - GET_YEAR(self), GET_MONTH(self), GET_DAY(self)); + const char *format = basic ? "%04d%02d%02d" : "%04d-%02d-%02d"; + return PyUnicode_FromFormat(format, GET_YEAR(self), GET_MONTH(self), GET_DAY(self)); } /* str() calls the appropriate isoformat() method. */ @@ -3992,8 +4002,7 @@ static PyMethodDef date_methods[] = { PyDoc_STR("Return a named tuple containing ISO year, week number, and " "weekday.")}, - {"isoformat", date_isoformat, METH_NOARGS, - PyDoc_STR("Return string in ISO 8601 format, YYYY-MM-DD.")}, + DATETIME_DATE_ISOFORMAT_METHODDEF {"isoweekday", date_isoweekday, METH_NOARGS, PyDoc_STR("Return the day of the week represented by the date.\n" @@ -4835,11 +4844,13 @@ time_str(PyObject *op) datetime.time.isoformat timespec: str(c_default="NULL") = 'auto' + basic: bool = False Return the time formatted according to ISO. -The full format is 'HH:MM:SS.mmmmmm+zz:zz'. By default, the fractional -part is omitted if self.microsecond == 0. +The full format is 'HH:MM:SS.mmmmmm+zz:zz'. If basic is true, +separators ':' are removed from the output (e.g., HHMMSS). +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. Valid options are 'auto', 'hours', @@ -4847,20 +4858,32 @@ terms of the time to include. Valid options are 'auto', 'hours', [clinic start generated code]*/ static PyObject * -datetime_time_isoformat_impl(PyDateTime_Time *self, const char *timespec) -/*[clinic end generated code: output=2bcc7cab65c35545 input=afbbbd953d10ad07]*/ +datetime_time_isoformat_impl(PyDateTime_Time *self, const char *timespec, + int basic) +/*[clinic end generated code: output=3eafefc852100d3c input=8d6445ce8d611c40]*/ { char buf[100]; PyObject *result; int us = TIME_GET_MICROSECOND(self); - static const char * const specs[][2] = { + static const char *const specs_extended[][2] = { {"hours", "%02d"}, {"minutes", "%02d:%02d"}, {"seconds", "%02d:%02d:%02d"}, {"milliseconds", "%02d:%02d:%02d.%03d"}, {"microseconds", "%02d:%02d:%02d.%06d"}, }; + static const char *const specs_basic[][2] = { + {"hours", "%02d"}, + {"minutes", "%02d%02d"}, + {"seconds", "%02d%02d%02d"}, + {"milliseconds", "%02d%02d%02d.%03d"}, + {"microseconds", "%02d%02d%02d.%06d"}, + }; + + const char *const (*specs)[2] = basic ? specs_basic : specs_extended; + // due to array decaying, Py_ARRAY_LENGTH(specs) would return 0 + size_t specs_count = basic ? Py_ARRAY_LENGTH(specs_basic) : Py_ARRAY_LENGTH(specs_extended); size_t given_spec; if (timespec == NULL || strcmp(timespec, "auto") == 0) { @@ -4874,7 +4897,7 @@ datetime_time_isoformat_impl(PyDateTime_Time *self, const char *timespec) } } else { - for (given_spec = 0; given_spec < Py_ARRAY_LENGTH(specs); given_spec++) { + for (given_spec = 0; given_spec < specs_count; given_spec++) { if (strcmp(timespec, specs[given_spec][0]) == 0) { if (given_spec == 3) { /* milliseconds */ @@ -4885,7 +4908,7 @@ datetime_time_isoformat_impl(PyDateTime_Time *self, const char *timespec) } } - if (given_spec == Py_ARRAY_LENGTH(specs)) { + if (given_spec == specs_count) { PyErr_Format(PyExc_ValueError, "Unknown timespec value"); return NULL; } @@ -4899,8 +4922,8 @@ datetime_time_isoformat_impl(PyDateTime_Time *self, const char *timespec) return result; /* We need to append the UTC offset. */ - if (format_utcoffset(buf, sizeof(buf), ":", self->tzinfo, - Py_None) < 0) { + const char *offset_sep = basic ? "" : ":"; + if (format_utcoffset(buf, sizeof(buf), offset_sep, self->tzinfo, Py_None) < 0) { Py_DECREF(result); return NULL; } @@ -6395,6 +6418,7 @@ datetime.datetime.isoformat sep: int(accept={str}, c_default="'T'", py_default="'T'") = ord('T') timespec: str(c_default="NULL") = 'auto' + basic: bool = False Return the time formatted according to ISO. @@ -6405,7 +6429,8 @@ If self.tzinfo is not None, the UTC offset is also attached, 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'. +time, default 'T'. If basic is true, separators ':' and '-' are +removed from the output (e.g., YYYYMMDDTHHMMSS). The optional argument timespec specifies the number of additional terms of the time to include. Valid options are 'auto', 'hours', @@ -6414,20 +6439,31 @@ terms of the time to include. Valid options are 'auto', 'hours', static PyObject * datetime_datetime_isoformat_impl(PyDateTime_DateTime *self, int sep, - const char *timespec) -/*[clinic end generated code: output=9b6ce1383189b0bf input=2fa2512172ccf5d5]*/ + const char *timespec, int basic) +/*[clinic end generated code: output=9df35f63fda85c52 input=e7936e3183ce301c]*/ { char buffer[100]; PyObject *result = NULL; int us = DATE_GET_MICROSECOND(self); - static const char * const specs[][2] = { + static const char *const specs_extended[][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"}, }; + static const char *const specs_basic[][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"}, + }; + + const char *const(*specs)[2] = basic ? specs_basic : specs_extended; + // due to array decaying, Py_ARRAY_LENGTH(specs) would return 0 + size_t specs_count = basic ? Py_ARRAY_LENGTH(specs_basic) : Py_ARRAY_LENGTH(specs_extended); size_t given_spec; if (timespec == NULL || strcmp(timespec, "auto") == 0) { @@ -6441,7 +6477,7 @@ datetime_datetime_isoformat_impl(PyDateTime_DateTime *self, int sep, } } else { - for (given_spec = 0; given_spec < Py_ARRAY_LENGTH(specs); given_spec++) { + for (given_spec = 0; given_spec < specs_count; given_spec++) { if (strcmp(timespec, specs[given_spec][0]) == 0) { if (given_spec == 3) { us = us / 1000; @@ -6451,7 +6487,7 @@ datetime_datetime_isoformat_impl(PyDateTime_DateTime *self, int sep, } } - if (given_spec == Py_ARRAY_LENGTH(specs)) { + if (given_spec == specs_count) { PyErr_Format(PyExc_ValueError, "Unknown timespec value"); return NULL; } @@ -6467,7 +6503,8 @@ datetime_datetime_isoformat_impl(PyDateTime_DateTime *self, int sep, return result; /* We need to append the UTC offset. */ - if (format_utcoffset(buffer, sizeof(buffer), ":", self->tzinfo, (PyObject *)self) < 0) { + const char *offset_sep = basic ? "" : ":"; + if (format_utcoffset(buffer, sizeof(buffer), offset_sep, self->tzinfo, (PyObject *)self) < 0) { Py_DECREF(result); return NULL; } diff --git a/Modules/clinic/_datetimemodule.c.h b/Modules/clinic/_datetimemodule.c.h index ee621c150c31e4..b2bcb0ebe2c4d9 100644 --- a/Modules/clinic/_datetimemodule.c.h +++ b/Modules/clinic/_datetimemodule.c.h @@ -409,6 +409,74 @@ datetime_date_strptime(PyObject *type, PyObject *const *args, Py_ssize_t nargs) return return_value; } +PyDoc_STRVAR(datetime_date_isoformat__doc__, +"isoformat($self, /, basic=False)\n" +"--\n" +"\n" +"Return string in ISO 8601 format, YYYY-MM-DD.\n" +"\n" +"If basic is true, uses the basic format, YYYYMMDD."); + +#define DATETIME_DATE_ISOFORMAT_METHODDEF \ + {"isoformat", _PyCFunction_CAST(datetime_date_isoformat), METH_FASTCALL|METH_KEYWORDS, datetime_date_isoformat__doc__}, + +static PyObject * +datetime_date_isoformat_impl(PyDateTime_Date *self, int basic); + +static PyObject * +datetime_date_isoformat(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(basic), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"basic", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "isoformat", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[1]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0; + int basic = 0; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, + /*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + if (!noptargs) { + goto skip_optional_pos; + } + basic = PyObject_IsTrue(args[0]); + if (basic < 0) { + goto exit; + } +skip_optional_pos: + return_value = datetime_date_isoformat_impl((PyDateTime_Date *)self, basic); + +exit: + return return_value; +} + PyDoc_STRVAR(datetime_date_strftime__doc__, "strftime($self, /, format)\n" "--\n" @@ -892,13 +960,14 @@ datetime_time_strptime(PyObject *type, PyObject *const *args, Py_ssize_t nargs) } PyDoc_STRVAR(datetime_time_isoformat__doc__, -"isoformat($self, /, timespec=\'auto\')\n" +"isoformat($self, /, timespec=\'auto\', basic=False)\n" "--\n" "\n" "Return the time formatted according to ISO.\n" "\n" -"The full format is \'HH:MM:SS.mmmmmm+zz:zz\'. By default, the fractional\n" -"part is omitted if self.microsecond == 0.\n" +"The full format is \'HH:MM:SS.mmmmmm+zz:zz\'. If basic is true,\n" +"separators \':\' are removed from the output (e.g., HHMMSS).\n" +"By default, the fractional part is omitted if self.microsecond == 0.\n" "\n" "The optional argument timespec specifies the number of additional\n" "terms of the time to include. Valid options are \'auto\', \'hours\',\n" @@ -908,7 +977,8 @@ PyDoc_STRVAR(datetime_time_isoformat__doc__, {"isoformat", _PyCFunction_CAST(datetime_time_isoformat), METH_FASTCALL|METH_KEYWORDS, datetime_time_isoformat__doc__}, static PyObject * -datetime_time_isoformat_impl(PyDateTime_Time *self, const char *timespec); +datetime_time_isoformat_impl(PyDateTime_Time *self, const char *timespec, + int basic); static PyObject * datetime_time_isoformat(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) @@ -916,7 +986,7 @@ datetime_time_isoformat(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - #define NUM_KEYWORDS 1 + #define NUM_KEYWORDS 2 static struct { PyGC_Head _this_is_not_used; PyObject_VAR_HEAD @@ -925,7 +995,7 @@ datetime_time_isoformat(PyObject *self, PyObject *const *args, Py_ssize_t nargs, } _kwtuple = { .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) .ob_hash = -1, - .ob_item = { &_Py_ID(timespec), }, + .ob_item = { &_Py_ID(timespec), &_Py_ID(basic), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -934,40 +1004,50 @@ datetime_time_isoformat(PyObject *self, PyObject *const *args, Py_ssize_t nargs, # define KWTUPLE NULL #endif // !Py_BUILD_CORE - static const char * const _keywords[] = {"timespec", NULL}; + static const char * const _keywords[] = {"timespec", "basic", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, .fname = "isoformat", .kwtuple = KWTUPLE, }; #undef KWTUPLE - PyObject *argsbuf[1]; + PyObject *argsbuf[2]; Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0; const char *timespec = NULL; + int basic = 0; args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, - /*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + /*minpos*/ 0, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf); if (!args) { goto exit; } if (!noptargs) { goto skip_optional_pos; } - if (!PyUnicode_Check(args[0])) { - _PyArg_BadArgument("isoformat", "argument 'timespec'", "str", args[0]); - goto exit; - } - Py_ssize_t timespec_length; - timespec = PyUnicode_AsUTF8AndSize(args[0], ×pec_length); - if (timespec == NULL) { - goto exit; + if (args[0]) { + if (!PyUnicode_Check(args[0])) { + _PyArg_BadArgument("isoformat", "argument 'timespec'", "str", args[0]); + goto exit; + } + Py_ssize_t timespec_length; + timespec = PyUnicode_AsUTF8AndSize(args[0], ×pec_length); + if (timespec == NULL) { + goto exit; + } + if (strlen(timespec) != (size_t)timespec_length) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + goto exit; + } + if (!--noptargs) { + goto skip_optional_pos; + } } - if (strlen(timespec) != (size_t)timespec_length) { - PyErr_SetString(PyExc_ValueError, "embedded null character"); + basic = PyObject_IsTrue(args[1]); + if (basic < 0) { goto exit; } skip_optional_pos: - return_value = datetime_time_isoformat_impl((PyDateTime_Time *)self, timespec); + return_value = datetime_time_isoformat_impl((PyDateTime_Time *)self, timespec, basic); exit: return return_value; @@ -1725,7 +1805,7 @@ datetime_datetime_fromisoformat(PyObject *type, PyObject *arg) } PyDoc_STRVAR(datetime_datetime_isoformat__doc__, -"isoformat($self, /, sep=\'T\', timespec=\'auto\')\n" +"isoformat($self, /, sep=\'T\', timespec=\'auto\', basic=False)\n" "--\n" "\n" "Return the time formatted according to ISO.\n" @@ -1737,7 +1817,8 @@ PyDoc_STRVAR(datetime_datetime_isoformat__doc__, "a full format of \'YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM\'.\n" "\n" "Optional argument sep specifies the separator between date and\n" -"time, default \'T\'.\n" +"time, default \'T\'. If basic is true, separators \':\' and \'-\' are\n" +"removed from the output (e.g., YYYYMMDDTHHMMSS).\n" "\n" "The optional argument timespec specifies the number of additional\n" "terms of the time to include. Valid options are \'auto\', \'hours\',\n" @@ -1748,7 +1829,7 @@ PyDoc_STRVAR(datetime_datetime_isoformat__doc__, static PyObject * datetime_datetime_isoformat_impl(PyDateTime_DateTime *self, int sep, - const char *timespec); + const char *timespec, int basic); static PyObject * datetime_datetime_isoformat(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) @@ -1756,7 +1837,7 @@ datetime_datetime_isoformat(PyObject *self, PyObject *const *args, Py_ssize_t na PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - #define NUM_KEYWORDS 2 + #define NUM_KEYWORDS 3 static struct { PyGC_Head _this_is_not_used; PyObject_VAR_HEAD @@ -1765,7 +1846,7 @@ datetime_datetime_isoformat(PyObject *self, PyObject *const *args, Py_ssize_t na } _kwtuple = { .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) .ob_hash = -1, - .ob_item = { &_Py_ID(sep), &_Py_ID(timespec), }, + .ob_item = { &_Py_ID(sep), &_Py_ID(timespec), &_Py_ID(basic), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -1774,20 +1855,21 @@ datetime_datetime_isoformat(PyObject *self, PyObject *const *args, Py_ssize_t na # define KWTUPLE NULL #endif // !Py_BUILD_CORE - static const char * const _keywords[] = {"sep", "timespec", NULL}; + static const char * const _keywords[] = {"sep", "timespec", "basic", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, .fname = "isoformat", .kwtuple = KWTUPLE, }; #undef KWTUPLE - PyObject *argsbuf[2]; + PyObject *argsbuf[3]; Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0; int sep = 'T'; const char *timespec = NULL; + int basic = 0; args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, - /*minpos*/ 0, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + /*minpos*/ 0, /*maxpos*/ 3, /*minkw*/ 0, /*varpos*/ 0, argsbuf); if (!args) { goto exit; } @@ -1811,21 +1893,30 @@ datetime_datetime_isoformat(PyObject *self, PyObject *const *args, Py_ssize_t na goto skip_optional_pos; } } - if (!PyUnicode_Check(args[1])) { - _PyArg_BadArgument("isoformat", "argument 'timespec'", "str", args[1]); - goto exit; - } - Py_ssize_t timespec_length; - timespec = PyUnicode_AsUTF8AndSize(args[1], ×pec_length); - if (timespec == NULL) { - goto exit; + if (args[1]) { + if (!PyUnicode_Check(args[1])) { + _PyArg_BadArgument("isoformat", "argument 'timespec'", "str", args[1]); + goto exit; + } + Py_ssize_t timespec_length; + timespec = PyUnicode_AsUTF8AndSize(args[1], ×pec_length); + if (timespec == NULL) { + goto exit; + } + if (strlen(timespec) != (size_t)timespec_length) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + goto exit; + } + if (!--noptargs) { + goto skip_optional_pos; + } } - if (strlen(timespec) != (size_t)timespec_length) { - PyErr_SetString(PyExc_ValueError, "embedded null character"); + basic = PyObject_IsTrue(args[2]); + if (basic < 0) { goto exit; } skip_optional_pos: - return_value = datetime_datetime_isoformat_impl((PyDateTime_DateTime *)self, sep, timespec); + return_value = datetime_datetime_isoformat_impl((PyDateTime_DateTime *)self, sep, timespec, basic); exit: return return_value; @@ -2090,4 +2181,4 @@ datetime_datetime___reduce__(PyObject *self, PyObject *Py_UNUSED(ignored)) { return datetime_datetime___reduce___impl((PyDateTime_DateTime *)self); } -/*[clinic end generated code: output=69658acff6a43ac4 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=a961d9d6e858f52d input=a9049054013a1b77]*/ diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index adb183000deeff..d64befb3d93d86 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -227,9 +227,11 @@ Modules/_ctypes/cfield.c - ffi_type_uint8 - Modules/_ctypes/cfield.c - ffi_type_void - Modules/_datetimemodule.c - epoch - Modules/_datetimemodule.c - max_fold_seconds - -Modules/_datetimemodule.c datetime_isoformat specs - +Modules/_datetimemodule.c datetime_isoformat specs_basic - +Modules/_datetimemodule.c datetime_isoformat specs_extended - Modules/_datetimemodule.c parse_hh_mm_ss_ff correction - -Modules/_datetimemodule.c time_isoformat specs - +Modules/_datetimemodule.c time_isoformat specs_basic - +Modules/_datetimemodule.c time_isoformat specs_extended - Modules/_datetimemodule.c - capi_types - Modules/_datetimemodule.c normalize_century cache - Modules/_decimal/_decimal.c - cond_map_template -