diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 1af7d6be750102..4c40e76884456e 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -772,13 +772,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__() @@ -1536,9 +1548,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 @@ -1550,13 +1562,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:: @@ -1603,6 +1622,9 @@ Instance methods: .. versionchanged:: 3.6 Added the *timespec* parameter. + .. versionadded:: next + Added the *basic* parameter. + .. method:: datetime.__str__() @@ -1954,15 +1976,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: @@ -1997,6 +2021,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.14.rst b/Doc/whatsnew/3.14.rst index 767cf9a1f08dc2..ec2b08fad0ddf0 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -786,6 +786,18 @@ operator (Contributed by Raymond Hettinger and Nico Mexis in :gh:`115808`.) +datetime +-------- + +* Add support for the ISO 8601 basic format for the following methods: + + - :meth:`date.isoformat ` + - :meth:`datetime.isoformat ` + - :meth:`time.isoformat ` + + (Contributed by Bénédikt Tran in :gh:`118948`.) + + os -- diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 50e21a12335611..13e6c55643c867 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. @@ -1117,16 +1126,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: - - http://www.w3.org/TR/NOTE-datetime - - http://www.cl.cam.ac.uk/~mgk25/iso-time.html + - 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 @@ -1574,10 +1585,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().""" @@ -1598,19 +1612,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 @@ -2118,6 +2134,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 @@ -2128,12 +2152,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 giving a full format of 'YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM'. @@ -2144,12 +2170,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 f9d20ef9c626a9..bf56a773200646 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1573,6 +1573,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) @@ -2227,76 +2228,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 assert dt.isoformat() == exp + assert dt.isoformat(basic=False) == exp + + with self.subTest(tzi=tzi, basic=True): + exp = exp_base_basic + exp_tz_basic + assert dt.isoformat(basic=True) == exp def test_format(self): dt = self.theclass(2007, 9, 10, 4, 5, 1, 123) @@ -4399,10 +4441,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)") @@ -5328,25 +5375,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 9bba0e3354b26b..1438c608da7357 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3535,11 +3535,16 @@ date_repr(PyObject *op) } static PyObject * -date_isoformat(PyObject *op, PyObject *Py_UNUSED(dummy)) +date_isoformat(PyObject *op, PyObject *args, PyObject *kw) { + int basic = 0; + static char *keywords[] = {"basic", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kw, "|p:isoformat", keywords, &basic)) { + return NULL; + } + const char *format = basic ? "%04d%02d%02d" : "%04d-%02d-%02d"; PyDateTime_Date *self = PyDate_CAST(op); - return PyUnicode_FromFormat("%04d-%02d-%02d", - GET_YEAR(self), GET_MONTH(self), GET_DAY(self)); + return PyUnicode_FromFormat(format, GET_YEAR(self), GET_MONTH(self), GET_DAY(self)); } /* str() calls the appropriate isoformat() method. */ @@ -3933,8 +3938,9 @@ 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.")}, + {"isoformat", _PyCFunction_CAST(date_isoformat), METH_VARARGS | METH_KEYWORDS, + PyDoc_STR("Return string in ISO 8601 format, YYYY-MM-DD.\n" + "If basic is true, uses the basic format, YYYYMMDD.")}, {"isoweekday", date_isoweekday, METH_NOARGS, PyDoc_STR("Return the day of the week represented by the date.\n" @@ -4759,22 +4765,35 @@ time_isoformat(PyObject *op, PyObject *args, PyObject *kw) { char buf[100]; const char *timespec = NULL; - static char *keywords[] = {"timespec", NULL}; + int basic = 0; + static char *keywords[] = {"timespec", "basic", NULL}; PyDateTime_Time *self = PyTime_CAST(op); PyObject *result; int us = TIME_GET_MICROSECOND(self); - static const char *specs[][2] = { + static const char *specs_extended[][2] = { {"hours", "%02d"}, {"minutes", "%02d:%02d"}, {"seconds", "%02d:%02d:%02d"}, {"milliseconds", "%02d:%02d:%02d.%03d"}, {"microseconds", "%02d:%02d:%02d.%06d"}, }; - size_t given_spec; + static const char *specs_basic[][2] = { + {"hours", "%02d"}, + {"minutes", "%02d%02d"}, + {"seconds", "%02d%02d%02d"}, + {"milliseconds", "%02d%02d%02d.%03d"}, + {"microseconds", "%02d%02d%02d.%06d"}, + }; - if (!PyArg_ParseTupleAndKeywords(args, kw, "|s:isoformat", keywords, ×pec)) + if (!PyArg_ParseTupleAndKeywords(args, kw, "|sp:isoformat", keywords, ×pec, &basic)) { return NULL; + } + + const char *(*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) { if (us == 0) { @@ -4787,7 +4806,7 @@ time_isoformat(PyObject *op, PyObject *args, PyObject *kw) } } 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 */ @@ -4798,7 +4817,7 @@ time_isoformat(PyObject *op, PyObject *args, PyObject *kw) } } - if (given_spec == Py_ARRAY_LENGTH(specs)) { + if (given_spec == specs_count) { PyErr_Format(PyExc_ValueError, "Unknown timespec value"); return NULL; } @@ -4812,8 +4831,8 @@ time_isoformat(PyObject *op, PyObject *args, PyObject *kw) 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; } @@ -5138,6 +5157,8 @@ static PyMethodDef time_methods[] = { {"isoformat", _PyCFunction_CAST(time_isoformat), METH_VARARGS | METH_KEYWORDS, PyDoc_STR("Return string in ISO 8601 format, [HH[:MM[:SS[.mmm[uuu]]]]]" "[+HH:MM].\n\n" + "If basic is true, separators ':' are removed " + "from the output (e.g., HHMMSS).\n" "The optional argument timespec specifies the number " "of additional terms\nof the time to include. Valid " "options are 'auto', 'hours', 'minutes',\n'seconds', " @@ -6215,23 +6236,36 @@ datetime_isoformat(PyObject *op, PyObject *args, PyObject *kw) { int sep = 'T'; char *timespec = NULL; - static char *keywords[] = {"sep", "timespec", NULL}; + int basic = 0; + static char *keywords[] = {"sep", "timespec", "basic", NULL}; char buffer[100]; PyDateTime_DateTime *self = PyDateTime_CAST(op); PyObject *result = NULL; int us = DATE_GET_MICROSECOND(self); - static const char *specs[][2] = { + static const char *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"}, }; - size_t given_spec; + static const char *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"}, + }; - if (!PyArg_ParseTupleAndKeywords(args, kw, "|Cs:isoformat", keywords, &sep, ×pec)) + if (!PyArg_ParseTupleAndKeywords(args, kw, "|Csp:isoformat", keywords, &sep, ×pec, &basic)) { return NULL; + } + + const char *(*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) { if (us == 0) { @@ -6244,7 +6278,7 @@ datetime_isoformat(PyObject *op, PyObject *args, PyObject *kw) } } 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; @@ -6254,7 +6288,7 @@ datetime_isoformat(PyObject *op, PyObject *args, PyObject *kw) } } - if (given_spec == Py_ARRAY_LENGTH(specs)) { + if (given_spec == specs_count) { PyErr_Format(PyExc_ValueError, "Unknown timespec value"); return NULL; } @@ -6270,7 +6304,8 @@ datetime_isoformat(PyObject *op, PyObject *args, PyObject *kw) return result; /* We need to append the UTC offset. */ - if (format_utcoffset(buffer, sizeof(buffer), ":", self->tzinfo, op) < 0) { + const char *offset_sep = basic ? "" : ":"; + if (format_utcoffset(buffer, sizeof(buffer), offset_sep, self->tzinfo, (PyObject *)self) < 0) { Py_DECREF(result); return NULL; } @@ -7047,9 +7082,11 @@ static PyMethodDef datetime_methods[] = { {"isoformat", _PyCFunction_CAST(datetime_isoformat), METH_VARARGS | METH_KEYWORDS, PyDoc_STR("[sep] -> string in ISO 8601 format, " - "YYYY-MM-DDT[HH[:MM[:SS[.mmm[uuu]]]]][+HH:MM].\n" + "YYYY-MM-DDT[HH[:MM[:SS[.mmm[uuu]]]]][+HH:MM].\n\n" "sep is used to separate the year from the time, and " "defaults to 'T'.\n" + "If basic is true, separators ':' and '-' are removed " + "from the output (e.g., YYYYMMDDTHHMMSS).\n" "The optional argument timespec specifies the number " "of additional terms\nof the time to include. Valid " "options are 'auto', 'hours', 'minutes',\n'seconds', " diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index 9b624d809879ff..96a0953733fe88 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -220,9 +220,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/_decimal/_decimal.c - cond_map_template - Modules/_decimal/_decimal.c - dec_signal_string -