diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 2598ab40b23da77..59f41d137bc0a5f 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -1581,7 +1581,7 @@ Instance methods: and ``weekday``. The same as ``self.date().isocalendar()``. -.. method:: datetime.isoformat(sep='T', timespec='auto') +.. method:: datetime.isoformat(sep='T', timespec='auto', use_utc_designator=False) Return a string representing the date and time in ISO 8601 format: @@ -1645,9 +1645,21 @@ Instance methods: >>> my_datetime.isoformat(timespec='microseconds') '2015-01-01T12:30:59.000000' + If the optional argument *use_utc_designator* is set to :const:`True` and + :meth:`tzname` returns exactly ``"UTC"``, then "Z" will be given as the UTC + offset in the formatted string:: + + >>> my_datetime = dt.datetime(2022, 3, 21, 12, 30, 59, tzinfo=dt.timezone.utc) + >>> my_datetime.isoformat() + '2022-03-21T12:30:59+00:00' + >>> my_datetime.isoformat(use_utc_designator=True) + '2022-03-21T12:30:59Z' + .. versionchanged:: 3.6 Added the *timespec* parameter. + .. versionadded:: 3.16 + Added the *use_utc_designator* parameter. .. method:: datetime.__str__() @@ -2003,7 +2015,7 @@ Instance methods: Added the *fold* parameter. -.. method:: time.isoformat(timespec='auto') +.. method:: time.isoformat(timespec='auto', use_utc_designator=False) Return a string representing the time in ISO 8601 format, one of: @@ -2032,6 +2044,10 @@ Instance methods: :exc:`ValueError` will be raised on an invalid *timespec* argument. + If the optional argument *use_utc_designator* is set to :const:`True` and + :meth:`tzname` returns exactly ``"UTC"``, then "Z" will be given as the UTC + offset in the formatted string. + Example:: >>> import datetime as dt @@ -2042,10 +2058,18 @@ Instance methods: '12:34:56.000000' >>> my_time.isoformat(timespec='auto') '12:34:56' + >>> my_time = dt.time(12, 30, 59, tzinfo=dt.timezone.utc) + >>> my_time.isoformat() + '12:30:59+00:00' + >>> my_time.isoformat(use_utc_designator=True) + '12:30:59Z' .. versionchanged:: 3.6 Added the *timespec* parameter. + .. versionadded:: 3.16 + Added the *use_utc_designator* parameter. + .. method:: time.__str__() diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index f5ac965d3cfedf4..dd0ae20674a1e28 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -1626,11 +1626,13 @@ def __repr__(self): s = s[:-1] + ", fold=1)" return s - def isoformat(self, timespec='auto'): + def isoformat(self, timespec='auto', use_utc_designator=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. + part is omitted if self.microsecond == 0. The UTC offset will be + replaced with a single "Z" if use_utc_designator is True and + self.tzname() is exactly "UTC" The optional argument timespec specifies the number of additional terms of the time to include. Valid options are 'auto', 'hours', @@ -1638,9 +1640,12 @@ def isoformat(self, timespec='auto'): """ s = _format_time(self._hour, self._minute, self._second, self._microsecond, timespec) - tz = self._tzstr() - if tz: - s += tz + if use_utc_designator and "UTC" == self.tzname(): + s += "Z" + else: + tz = self._tzstr() + if tz: + s += tz return s __str__ = isoformat @@ -2175,14 +2180,16 @@ 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', use_utc_designator=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 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'. + If self.tzinfo is not None, the UTC offset is also attached. If the + optional argument use_utc_designator is True and the timezone name is + "UTC", a Z is appended: 'YYYY-MM-DD HH:MM:SS.mmmmmmZ'. + Otherwise the format is 'YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM'. Optional argument sep specifies the separator between date and time, default 'T'. @@ -2196,9 +2203,12 @@ def isoformat(self, sep='T', timespec='auto'): self._microsecond, timespec)) off = self.utcoffset() - tz = _format_offset(off) - if tz: - s += tz + if use_utc_designator and "UTC" == self.tzname(): + s += "Z" + else: + tz = _format_offset(off) + if tz: + s += tz return s diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index df7d540cb362f75..17cd0b15379221e 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2304,6 +2304,8 @@ def test_isoformat(self): 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.assertEqual(t.isoformat(use_utc_designator=False), "0001-02-03T04:05:01.000123") + self.assertEqual(t.isoformat(use_utc_designator=True), "0001-02-03T04:05:01.000123") self.assertRaises(ValueError, t.isoformat, timespec='foo') # bpo-34482: Check that surrogates are handled properly. self.assertRaises(ValueError, t.isoformat, timespec='\ud800') @@ -2312,6 +2314,8 @@ def test_isoformat(self): 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(use_utc_designator=False), "0001-02-03T04:05:01.999500+00:00") + self.assertEqual(t.isoformat(use_utc_designator=True), "0001-02-03T04:05:01.999500Z") t = self.theclass(1, 2, 3, 4, 5, 1, 999500) self.assertEqual(t.isoformat(timespec='milliseconds'), "0001-02-03T04:05:01.999") @@ -2331,6 +2335,8 @@ def test_isoformat(self): 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(use_utc_designator=False), "0002-03-02T00:00:00+00:00:16") + self.assertEqual(t.isoformat(use_utc_designator=True), "0002-03-02T00:00:00+00:00:16") def test_isoformat_timezone(self): tzoffsets = [ @@ -4107,6 +4113,74 @@ def test_isoformat_timezone(self): with self.subTest(tzi=tzi): self.assertEqual(t.isoformat(), exp) + def test_isoformat_utc_designator(self): + t = self.theclass(hour=12, minute=34, second=56, microsecond=123456) + self.assertEqual(t.isoformat(), "12:34:56.123456") + self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456") + self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456") + + t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, + tzinfo=timezone.utc) + self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z") + + t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, + tzinfo=timezone(timedelta(0))) + self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z") + + t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, + tzinfo=timezone(timedelta(0), "UTC")) + self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z") + + t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, + tzinfo=timezone(timedelta(0), "GMT")) + self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456+00:00") + + t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, + tzinfo=timezone(timedelta(hours=5), "UTC")) + self.assertEqual(t.isoformat(), "12:34:56.123456+05:00") + self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+05:00") + self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456Z") + + class UnnamedTimezone(tzinfo): + def utcoffset(self, dt): + return timedelta(0) + + def dst(self, dt): + return timedelta(0) + + def tzname(self, dt): + return None + + t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, + tzinfo=UnnamedTimezone()) + self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456+00:00") + + class NonStringNamedTimezone(tzinfo): + def utcoffset(self, dt): + return timedelta(0) + + def dst(self, dt): + return timedelta(0) + + def tzname(self, dt): + return 42 + + t = self.theclass(hour=12, minute=34, second=56, microsecond=123456, + tzinfo=UnnamedTimezone()) + self.assertEqual(t.isoformat(), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=False), "12:34:56.123456+00:00") + self.assertEqual(t.isoformat(use_utc_designator=True), "12:34:56.123456+00:00") + def test_1653736(self): # verify it doesn't accept extra keyword arguments t = self.theclass(second=1) diff --git a/Misc/NEWS.d/next/Library/2022-03-22-00-27-11.bpo-46614.Rgt_1Y.rst b/Misc/NEWS.d/next/Library/2022-03-22-00-27-11.bpo-46614.Rgt_1Y.rst new file mode 100644 index 000000000000000..eb5940805bb52ff --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-03-22-00-27-11.bpo-46614.Rgt_1Y.rst @@ -0,0 +1,4 @@ +Add *use_utc_designator* as an optional parameter to +:meth:`datetime.datetime.isoformat` and :meth:`datetime.time.isoformat`. If +it's set to true, the UTC offset will be formatted as "Z" rather than "+00:00" +if the object is associated with a timezone named exactly ``"UTC"``. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index cd2dd94c8164a53..eb27252cea9b9f5 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1720,8 +1720,10 @@ format_ctime(PyObject *date, int hours, int minutes, int seconds) static PyObject *delta_negative(PyObject *op); /* Add formatted UTC offset string to buf. buf has no more than - * buflen bytes remaining. The UTC offset is gotten by calling - * tzinfo.uctoffset(tzinfoarg). If that returns None, \0 is stored into + * buflen bytes remaining. If use_utc_designator is true, + * tzinfo.tzname(tzinfoarg) will be called, and if it returns "UTC", + * only "Z\0" will be added. Otherwise, the UTC offset is gotten by calling + * tzinfo.utcoffset(tzinfoarg). If that returns None, \0 is stored into * *buf, and that's all. Else the returned value is checked for sanity (an * integer in range), and if that's OK it's converted to an hours & minutes * string of the form @@ -1731,6 +1733,7 @@ static PyObject *delta_negative(PyObject *op); */ static int format_utcoffset(char *buf, size_t buflen, const char *sep, + int use_utc_designator, PyObject *tzinfo, PyObject *tzinfoarg) { PyObject *offset; @@ -1739,6 +1742,20 @@ format_utcoffset(char *buf, size_t buflen, const char *sep, assert(buflen >= 1); + if (use_utc_designator) { + PyObject* name = PyObject_CallMethod(tzinfo, "tzname", "O", tzinfoarg); + if (name == NULL) + return -1; + int tz_is_utc = (PyUnicode_Check(name) && + 0 == strcmp("UTC", PyUnicode_AsUTF8(name))); + Py_DECREF(name); + + if (tz_is_utc) { + PyOS_snprintf(buf, buflen, "Z"); + return 0; + } + } + offset = call_utcoffset(tzinfo, tzinfoarg); if (offset == NULL) return -1; @@ -1811,6 +1828,7 @@ make_somezreplacement(PyObject *object, char *sep, PyObject *tzinfoarg) if (format_utcoffset(buf, sizeof(buf), sep, + 0, tzinfo, tzinfoarg) < 0) return NULL; @@ -4901,11 +4919,14 @@ time_str(PyObject *op) datetime.time.isoformat timespec: str(c_default="NULL") = 'auto' + use_utc_designator: 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. +fractional part is omitted if self.microsecond == 0. The UTC offset +will be replaced with a single "Z" if use_utc_designator is True and +self.tzname() is exactly "UTC". The optional argument timespec specifies the number of additional terms of the time to include. Valid options are 'auto', 'hours', @@ -4913,8 +4934,9 @@ 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=0efae103081060f4]*/ +datetime_time_isoformat_impl(PyDateTime_Time *self, const char *timespec, + int use_utc_designator) +/*[clinic end generated code: output=938059b5f2a218ce input=6b40be753a404860]*/ { char buf[100]; @@ -4965,8 +4987,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) { + if (format_utcoffset(buf, sizeof(buf), ":", use_utc_designator, + self->tzinfo, Py_None) < 0) { Py_DECREF(result); return NULL; } @@ -6456,14 +6478,17 @@ datetime.datetime.isoformat sep: int(accept={str}) = 'T' timespec: str(c_default="NULL") = 'auto' + use_utc_designator: bool = 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 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'. +If self.tzinfo is not None, the UTC offset is also attached. If +the optional argument use_utc_designator is True and the timezone +name is "UTC", a Z is appended: 'YYYY-MM-DD HH:MM:SS.mmmmmmZ'. +Otherwise the format is 'YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM'. Optional argument sep specifies the separator between date and time, default 'T'. @@ -6475,8 +6500,9 @@ 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=db935a57fa697c5e]*/ + const char *timespec, + int use_utc_designator) +/*[clinic end generated code: output=1996e7d9ca2b38c8 input=0d182cca4b18668b]*/ { char buffer[100]; @@ -6528,7 +6554,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) { + if (format_utcoffset(buffer, sizeof(buffer), ":", use_utc_designator, + 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 392a9da93ddbb41..050a10778894ebf 100644 --- a/Modules/clinic/_datetimemodule.c.h +++ b/Modules/clinic/_datetimemodule.c.h @@ -893,13 +893,15 @@ 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\', use_utc_designator=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\n" -"fractional part is omitted if self.microsecond == 0.\n" +"fractional part is omitted if self.microsecond == 0. The UTC offset\n" +"will be replaced with a single \"Z\" if use_utc_designator is True and\n" +"self.tzname() is exactly \"UTC\".\n" "\n" "The optional argument timespec specifies the number of additional\n" "terms of the time to include. Valid options are \'auto\', \'hours\',\n" @@ -909,7 +911,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 use_utc_designator); static PyObject * datetime_time_isoformat(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) @@ -917,7 +920,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 @@ -926,7 +929,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(use_utc_designator), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -935,35 +938,45 @@ 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", "use_utc_designator", 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 use_utc_designator = 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; + if (args[0]) { + if (!PyUnicode_Check(args[0])) { + _PyArg_BadArgument("isoformat", "argument 'timespec'", "str", args[0]); + goto exit; + } + timespec = _PyUnicode_AsUTF8NoNUL(args[0]); + if (timespec == NULL) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_pos; + } } - timespec = _PyUnicode_AsUTF8NoNUL(args[0]); - if (timespec == NULL) { + use_utc_designator = PyObject_IsTrue(args[1]); + if (use_utc_designator < 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, use_utc_designator); exit: return return_value; @@ -1722,7 +1735,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\', use_utc_designator=False)\n" "--\n" "\n" "Return the time formatted according to ISO.\n" @@ -1730,8 +1743,10 @@ PyDoc_STRVAR(datetime_datetime_isoformat__doc__, "The full format looks like \'YYYY-MM-DD HH:MM:SS.mmmmmm\'.\n" "By default, the fractional part is omitted if self.microsecond == 0.\n" "\n" -"If self.tzinfo is not None, the UTC offset is also attached, giving\n" -"a full format of \'YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM\'.\n" +"If self.tzinfo is not None, the UTC offset is also attached. If\n" +"the optional argument use_utc_designator is True and the timezone\n" +"name is \"UTC\", a Z is appended: \'YYYY-MM-DD HH:MM:SS.mmmmmmZ\'.\n" +"Otherwise the format is \'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" @@ -1745,7 +1760,8 @@ PyDoc_STRVAR(datetime_datetime_isoformat__doc__, static PyObject * datetime_datetime_isoformat_impl(PyDateTime_DateTime *self, int sep, - const char *timespec); + const char *timespec, + int use_utc_designator); static PyObject * datetime_datetime_isoformat(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) @@ -1753,7 +1769,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 @@ -1762,7 +1778,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(use_utc_designator), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -1771,20 +1787,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", "use_utc_designator", 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 use_utc_designator = 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; } @@ -1808,16 +1825,25 @@ 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; + if (args[1]) { + if (!PyUnicode_Check(args[1])) { + _PyArg_BadArgument("isoformat", "argument 'timespec'", "str", args[1]); + goto exit; + } + timespec = _PyUnicode_AsUTF8NoNUL(args[1]); + if (timespec == NULL) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_pos; + } } - timespec = _PyUnicode_AsUTF8NoNUL(args[1]); - if (timespec == NULL) { + use_utc_designator = PyObject_IsTrue(args[2]); + if (use_utc_designator < 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, use_utc_designator); exit: return return_value; @@ -2082,4 +2108,4 @@ datetime_datetime___reduce__(PyObject *self, PyObject *Py_UNUSED(ignored)) { return datetime_datetime___reduce___impl((PyDateTime_DateTime *)self); } -/*[clinic end generated code: output=7bdfdfbd568c63ed input=a9049054013a1b77]*/ +/*[clinic end generated code: output=3416cb44b54c6220 input=a9049054013a1b77]*/