From ad5ffab70908b4c55f6caad08fa1d1b270ce015f Mon Sep 17 00:00:00 2001 From: Stephane Wirtel Date: Wed, 7 Feb 2018 11:02:29 +0100 Subject: [PATCH 01/16] bpo-1100942: Add datetime.time.strptime and datetime.date.strptime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add datetime.date.strptime and datetime.time.strptime. Fix the documentation of _strptime._strptime, the documentation was wrong, return a 3-tuple and not a 2-tuple Co-authored-by: Alexander Belopolsky Co-authored-by: Amaury Forgeot d'Arc Co-authored-by: Berker Peksag Co-authored-by: Josh-sf Co-authored-by: Juarez Bochi Co-authored-by: Maciej Szulik Co-authored-by: Stéphane Wirtel Co-authored-by: Matheus Vieira Portela --- Doc/library/datetime.rst | 33 +++++-- Lib/_strptime.py | 38 +++++++- Lib/datetime.py | 28 +++++- Lib/test/datetimetester.py | 33 +++++++ ...2018-02-07-10-55-58.bpo-1100942.md4agI.rst | 1 + Modules/_datetimemodule.c | 93 ++++++++++++++++++- 6 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2018-02-07-10-55-58.bpo-1100942.md4agI.rst diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 1ee23c2175a27d..b237d0f4e3fb41 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -443,6 +443,14 @@ Other constructors, all class methods: date.max.toordinal()``. For any date *d*, ``date.fromordinal(d.toordinal()) == d``. +.. classmethod:: date.strptime(date_string, format) + + Return a :class:`date` corresponding to *date_string*, parsed according to + *format*. :exc:`ValueError` is raised if the date string and format can't be + parsed by `time.strptime`, or if it returns a value where the time part is + nonzero. + + .. versionadded:: 3.8 .. classmethod:: date.fromisoformat(date_string) @@ -1426,6 +1434,19 @@ day, and subject to adjustment via a :class:`tzinfo` object. If an argument outside those ranges is given, :exc:`ValueError` is raised. All default to ``0`` except *tzinfo*, which defaults to :const:`None`. + +Other constructors, all class methods: + +.. classmethod:: time.strptime(date_string, format) + + Return a :class:`time` corresponding to *date_string, parsed according to + *format*. :exc:`ValueError` is raised if the date string and format can't be + parsed by `time.strptime`, if it returns a value which isn't a time tuple, + or if the date part is nonzero. + + .. versionadded:: 3.8 + + Class attributes: @@ -2023,13 +2044,13 @@ equivalent to ``datetime(*(time.strptime(date_string, format)[0:6]))``, except when the format includes sub-second components or timezone offset information, which are supported in ``datetime.strptime`` but are discarded by ``time.strptime``. -For :class:`.time` objects, the format codes for year, month, and day should not -be used, as time objects have no such values. If they're used anyway, ``1900`` -is substituted for the year, and ``1`` for the month and day. +The :meth:`date.strptime` class method creates a :class:`date` object from a +string representing a date and a corresponding format string. :exc:`ValueError` +raised if the format codes for hours, minutes, seconds, and microseconds are used. -For :class:`date` objects, the format codes for hours, minutes, seconds, and -microseconds should not be used, as :class:`date` objects have no such -values. If they're used anyway, ``0`` is substituted for them. +The :meth:`.time.strptime` class method creates a :class:`.time` object from a +string representing a time and a corresponding format string. :exc:`ValueError` +raised if the format codes for year, month, and day are used. The full set of format codes supported varies across platforms, because Python calls the platform C library's :func:`strftime` function, and platform diff --git a/Lib/_strptime.py b/Lib/_strptime.py index f4f3c0b80c1d05..17c0b74ddffe93 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -17,6 +17,7 @@ from re import IGNORECASE from re import escape as re_escape from datetime import (date as datetime_date, + datetime as datetime_datetime, timedelta as datetime_timedelta, timezone as datetime_timezone) from _thread import allocate_lock as _thread_allocate_lock @@ -307,9 +308,9 @@ def _calc_julian_from_V(iso_year, iso_week, iso_weekday): def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): - """Return a 2-tuple consisting of a time struct and an int containing + """Return a 3-tuple consisting of a time struct and an int containing the number of microseconds based on the input string and the - format string.""" + format string, and the GMT offset.""" for index, arg in enumerate([data_string, format]): if not isinstance(arg, str): @@ -556,6 +557,10 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): hour, minute, second, weekday, julian, tz, tzname, gmtoff), fraction, gmtoff_fraction +date_specs = ('%a', '%A', '%b', '%B', '%c', '%d', '%j', '%m', '%U', + '%w', '%W', '%x', '%y', '%Y',) +time_specs = ('%T', '%R', '%H', '%I', '%M', '%S', '%f', '%i', '%s',) + def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"): """Return a time struct based on the input string and the format string.""" @@ -577,3 +582,32 @@ def _strptime_datetime(cls, data_string, format="%a %b %d %H:%M:%S %Y"): args += (tz,) return cls(*args) + +def _strptime_datetime_date(data_string, format): + """Return a date based on the input string and the format string.""" + if not format: + raise ValueError("Date format is not valid.") + msg = "'{!s}' {} not valid in date format specification." + if _check_invalid_datetime_specs(format, time_specs, msg): + _date = _strptime_datetime(datetime_datetime, data_string, format) + return _date.date() + +def _strptime_datetime_time(data_string, format): + """Return a time based on the input string and the format string.""" + if not format: + raise ValueError("Date format is not valid.") + msg = "'{!s}' {} not valid in time format specification." + if _check_invalid_datetime_specs(format, date_specs, msg): + _time = _strptime_datetime(datetime_datetime, data_string, format) + return _time.time() + +def _check_invalid_datetime_specs(fmt, specs, msg): + found_invalid_specs = [] + for spec in specs: + if spec in fmt: + found_invalid_specs.append(spec) + if found_invalid_specs: + suffix = "are" if len(found_invalid_specs) > 1 else "is" + raise ValueError(msg.format(", ".join(found_invalid_specs), + suffix)) + return True diff --git a/Lib/datetime.py b/Lib/datetime.py index 85bfa48e05dea4..850113bf1e56a6 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -795,6 +795,7 @@ class date: fromtimestamp() today() fromordinal() + strptime() Operators: @@ -885,6 +886,16 @@ def fromisoformat(cls, date_string): raise ValueError(f'Invalid isoformat string: {date_string!r}') + @classmethod + def strptime(cls, date_string, format): + """string, format -> new date instance parsed from a string. + + >>> datetime.date.strptime('2012/07/20', '%Y/%m/%d') + datetime.date(2012, 7, 20) + """ + import _strptime + return _strptime._strptime_datetime_date(date_string, format) + # Conversions to string def __repr__(self): @@ -1180,6 +1191,7 @@ class time: Constructors: __new__() + strptime() Operators: @@ -1238,6 +1250,16 @@ def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold self._fold = fold return self + @staticmethod + def strptime(time_string, format): + """string, format -> new time instance parsed from a string. + + >>> datetime.time.strptime('10:40am', '%H:%M%p') + datetime.time(10, 40) + """ + import _strptime + return _strptime._strptime_datetime_time(time_string, format) + # Read-only field accessors @property def hour(self): @@ -1906,7 +1928,11 @@ def __str__(self): @classmethod def strptime(cls, date_string, format): - 'string, format -> new datetime parsed from a string (like time.strptime()).' + """string, format -> new datetime parsed from a string. + + >>> datetime.datetime.strptime('2012/07/20 10:40am', '%Y/%m/%d %H:%M%p') + datetime.datetime(2012, 7, 20, 10, 40) + """ import _strptime return _strptime._strptime_datetime(cls, date_string, format) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 715f0ea6b40d23..c94be1c113b4ef 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1017,6 +1017,23 @@ def test_delta_non_days_ignored(self): dt2 = dt - delta self.assertEqual(dt2, dt - days) + def test_strptime_valid_format(self): + tests = [(('2004-12-01', '%Y-%m-%d'), + date(2004, 12, 1)), + (('2004', '%Y'), date(2004, 1, 1)),] + for (date_string, date_format), expected in tests: + self.assertEqual(expected, date.strptime(date_string, date_format)) + + def test_strptime_invalid_format(self): + tests = [('2004-12-01 13:02:47.197', + '%Y-%m-%d %H:%M:%S.%f'), + ('01', '%M'), + ('02', '%H'),] + for test in tests: + with self.assertRaises(ValueError): + date.strptime(test[0], test[1]) + + class SubclassDate(date): sub_var = 1 @@ -3105,6 +3122,22 @@ def test_strftime(self): except UnicodeEncodeError: pass + def test_strptime_invalid(self): + tests = [('2004-12-01 13:02:47.197', + '%Y-%m-%d %H:%M:%S.%f'), + ('2004-12-01', '%Y-%m-%d'),] + for date_string, date_format in tests: + with self.assertRaises(ValueError): + time.strptime(date_string, date_format) + + def test_strptime_valid(self): + string = '13:02:47.197' + format = '%H:%M:%S.%f' + result, frac, gmtoff = _strptime._strptime(string, format) + expected = self.theclass(*(result[3:6] + (frac, ))) + got = time.strptime(string, format) + self.assertEqual(expected, got) + def test_format(self): t = self.theclass(1, 2, 3, 4) self.assertEqual(t.__format__(''), str(t)) diff --git a/Misc/NEWS.d/next/Core and Builtins/2018-02-07-10-55-58.bpo-1100942.md4agI.rst b/Misc/NEWS.d/next/Core and Builtins/2018-02-07-10-55-58.bpo-1100942.md4agI.rst new file mode 100644 index 00000000000000..04cbe33f66ecba --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2018-02-07-10-55-58.bpo-1100942.md4agI.rst @@ -0,0 +1 @@ +Add datetime.date.strptime and datetime.time.strptime class methods. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index c1557b5e6f491d..11e958517d49f0 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -133,6 +133,10 @@ static PyTypeObject PyDateTime_DeltaType; static PyTypeObject PyDateTime_TimeType; static PyTypeObject PyDateTime_TZInfoType; static PyTypeObject PyDateTime_TimeZoneType; +static PyObject *datetime_strptime(PyObject *cls, PyObject *args); +static PyObject *datetime_getdate(PyDateTime_DateTime *self); +static PyObject *datetime_gettime(PyDateTime_DateTime *self); + static int check_tzinfo_subclass(PyObject *p); @@ -2946,6 +2950,33 @@ date_fromordinal(PyObject *cls, PyObject *args) return result; } + +/* Return new date from time.strptime(). */ +static PyObject * +date_strptime(PyObject *cls, PyObject *args) +{ + PyObject *date = NULL; + PyObject *datetime; + + datetime = datetime_strptime((PyObject *)&PyDateTime_DateTimeType, args); + + if (datetime == NULL) + return NULL; + + if (DATE_GET_HOUR(datetime) || + DATE_GET_MINUTE(datetime) || + DATE_GET_SECOND(datetime) || + DATE_GET_MICROSECOND(datetime)) + PyErr_SetString(PyExc_ValueError, + "date.strptime value cannot have a time part"); + else + date = datetime_getdate((PyDateTime_DateTime *)datetime); + + Py_DECREF(datetime); + return date; +} + + /* Return the new date from a string as generated by date.isoformat() */ static PyObject * date_fromisoformat(PyObject *cls, PyObject *dtstr) @@ -3283,6 +3314,11 @@ static PyMethodDef date_methods[] = { PyDoc_STR("Current date or datetime: same as " "self.__class__.fromtimestamp(time.time()).")}, + {"strptime", (PyCFunction)date_strptime, METH_VARARGS | METH_CLASS, + PyDoc_STR("string, format -> new date instance parsed from a string.\n\n" + ">>> datetime.date.strptime('2012/07/20', '%Y/%m/%d')\n" + "datetime.date(2012, 7, 20)")}, + /* Instance methods: */ {"ctime", (PyCFunction)date_ctime, METH_NOARGS, @@ -4026,6 +4062,49 @@ time_new(PyTypeObject *type, PyObject *args, PyObject *kw) return self; } + +/* Return new time from time.strptime(). */ +static PyObject * +time_strptime(PyObject *cls, PyObject *args) +{ + PyObject *time = NULL; + PyObject *datetime; + + static PyObject *emptyDatetime = NULL; + + /* To ensure that the given string does not contain a date, + * compare with the result of an empty date string. + */ + if (emptyDatetime == NULL) { + PyObject *emptyStringPair = Py_BuildValue("ss", "", ""); + if (emptyStringPair == NULL) + return NULL; + emptyDatetime = datetime_strptime( + (PyObject *)&PyDateTime_DateTimeType, + emptyStringPair); + Py_DECREF(emptyStringPair); + if (emptyDatetime == NULL) + return NULL; + } + + datetime = datetime_strptime((PyObject *)&PyDateTime_DateTimeType, args); + + if (datetime == NULL) + return NULL; + + if (GET_YEAR(datetime) != GET_YEAR(emptyDatetime) + || GET_MONTH(datetime) != GET_MONTH(emptyDatetime) + || GET_DAY(datetime) != GET_DAY(emptyDatetime)) + PyErr_SetString(PyExc_ValueError, + "time.strptime value cannot have a date part"); + else + time = datetime_gettime((PyDateTime_DateTime *)datetime); + + Py_DECREF(datetime); + return time; +} + + /* * Destructor. */ @@ -4451,6 +4530,14 @@ time_reduce(PyDateTime_Time *self, PyObject *arg) } static PyMethodDef time_methods[] = { + /* Class methods: */ + + {"strptime", (PyCFunction)time_strptime, METH_VARARGS | METH_CLASS, + PyDoc_STR("string, format -> new time parsed from a string.\n\n" + ">>> datetime.time.strptime('10:40am', '%H:%M%p')\n" + "datetime.time(10, 40)")}, + + /* Instance methods: */ {"isoformat", (PyCFunction)(void(*)(void))time_isoformat, METH_VARARGS | METH_KEYWORDS, PyDoc_STR("Return string in ISO 8601 format, [HH[:MM[:SS[.mmm[uuu]]]]]" @@ -6123,8 +6210,10 @@ static PyMethodDef datetime_methods[] = { {"strptime", (PyCFunction)datetime_strptime, METH_VARARGS | METH_CLASS, - PyDoc_STR("string, format -> new datetime parsed from a string " - "(like time.strptime()).")}, + PyDoc_STR("string, format -> new datetime parsed from a string.\n\n" + ">>> datetime.datetime.strptime('2012/07/20 10:40am', " + "'%Y/%m/%d %H:%M%p')\n" + "datetime.datetime(2012, 7, 20, 10, 40)")}, {"combine", (PyCFunction)(void(*)(void))datetime_combine, METH_VARARGS | METH_KEYWORDS | METH_CLASS, From 50606eb882c6fcf2c1dd08746132d2a3afa5115b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Wirtel?= Date: Tue, 15 May 2018 19:39:14 +0200 Subject: [PATCH 02/16] Follow the PEP7 for the C Code --- Modules/_datetimemodule.c | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 11e958517d49f0..7bc855a3d6fbae 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -2960,17 +2960,20 @@ date_strptime(PyObject *cls, PyObject *args) datetime = datetime_strptime((PyObject *)&PyDateTime_DateTimeType, args); - if (datetime == NULL) + if (datetime == NULL) { return NULL; + } if (DATE_GET_HOUR(datetime) || DATE_GET_MINUTE(datetime) || DATE_GET_SECOND(datetime) || - DATE_GET_MICROSECOND(datetime)) + DATE_GET_MICROSECOND(datetime)) { PyErr_SetString(PyExc_ValueError, "date.strptime value cannot have a time part"); - else + } + else { date = datetime_getdate((PyDateTime_DateTime *)datetime); + } Py_DECREF(datetime); return date; @@ -4077,28 +4080,33 @@ time_strptime(PyObject *cls, PyObject *args) */ if (emptyDatetime == NULL) { PyObject *emptyStringPair = Py_BuildValue("ss", "", ""); - if (emptyStringPair == NULL) + if (emptyStringPair == NULL) { return NULL; + } emptyDatetime = datetime_strptime( (PyObject *)&PyDateTime_DateTimeType, emptyStringPair); Py_DECREF(emptyStringPair); - if (emptyDatetime == NULL) + if (emptyDatetime == NULL) { return NULL; + } } datetime = datetime_strptime((PyObject *)&PyDateTime_DateTimeType, args); - if (datetime == NULL) + if (datetime == NULL) { return NULL; + } if (GET_YEAR(datetime) != GET_YEAR(emptyDatetime) || GET_MONTH(datetime) != GET_MONTH(emptyDatetime) - || GET_DAY(datetime) != GET_DAY(emptyDatetime)) + || GET_DAY(datetime) != GET_DAY(emptyDatetime)) { PyErr_SetString(PyExc_ValueError, "time.strptime value cannot have a date part"); - else + } + else { time = datetime_gettime((PyDateTime_DateTime *)datetime); + } Py_DECREF(datetime); return time; From ab5030fc1b89a249b320183bb02ba165c8a3fba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Wirtel?= Date: Tue, 15 May 2018 19:45:03 +0200 Subject: [PATCH 03/16] Remove unused formats --- Lib/_strptime.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/_strptime.py b/Lib/_strptime.py index 17c0b74ddffe93..2618f3e04f91e8 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -558,8 +558,8 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): weekday, julian, tz, tzname, gmtoff), fraction, gmtoff_fraction date_specs = ('%a', '%A', '%b', '%B', '%c', '%d', '%j', '%m', '%U', - '%w', '%W', '%x', '%y', '%Y',) -time_specs = ('%T', '%R', '%H', '%I', '%M', '%S', '%f', '%i', '%s',) + '%w', '%W', '%x', '%y', '%Y', '%G', '%u', '%V',) +time_specs = ('%H', '%I', '%M', '%S', '%f',) def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"): """Return a time struct based on the input string and the From 07fb326c7c4661c7fa7cf6656781c0c9204fae4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Wirtel?= Date: Wed, 17 Oct 2018 22:17:13 +0200 Subject: [PATCH 04/16] Use subtest for the tests --- Lib/test/datetimetester.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index c94be1c113b4ef..eda0418800814c 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1018,20 +1018,26 @@ def test_delta_non_days_ignored(self): self.assertEqual(dt2, dt - days) def test_strptime_valid_format(self): - tests = [(('2004-12-01', '%Y-%m-%d'), - date(2004, 12, 1)), - (('2004', '%Y'), date(2004, 1, 1)),] - for (date_string, date_format), expected in tests: - self.assertEqual(expected, date.strptime(date_string, date_format)) + tests = [ + ('2004-12-01', '%Y-%m-%d', date(2004, 12, 1)), + ('2004', '%Y', date(2004, 1, 1)), + ] + for date_string, date_format, expected in tests: + with self.subTest(date_string=date_string, + date_format=date_format, + expected=expected): + self.assertEqual(expected, date.strptime(date_string, date_format)) def test_strptime_invalid_format(self): - tests = [('2004-12-01 13:02:47.197', - '%Y-%m-%d %H:%M:%S.%f'), - ('01', '%M'), - ('02', '%H'),] - for test in tests: - with self.assertRaises(ValueError): - date.strptime(test[0], test[1]) + tests = [ + ('2004-12-01 13:02:47.197', '%Y-%m-%d %H:%M:%S.%f'), + ('01', '%M'), + ('02', '%H'), + ] + for hour, format in tests: + with self.subTest(hour=hour, format=format): + with self.assertRaises(ValueError): + date.strptime(hour, format) class SubclassDate(date): From 010ac8ba36ce2df7ddfeeac81b2796ab169125ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Wirtel?= Date: Thu, 25 Oct 2018 15:39:45 +0200 Subject: [PATCH 05/16] Fix the signatures --- Modules/_datetimemodule.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 7bc855a3d6fbae..329aae25e945f0 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -6084,7 +6084,7 @@ datetime_timestamp(PyDateTime_DateTime *self, PyObject *Py_UNUSED(ignored)) } static PyObject * -datetime_getdate(PyDateTime_DateTime *self, PyObject *Py_UNUSED(ignored)) +datetime_getdate(PyDateTime_DateTime *self) { return new_date(GET_YEAR(self), GET_MONTH(self), @@ -6092,7 +6092,7 @@ datetime_getdate(PyDateTime_DateTime *self, PyObject *Py_UNUSED(ignored)) } static PyObject * -datetime_gettime(PyDateTime_DateTime *self, PyObject *Py_UNUSED(ignored)) +datetime_gettime(PyDateTime_DateTime *self) { return new_time(DATE_GET_HOUR(self), DATE_GET_MINUTE(self), From 5b58fc1316e537eee5985dc0af60bce3d268a334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Wirtel?= Date: Thu, 25 Oct 2018 15:56:50 +0200 Subject: [PATCH 06/16] Update with the recommendation of @pganssle --- Lib/_strptime.py | 15 +++++---------- Lib/test/datetimetester.py | 12 +++++++----- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Lib/_strptime.py b/Lib/_strptime.py index 2618f3e04f91e8..3de9bd6059272b 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -557,8 +557,8 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): hour, minute, second, weekday, julian, tz, tzname, gmtoff), fraction, gmtoff_fraction -date_specs = ('%a', '%A', '%b', '%B', '%c', '%d', '%j', '%m', '%U', - '%w', '%W', '%x', '%y', '%Y', '%G', '%u', '%V',) +date_specs = ('%a', '%A', '%b', '%B', '%c', '%d', '%j', '%m', '%U', '%G', + '%u', '%V', '%w', '%W', '%x', '%y', '%Y', '%G', '%u', '%V',) time_specs = ('%H', '%I', '%M', '%S', '%f',) def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"): @@ -585,8 +585,6 @@ def _strptime_datetime(cls, data_string, format="%a %b %d %H:%M:%S %Y"): def _strptime_datetime_date(data_string, format): """Return a date based on the input string and the format string.""" - if not format: - raise ValueError("Date format is not valid.") msg = "'{!s}' {} not valid in date format specification." if _check_invalid_datetime_specs(format, time_specs, msg): _date = _strptime_datetime(datetime_datetime, data_string, format) @@ -594,20 +592,17 @@ def _strptime_datetime_date(data_string, format): def _strptime_datetime_time(data_string, format): """Return a time based on the input string and the format string.""" - if not format: - raise ValueError("Date format is not valid.") msg = "'{!s}' {} not valid in time format specification." if _check_invalid_datetime_specs(format, date_specs, msg): _time = _strptime_datetime(datetime_datetime, data_string, format) return _time.time() -def _check_invalid_datetime_specs(fmt, specs, msg): +def _check_invalid_datetime_specs(fmt, blacklist_specs, msg): found_invalid_specs = [] - for spec in specs: + for spec in blacklist_specs: if spec in fmt: found_invalid_specs.append(spec) if found_invalid_specs: suffix = "are" if len(found_invalid_specs) > 1 else "is" - raise ValueError(msg.format(", ".join(found_invalid_specs), - suffix)) + raise ValueError(msg.format(", ".join(found_invalid_specs), suffix)) return True diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index eda0418800814c..803636b83f2937 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3129,12 +3129,14 @@ def test_strftime(self): pass def test_strptime_invalid(self): - tests = [('2004-12-01 13:02:47.197', - '%Y-%m-%d %H:%M:%S.%f'), - ('2004-12-01', '%Y-%m-%d'),] + tests = [ + ('2004-12-01 13:02:47.197', '%Y-%m-%d %H:%M:%S.%f'), + ('2004-12-01', '%Y-%m-%d'), + ] for date_string, date_format in tests: - with self.assertRaises(ValueError): - time.strptime(date_string, date_format) + with self.subTest(date_string=date_string, date_format=date_format): + with self.assertRaises(ValueError): + time.strptime(date_string, date_format) def test_strptime_valid(self): string = '13:02:47.197' From 8353a65e702efb5e87efff23d5ec4b376849bb59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Wirtel?= Date: Sun, 4 Nov 2018 23:07:50 +0100 Subject: [PATCH 07/16] Update --- Lib/test/datetimetester.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 803636b83f2937..b634a3f658cf62 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1031,6 +1031,8 @@ def test_strptime_valid_format(self): def test_strptime_invalid_format(self): tests = [ ('2004-12-01 13:02:47.197', '%Y-%m-%d %H:%M:%S.%f'), + ('2018-01-01 00:00', '%Y-%m-%d %H:%M'), + ('2018-01-01', ''), ('01', '%M'), ('02', '%H'), ] @@ -3132,6 +3134,8 @@ def test_strptime_invalid(self): tests = [ ('2004-12-01 13:02:47.197', '%Y-%m-%d %H:%M:%S.%f'), ('2004-12-01', '%Y-%m-%d'), + ('1900-01-01 12:30', '%Y-%m-%d %H:%M'), + ('12:30:15', '') ] for date_string, date_format in tests: with self.subTest(date_string=date_string, date_format=date_format): From cff74a9540940415fb43b9d40b7c323a4c53bd2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Wirtel?= Date: Sun, 11 Nov 2018 14:56:42 +0000 Subject: [PATCH 08/16] Add use case where we generate an exception --- Lib/test/datetimetester.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index b634a3f658cf62..2c8717d1c72e19 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1031,7 +1031,6 @@ def test_strptime_valid_format(self): def test_strptime_invalid_format(self): tests = [ ('2004-12-01 13:02:47.197', '%Y-%m-%d %H:%M:%S.%f'), - ('2018-01-01 00:00', '%Y-%m-%d %H:%M'), ('2018-01-01', ''), ('01', '%M'), ('02', '%H'), @@ -3134,8 +3133,7 @@ def test_strptime_invalid(self): tests = [ ('2004-12-01 13:02:47.197', '%Y-%m-%d %H:%M:%S.%f'), ('2004-12-01', '%Y-%m-%d'), - ('1900-01-01 12:30', '%Y-%m-%d %H:%M'), - ('12:30:15', '') + ('12:30:15', ''), ] for date_string, date_format in tests: with self.subTest(date_string=date_string, date_format=date_format): From f92a799906d12ae52469f643cdddb7031a8d8b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Wirtel?= Date: Sun, 11 Nov 2018 16:59:55 +0000 Subject: [PATCH 09/16] Add the Py_UNUSED(ignore) for datetime_gettime & datetime_getdate --- Doc/library/datetime.rst | 6 +++--- Modules/_datetimemodule.c | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index b237d0f4e3fb41..f77d0ba88c9341 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -447,8 +447,8 @@ Other constructors, all class methods: Return a :class:`date` corresponding to *date_string*, parsed according to *format*. :exc:`ValueError` is raised if the date string and format can't be - parsed by `time.strptime`, or if it returns a value where the time part is - nonzero. + parsed by :meth:`time.strptime`, or if time components are present in the + format string. .. versionadded:: 3.8 @@ -1441,7 +1441,7 @@ Other constructors, all class methods: Return a :class:`time` corresponding to *date_string, parsed according to *format*. :exc:`ValueError` is raised if the date string and format can't be - parsed by `time.strptime`, if it returns a value which isn't a time tuple, + parsed by :meth:`time.strptime`, if it returns a value which isn't a time tuple, or if the date part is nonzero. .. versionadded:: 3.8 diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 329aae25e945f0..87b38186388ddb 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -134,8 +134,8 @@ static PyTypeObject PyDateTime_TimeType; static PyTypeObject PyDateTime_TZInfoType; static PyTypeObject PyDateTime_TimeZoneType; static PyObject *datetime_strptime(PyObject *cls, PyObject *args); -static PyObject *datetime_getdate(PyDateTime_DateTime *self); -static PyObject *datetime_gettime(PyDateTime_DateTime *self); +static PyObject *datetime_getdate(PyDateTime_DateTime *self, PyObject *Py_UNUSED(ignored)); +static PyObject *datetime_gettime(PyDateTime_DateTime *self, PyObject *Py_UNUSED(ignored)); static int check_tzinfo_subclass(PyObject *p); @@ -2972,7 +2972,7 @@ date_strptime(PyObject *cls, PyObject *args) "date.strptime value cannot have a time part"); } else { - date = datetime_getdate((PyDateTime_DateTime *)datetime); + date = datetime_getdate((PyDateTime_DateTime *)datetime, NULL); } Py_DECREF(datetime); @@ -4105,7 +4105,7 @@ time_strptime(PyObject *cls, PyObject *args) "time.strptime value cannot have a date part"); } else { - time = datetime_gettime((PyDateTime_DateTime *)datetime); + time = datetime_gettime((PyDateTime_DateTime *)datetime, NULL); } Py_DECREF(datetime); @@ -6084,7 +6084,7 @@ datetime_timestamp(PyDateTime_DateTime *self, PyObject *Py_UNUSED(ignored)) } static PyObject * -datetime_getdate(PyDateTime_DateTime *self) +datetime_getdate(PyDateTime_DateTime *self, PyObject *Py_UNUSED(ignored)) { return new_date(GET_YEAR(self), GET_MONTH(self), @@ -6092,7 +6092,7 @@ datetime_getdate(PyDateTime_DateTime *self) } static PyObject * -datetime_gettime(PyDateTime_DateTime *self) +datetime_gettime(PyDateTime_DateTime *self, PyObject *Py_UNUSED(ignored)) { return new_time(DATE_GET_HOUR(self), DATE_GET_MINUTE(self), From 4c4a7cc09f541d66cf040b948acbb8a366c2e123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Wirtel?= Date: Wed, 6 Mar 2019 14:43:54 +0100 Subject: [PATCH 10/16] Add a link to the strtime() and strptime() behaviour --- Doc/library/datetime.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index f77d0ba88c9341..c02fbe0f088cd9 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -448,7 +448,8 @@ Other constructors, all class methods: Return a :class:`date` corresponding to *date_string*, parsed according to *format*. :exc:`ValueError` is raised if the date string and format can't be parsed by :meth:`time.strptime`, or if time components are present in the - format string. + format string. For a complete list of formatting directives, see + :ref:`strftime-strptime-behavior`. .. versionadded:: 3.8 @@ -1441,8 +1442,9 @@ Other constructors, all class methods: Return a :class:`time` corresponding to *date_string, parsed according to *format*. :exc:`ValueError` is raised if the date string and format can't be - parsed by :meth:`time.strptime`, if it returns a value which isn't a time tuple, - or if the date part is nonzero. + parsed by :meth:`time.strptime`, if it returns a value which isn't a time + tuple, or if the date part is nonzero. For a complete list of formatting + directives, see :ref:`strftime-strptime-behavior`. .. versionadded:: 3.8 From b0061722a06fa82176ef77224bfbdf46ad35c6b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Wirtel?= Date: Wed, 6 Mar 2019 14:44:58 +0100 Subject: [PATCH 11/16] Update the blurb entry and add an entry in the Doc/whatsnew/3.8.rst --- Doc/whatsnew/3.8.rst | 9 +++++++++ .../2018-02-07-10-55-58.bpo-1100942.md4agI.rst | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index 18ec2c2f662dd0..fbc4ea8411519a 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -175,6 +175,15 @@ Added :func:`~gettext.pgettext` and its variants. (Contributed by Franz Glasner, Éric Araujo, and Cheryl Sabella in :issue:`2504`.) +datetime +-------- + +Added :func:`~datetime.date.strptime` and :func:`~datetime.time.strptime`. +(Patch by Alexander Belopolsky, Amaury Forgeot d'Arc, Berker Peksag, Josh-sf, +Juraez Bochi, Maciej Szulik, Matheus Vieira Portela. Contributed by Stéphane +Wirtel) + + gc -- diff --git a/Misc/NEWS.d/next/Core and Builtins/2018-02-07-10-55-58.bpo-1100942.md4agI.rst b/Misc/NEWS.d/next/Core and Builtins/2018-02-07-10-55-58.bpo-1100942.md4agI.rst index 04cbe33f66ecba..788023cedb672d 100644 --- a/Misc/NEWS.d/next/Core and Builtins/2018-02-07-10-55-58.bpo-1100942.md4agI.rst +++ b/Misc/NEWS.d/next/Core and Builtins/2018-02-07-10-55-58.bpo-1100942.md4agI.rst @@ -1 +1 @@ -Add datetime.date.strptime and datetime.time.strptime class methods. +Add :func:`datetime.date.strptime` and :func:`datetime.time.strptime` class methods. From ff7487f64b5fee95e62f37d0c47b60911b6cb004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Wirtel?= Date: Wed, 6 Mar 2019 14:45:46 +0100 Subject: [PATCH 12/16] Fix doc --- Lib/_strptime.py | 2 +- Modules/_datetimemodule.c | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/_strptime.py b/Lib/_strptime.py index 3de9bd6059272b..9390ddcae660ef 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -310,7 +310,7 @@ def _calc_julian_from_V(iso_year, iso_week, iso_weekday): def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): """Return a 3-tuple consisting of a time struct and an int containing the number of microseconds based on the input string and the - format string, and the GMT offset.""" + format string, and the UTC offset.""" for index, arg in enumerate([data_string, format]): if not isinstance(arg, str): diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 87b38186388ddb..9fbac3dac7711f 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -2951,7 +2951,7 @@ date_fromordinal(PyObject *cls, PyObject *args) } -/* Return new date from time.strptime(). */ +/* Return new date from date.strptime(). */ static PyObject * date_strptime(PyObject *cls, PyObject *args) { @@ -2959,7 +2959,6 @@ date_strptime(PyObject *cls, PyObject *args) PyObject *datetime; datetime = datetime_strptime((PyObject *)&PyDateTime_DateTimeType, args); - if (datetime == NULL) { return NULL; } From a048f0e376ddecdc0def42fec7651914feebea0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Wirtel?= Date: Mon, 18 Mar 2019 03:28:43 +0100 Subject: [PATCH 13/16] Convert the _check_invalid_datetime_spec to the C-API --- Lib/_strptime.py | 12 ++------- Modules/_datetimemodule.c | 54 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/Lib/_strptime.py b/Lib/_strptime.py index 9390ddcae660ef..50013c20c0dc43 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -586,6 +586,7 @@ def _strptime_datetime(cls, data_string, format="%a %b %d %H:%M:%S %Y"): def _strptime_datetime_date(data_string, format): """Return a date based on the input string and the format string.""" msg = "'{!s}' {} not valid in date format specification." + from _datetime import _check_invalid_datetime_specs if _check_invalid_datetime_specs(format, time_specs, msg): _date = _strptime_datetime(datetime_datetime, data_string, format) return _date.date() @@ -593,16 +594,7 @@ def _strptime_datetime_date(data_string, format): def _strptime_datetime_time(data_string, format): """Return a time based on the input string and the format string.""" msg = "'{!s}' {} not valid in time format specification." + from _datetime import _check_invalid_datetime_specs if _check_invalid_datetime_specs(format, date_specs, msg): _time = _strptime_datetime(datetime_datetime, data_string, format) return _time.time() - -def _check_invalid_datetime_specs(fmt, blacklist_specs, msg): - found_invalid_specs = [] - for spec in blacklist_specs: - if spec in fmt: - found_invalid_specs.append(spec) - if found_invalid_specs: - suffix = "are" if len(found_invalid_specs) > 1 else "is" - raise ValueError(msg.format(", ".join(found_invalid_specs), suffix)) - return True diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 9fbac3dac7711f..a442b634c1d1d0 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -6351,7 +6351,61 @@ static PyTypeObject PyDateTime_DateTimeType = { * Module methods and initialization. */ +static PyObject * +_check_invalid_datetime_specs(PyObject *module, PyObject *args) +{ + PyObject *format = NULL, *specs = NULL, *message = NULL; + + if (!PyArg_ParseTuple(args, "OOO", &format, &specs, &message)) { + return NULL; + } + + PyObject *found_invalid_specs = PyList_New(0); + if (found_invalid_specs == NULL) { + return NULL; + } + + for (Py_ssize_t pos = 0; pos < PyTuple_GET_SIZE(specs); pos++) { + PyObject *spec = PyTuple_GET_ITEM(specs, pos); + if (PySequence_Contains(format, spec)) { + PyList_Append(found_invalid_specs, spec); + } + } + + Py_ssize_t specs_length = PyList_GET_SIZE(found_invalid_specs); + if (specs_length) { + PyObject *suffix = PyUnicode_FromString(specs_length > 1 ? "are" : "is"); + PyObject *separator = PyUnicode_FromString(", "); + PyObject *left_part = PyUnicode_Join(separator, found_invalid_specs); + + PyObject *error_message = PyObject_CallMethod(message, + "format", "OO", + left_part, suffix, NULL); + Py_DECREF(suffix); + Py_DECREF(left_part); + Py_DECREF(separator); + Py_DECREF(found_invalid_specs); + PyErr_SetObject(PyExc_ValueError, error_message); + Py_DECREF(error_message); + return NULL; + } + + Py_DECREF(found_invalid_specs); + return Py_INCREF(Py_True), Py_True; +} + +PyDoc_STRVAR( + _check_invalid_datetime_specs__doc__, + "_check_invalid_datetime_specs(format, specs, message)" +); + static PyMethodDef module_methods[] = { + { + "_check_invalid_datetime_specs", + (PyCFunction) _check_invalid_datetime_specs, + METH_VARARGS, + _check_invalid_datetime_specs__doc__ + }, {NULL, NULL} }; From 9ac0af48a1cc7504d876c78939475360efba3d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Wirtel?= Date: Mon, 18 Mar 2019 03:55:56 +0100 Subject: [PATCH 14/16] Support tuple and list for the specs --- Modules/_datetimemodule.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index a442b634c1d1d0..4e9d217b9adb1a 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -6360,13 +6360,18 @@ _check_invalid_datetime_specs(PyObject *module, PyObject *args) return NULL; } + assert(PyTuple_CheckExact(specs) || PyList_CheckExact(specs)); + PyObject *found_invalid_specs = PyList_New(0); if (found_invalid_specs == NULL) { return NULL; } - for (Py_ssize_t pos = 0; pos < PyTuple_GET_SIZE(specs); pos++) { - PyObject *spec = PyTuple_GET_ITEM(specs, pos); + PyObject *(*GetItem)(PyObject *, Py_ssize_t) = \ + PyTuple_CheckExact(specs) ? PyTuple_GetItem : PyList_GetItem; + + for (Py_ssize_t pos = 0; pos < PyObject_Size(specs); pos++) { + PyObject *spec = GetItem(specs, pos); if (PySequence_Contains(format, spec)) { PyList_Append(found_invalid_specs, spec); } From a952bb7077f43f6e97f1ae28eb8ecb9814a26060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Wirtel?= Date: Sun, 24 Mar 2019 14:20:40 +0100 Subject: [PATCH 15/16] _datetimemodule._check_invalid_datetime_specs only accepts tuple and not list --- Modules/_datetimemodule.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 4e9d217b9adb1a..e562fe3752c854 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -6360,18 +6360,15 @@ _check_invalid_datetime_specs(PyObject *module, PyObject *args) return NULL; } - assert(PyTuple_CheckExact(specs) || PyList_CheckExact(specs)); + assert(PyTuple_Check(specs)); PyObject *found_invalid_specs = PyList_New(0); if (found_invalid_specs == NULL) { return NULL; } - PyObject *(*GetItem)(PyObject *, Py_ssize_t) = \ - PyTuple_CheckExact(specs) ? PyTuple_GetItem : PyList_GetItem; - for (Py_ssize_t pos = 0; pos < PyObject_Size(specs); pos++) { - PyObject *spec = GetItem(specs, pos); + PyObject *spec = PyTuple_GetItem(specs, pos); if (PySequence_Contains(format, spec)) { PyList_Append(found_invalid_specs, spec); } From 412aea22b412f174610baa5bd72057fb3de26323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Wirtel?= Date: Sun, 24 Mar 2019 14:55:34 +0100 Subject: [PATCH 16/16] Fix the code with the recommendation of Martin Panter --- Doc/library/datetime.rst | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index c02fbe0f088cd9..377542eb4e7266 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -447,8 +447,8 @@ Other constructors, all class methods: Return a :class:`date` corresponding to *date_string*, parsed according to *format*. :exc:`ValueError` is raised if the date string and format can't be - parsed by :meth:`time.strptime`, or if time components are present in the - format string. For a complete list of formatting directives, see + parsed by :meth:`datetime.time.strptime`, or if time components are present + in the format string. For a complete list of formatting directives, see :ref:`strftime-strptime-behavior`. .. versionadded:: 3.8 @@ -1440,11 +1440,10 @@ Other constructors, all class methods: .. classmethod:: time.strptime(date_string, format) - Return a :class:`time` corresponding to *date_string, parsed according to - *format*. :exc:`ValueError` is raised if the date string and format can't be - parsed by :meth:`time.strptime`, if it returns a value which isn't a time - tuple, or if the date part is nonzero. For a complete list of formatting - directives, see :ref:`strftime-strptime-behavior`. + Return a :class:`datetime.time` corresponding to *date_string*, parsed + according to *format*. :exc:`ValueError` is raised if the date string and + format can't be parsed by :meth:`time.strptime`. For a complete list of + formatting directives, see :ref:`strftime-strptime-behavior`. .. versionadded:: 3.8 @@ -2044,11 +2043,13 @@ Conversely, the :meth:`datetime.strptime` class method creates a corresponding format string. ``datetime.strptime(date_string, format)`` is equivalent to ``datetime(*(time.strptime(date_string, format)[0:6]))``, except when the format includes sub-second components or timezone offset information, -which are supported in ``datetime.strptime`` but are discarded by ``time.strptime``. +which are supported in ``datetime.strptime`` but are discarded by +:meth:`time.strptime`. The :meth:`date.strptime` class method creates a :class:`date` object from a string representing a date and a corresponding format string. :exc:`ValueError` -raised if the format codes for hours, minutes, seconds, and microseconds are used. +is raised if the format codes for hours, minutes, seconds, or microseconds are +used. The :meth:`.time.strptime` class method creates a :class:`.time` object from a string representing a time and a corresponding format string. :exc:`ValueError`