diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 1ee23c2175a27d..377542eb4e7266 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -443,6 +443,15 @@ 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 :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 .. classmethod:: date.fromisoformat(date_string) @@ -1426,6 +1435,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:`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 + + Class attributes: @@ -2021,15 +2043,17 @@ 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`. -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` +is raised if the format codes for hours, minutes, seconds, or 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/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/Lib/_strptime.py b/Lib/_strptime.py index f4f3c0b80c1d05..50013c20c0dc43 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 UTC 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', '%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"): """Return a time struct based on the input string and the format string.""" @@ -577,3 +582,19 @@ 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.""" + 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() + +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() 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..2c8717d1c72e19 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1017,6 +1017,30 @@ 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: + 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'), + ('2018-01-01', ''), + ('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): sub_var = 1 @@ -3105,6 +3129,25 @@ 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'), + ('12:30:15', ''), + ] + for date_string, date_format in tests: + 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' + 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..788023cedb672d --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2018-02-07-10-55-58.bpo-1100942.md4agI.rst @@ -0,0 +1 @@ +Add :func:`datetime.date.strptime` and :func:`datetime.time.strptime` class methods. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index c1557b5e6f491d..e562fe3752c854 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, PyObject *Py_UNUSED(ignored)); +static PyObject *datetime_gettime(PyDateTime_DateTime *self, PyObject *Py_UNUSED(ignored)); + static int check_tzinfo_subclass(PyObject *p); @@ -2946,6 +2950,35 @@ date_fromordinal(PyObject *cls, PyObject *args) return result; } + +/* Return new date from date.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, NULL); + } + + 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 +3316,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 +4064,54 @@ 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, NULL); + } + + Py_DECREF(datetime); + return time; +} + + /* * Destructor. */ @@ -4451,6 +4537,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 +6217,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, @@ -6255,7 +6351,63 @@ 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; + } + + assert(PyTuple_Check(specs)); + + PyObject *found_invalid_specs = PyList_New(0); + if (found_invalid_specs == NULL) { + return NULL; + } + + for (Py_ssize_t pos = 0; pos < PyObject_Size(specs); pos++) { + PyObject *spec = PyTuple_GetItem(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} };