Skip to content

Commit a7b624a

Browse files
matrixiseabalkinamauryfaberkerpeksagJosh-sf
committed
bpo-1100942: Add datetime.time.strptime and datetime.date.strptime
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 <[email protected]> Co-authored-by: Amaury Forgeot d'Arc <[email protected]> Co-authored-by: Berker Peksag <[email protected]> Co-authored-by: Josh-sf <[email protected]> Co-authored-by: Juarez Bochi <[email protected]> Co-authored-by: Maciej Szulik <[email protected]> Co-authored-by: Stéphane Wirtel <[email protected]> Co-authored-by: Matheus Vieira Portela <[email protected]>
1 parent 7745ec4 commit a7b624a

File tree

6 files changed

+215
-11
lines changed

6 files changed

+215
-11
lines changed

Doc/library/datetime.rst

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,14 @@ Other constructors, all class methods:
435435
date.max.toordinal()``. For any date *d*, ``date.fromordinal(d.toordinal()) ==
436436
d``.
437437

438+
.. classmethod:: date.strptime(date_string, format)
439+
440+
Return a :class:`date` corresponding to *date_string*, parsed according to
441+
*format*. :exc:`ValueError` is raised if the date string and format can't be
442+
parsed by `time.strptime`, or if it returns a value where the time part is
443+
nonzero.
444+
445+
.. versionadded:: 3.8
438446

439447
.. classmethod:: date.fromisoformat(date_string)
440448

@@ -1424,6 +1432,19 @@ day, and subject to adjustment via a :class:`tzinfo` object.
14241432
If an argument outside those ranges is given, :exc:`ValueError` is raised. All
14251433
default to ``0`` except *tzinfo*, which defaults to :const:`None`.
14261434

1435+
1436+
Other constructors, all class methods:
1437+
1438+
.. classmethod:: time.strptime(date_string, format)
1439+
1440+
Return a :class:`time` corresponding to *date_string, parsed according to
1441+
*format*. :exc:`ValueError` is raised if the date string and format can't be
1442+
parsed by `time.strptime`, if it returns a value which isn't a time tuple,
1443+
or if the date part is nonzero.
1444+
1445+
.. versionadded:: 3.8
1446+
1447+
14271448
Class attributes:
14281449

14291450

@@ -2016,13 +2037,13 @@ Conversely, the :meth:`datetime.strptime` class method creates a
20162037
corresponding format string. ``datetime.strptime(date_string, format)`` is
20172038
equivalent to ``datetime(*(time.strptime(date_string, format)[0:6]))``.
20182039

2019-
For :class:`.time` objects, the format codes for year, month, and day should not
2020-
be used, as time objects have no such values. If they're used anyway, ``1900``
2021-
is substituted for the year, and ``1`` for the month and day.
2040+
The :meth:`date.strptime` class method creates a :class:`date` object from a
2041+
string representing a date and a corresponding format string. :exc:`ValueError`
2042+
raised if the format codes for hours, minutes, seconds, and microseconds are used.
20222043

2023-
For :class:`date` objects, the format codes for hours, minutes, seconds, and
2024-
microseconds should not be used, as :class:`date` objects have no such
2025-
values. If they're used anyway, ``0`` is substituted for them.
2044+
The :meth:`.time.strptime` class method creates a :class:`.time` object from a
2045+
string representing a time and a corresponding format string. :exc:`ValueError`
2046+
raised if the format codes for year, month, and day are used.
20262047

20272048
The full set of format codes supported varies across platforms, because Python
20282049
calls the platform C library's :func:`strftime` function, and platform

Lib/_strptime.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from re import IGNORECASE
1818
from re import escape as re_escape
1919
from datetime import (date as datetime_date,
20+
datetime as datetime_datetime,
2021
timedelta as datetime_timedelta,
2122
timezone as datetime_timezone)
2223
from _thread import allocate_lock as _thread_allocate_lock
@@ -316,9 +317,9 @@ def _calc_julian_from_V(iso_year, iso_week, iso_weekday):
316317

317318

318319
def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
319-
"""Return a 2-tuple consisting of a time struct and an int containing
320+
"""Return a 3-tuple consisting of a time struct and an int containing
320321
the number of microseconds based on the input string and the
321-
format string."""
322+
format string, and the GMT offset."""
322323

323324
for index, arg in enumerate([data_string, format]):
324325
if not isinstance(arg, str):
@@ -565,6 +566,10 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
565566
hour, minute, second,
566567
weekday, julian, tz, tzname, gmtoff), fraction, gmtoff_fraction
567568

569+
date_specs = ('%a', '%A', '%b', '%B', '%c', '%d', '%j', '%m', '%U',
570+
'%w', '%W', '%x', '%y', '%Y',)
571+
time_specs = ('%T', '%R', '%H', '%I', '%M', '%S', '%f', '%i', '%s',)
572+
568573
def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"):
569574
"""Return a time struct based on the input string and the
570575
format string."""
@@ -586,3 +591,32 @@ def _strptime_datetime(cls, data_string, format="%a %b %d %H:%M:%S %Y"):
586591
args += (tz,)
587592

588593
return cls(*args)
594+
595+
def _strptime_datetime_date(data_string, format):
596+
"""Return a date based on the input string and the format string."""
597+
if not format:
598+
raise ValueError("Date format is not valid.")
599+
msg = "'{!s}' {} not valid in date format specification."
600+
if _check_invalid_datetime_specs(format, time_specs, msg):
601+
_date = _strptime_datetime(datetime_datetime, data_string, format)
602+
return _date.date()
603+
604+
def _strptime_datetime_time(data_string, format):
605+
"""Return a time based on the input string and the format string."""
606+
if not format:
607+
raise ValueError("Date format is not valid.")
608+
msg = "'{!s}' {} not valid in time format specification."
609+
if _check_invalid_datetime_specs(format, date_specs, msg):
610+
_time = _strptime_datetime(datetime_datetime, data_string, format)
611+
return _time.time()
612+
613+
def _check_invalid_datetime_specs(fmt, specs, msg):
614+
found_invalid_specs = []
615+
for spec in specs:
616+
if spec in fmt:
617+
found_invalid_specs.append(spec)
618+
if found_invalid_specs:
619+
suffix = "are" if len(found_invalid_specs) > 1 else "is"
620+
raise ValueError(msg.format(", ".join(found_invalid_specs),
621+
suffix))
622+
return True

Lib/datetime.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,7 @@ class date:
779779
fromtimestamp()
780780
today()
781781
fromordinal()
782+
strptime()
782783
783784
Operators:
784785
@@ -859,6 +860,16 @@ def fromisoformat(cls, date_string):
859860
raise ValueError('Invalid isoformat string: %s' % date_string)
860861

861862

863+
@classmethod
864+
def strptime(cls, date_string, format):
865+
"""string, format -> new date instance parsed from a string.
866+
867+
>>> datetime.date.strptime('2012/07/20', '%Y/%m/%d')
868+
datetime.date(2012, 7, 20)
869+
"""
870+
import _strptime
871+
return _strptime._strptime_datetime_date(date_string, format)
872+
862873
# Conversions to string
863874

864875
def __repr__(self):
@@ -1154,6 +1165,7 @@ class time:
11541165
Constructors:
11551166
11561167
__new__()
1168+
strptime()
11571169
11581170
Operators:
11591171
@@ -1202,6 +1214,16 @@ def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold
12021214
self._fold = fold
12031215
return self
12041216

1217+
@staticmethod
1218+
def strptime(time_string, format):
1219+
"""string, format -> new time instance parsed from a string.
1220+
1221+
>>> datetime.time.strptime('10:40am', '%H:%M%p')
1222+
datetime.time(10, 40)
1223+
"""
1224+
import _strptime
1225+
return _strptime._strptime_datetime_time(time_string, format)
1226+
12051227
# Read-only field accessors
12061228
@property
12071229
def hour(self):
@@ -1856,7 +1878,11 @@ def __str__(self):
18561878

18571879
@classmethod
18581880
def strptime(cls, date_string, format):
1859-
'string, format -> new datetime parsed from a string (like time.strptime()).'
1881+
"""string, format -> new datetime parsed from a string.
1882+
1883+
>>> datetime.datetime.strptime('2012/07/20 10:40am', '%Y/%m/%d %H:%M%p')
1884+
datetime.datetime(2012, 7, 20, 10, 40)
1885+
"""
18601886
import _strptime
18611887
return _strptime._strptime_datetime(cls, date_string, format)
18621888

Lib/test/datetimetester.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,23 @@ def test_delta_non_days_ignored(self):
944944
dt2 = dt - delta
945945
self.assertEqual(dt2, dt - days)
946946

947+
def test_strptime_valid_format(self):
948+
tests = [(('2004-12-01', '%Y-%m-%d'),
949+
date(2004, 12, 1)),
950+
(('2004', '%Y'), date(2004, 1, 1)),]
951+
for (date_string, date_format), expected in tests:
952+
self.assertEqual(expected, date.strptime(date_string, date_format))
953+
954+
def test_strptime_invalid_format(self):
955+
tests = [('2004-12-01 13:02:47.197',
956+
'%Y-%m-%d %H:%M:%S.%f'),
957+
('01', '%M'),
958+
('02', '%H'),]
959+
for test in tests:
960+
with self.assertRaises(ValueError):
961+
date.strptime(test[0], test[1])
962+
963+
947964
class SubclassDate(date):
948965
sub_var = 1
949966

@@ -2905,6 +2922,22 @@ def test_strftime(self):
29052922
# A naive object replaces %z and %Z with empty strings.
29062923
self.assertEqual(t.strftime("'%z' '%Z'"), "'' ''")
29072924

2925+
def test_strptime_invalid(self):
2926+
tests = [('2004-12-01 13:02:47.197',
2927+
'%Y-%m-%d %H:%M:%S.%f'),
2928+
('2004-12-01', '%Y-%m-%d'),]
2929+
for date_string, date_format in tests:
2930+
with self.assertRaises(ValueError):
2931+
time.strptime(date_string, date_format)
2932+
2933+
def test_strptime_valid(self):
2934+
string = '13:02:47.197'
2935+
format = '%H:%M:%S.%f'
2936+
result, frac, gmtoff = _strptime._strptime(string, format)
2937+
expected = self.theclass(*(result[3:6] + (frac, )))
2938+
got = time.strptime(string, format)
2939+
self.assertEqual(expected, got)
2940+
29082941
def test_format(self):
29092942
t = self.theclass(1, 2, 3, 4)
29102943
self.assertEqual(t.__format__(''), str(t))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add datetime.date.strptime and datetime.time.strptime class methods.

Modules/_datetimemodule.c

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ static PyTypeObject PyDateTime_DeltaType;
119119
static PyTypeObject PyDateTime_TimeType;
120120
static PyTypeObject PyDateTime_TZInfoType;
121121
static PyTypeObject PyDateTime_TimeZoneType;
122+
static PyObject *datetime_strptime(PyObject *cls, PyObject *args);
123+
static PyObject *datetime_getdate(PyDateTime_DateTime *self);
124+
static PyObject *datetime_gettime(PyDateTime_DateTime *self);
125+
122126

123127
static int check_tzinfo_subclass(PyObject *p);
124128

@@ -2870,6 +2874,33 @@ date_fromordinal(PyObject *cls, PyObject *args)
28702874
return result;
28712875
}
28722876

2877+
2878+
/* Return new date from time.strptime(). */
2879+
static PyObject *
2880+
date_strptime(PyObject *cls, PyObject *args)
2881+
{
2882+
PyObject *date = NULL;
2883+
PyObject *datetime;
2884+
2885+
datetime = datetime_strptime((PyObject *)&PyDateTime_DateTimeType, args);
2886+
2887+
if (datetime == NULL)
2888+
return NULL;
2889+
2890+
if (DATE_GET_HOUR(datetime) ||
2891+
DATE_GET_MINUTE(datetime) ||
2892+
DATE_GET_SECOND(datetime) ||
2893+
DATE_GET_MICROSECOND(datetime))
2894+
PyErr_SetString(PyExc_ValueError,
2895+
"date.strptime value cannot have a time part");
2896+
else
2897+
date = datetime_getdate((PyDateTime_DateTime *)datetime);
2898+
2899+
Py_DECREF(datetime);
2900+
return date;
2901+
}
2902+
2903+
28732904
/* Return the new date from a string as generated by date.isoformat() */
28742905
static PyObject *
28752906
date_fromisoformat(PyObject *cls, PyObject *dtstr) {
@@ -3203,6 +3234,11 @@ static PyMethodDef date_methods[] = {
32033234
PyDoc_STR("Current date or datetime: same as "
32043235
"self.__class__.fromtimestamp(time.time()).")},
32053236

3237+
{"strptime", (PyCFunction)date_strptime, METH_VARARGS | METH_CLASS,
3238+
PyDoc_STR("string, format -> new date instance parsed from a string.\n\n"
3239+
">>> datetime.date.strptime('2012/07/20', '%Y/%m/%d')\n"
3240+
"datetime.date(2012, 7, 20)")},
3241+
32063242
/* Instance methods: */
32073243

32083244
{"ctime", (PyCFunction)date_ctime, METH_NOARGS,
@@ -3916,6 +3952,49 @@ time_new(PyTypeObject *type, PyObject *args, PyObject *kw)
39163952
return self;
39173953
}
39183954

3955+
3956+
/* Return new time from time.strptime(). */
3957+
static PyObject *
3958+
time_strptime(PyObject *cls, PyObject *args)
3959+
{
3960+
PyObject *time = NULL;
3961+
PyObject *datetime;
3962+
3963+
static PyObject *emptyDatetime = NULL;
3964+
3965+
/* To ensure that the given string does not contain a date,
3966+
* compare with the result of an empty date string.
3967+
*/
3968+
if (emptyDatetime == NULL) {
3969+
PyObject *emptyStringPair = Py_BuildValue("ss", "", "");
3970+
if (emptyStringPair == NULL)
3971+
return NULL;
3972+
emptyDatetime = datetime_strptime(
3973+
(PyObject *)&PyDateTime_DateTimeType,
3974+
emptyStringPair);
3975+
Py_DECREF(emptyStringPair);
3976+
if (emptyDatetime == NULL)
3977+
return NULL;
3978+
}
3979+
3980+
datetime = datetime_strptime((PyObject *)&PyDateTime_DateTimeType, args);
3981+
3982+
if (datetime == NULL)
3983+
return NULL;
3984+
3985+
if (GET_YEAR(datetime) != GET_YEAR(emptyDatetime)
3986+
|| GET_MONTH(datetime) != GET_MONTH(emptyDatetime)
3987+
|| GET_DAY(datetime) != GET_DAY(emptyDatetime))
3988+
PyErr_SetString(PyExc_ValueError,
3989+
"time.strptime value cannot have a date part");
3990+
else
3991+
time = datetime_gettime((PyDateTime_DateTime *)datetime);
3992+
3993+
Py_DECREF(datetime);
3994+
return time;
3995+
}
3996+
3997+
39193998
/*
39203999
* Destructor.
39214000
*/
@@ -4334,6 +4413,14 @@ time_reduce(PyDateTime_Time *self, PyObject *arg)
43344413
}
43354414

43364415
static PyMethodDef time_methods[] = {
4416+
/* Class methods: */
4417+
4418+
{"strptime", (PyCFunction)time_strptime, METH_VARARGS | METH_CLASS,
4419+
PyDoc_STR("string, format -> new time parsed from a string.\n\n"
4420+
">>> datetime.time.strptime('10:40am', '%H:%M%p')\n"
4421+
"datetime.time(10, 40)")},
4422+
4423+
/* Instance methods: */
43374424

43384425
{"isoformat", (PyCFunction)time_isoformat, METH_VARARGS | METH_KEYWORDS,
43394426
PyDoc_STR("Return string in ISO 8601 format, [HH[:MM[:SS[.mmm[uuu]]]]]"
@@ -5891,8 +5978,10 @@ static PyMethodDef datetime_methods[] = {
58915978

58925979
{"strptime", (PyCFunction)datetime_strptime,
58935980
METH_VARARGS | METH_CLASS,
5894-
PyDoc_STR("string, format -> new datetime parsed from a string "
5895-
"(like time.strptime()).")},
5981+
PyDoc_STR("string, format -> new datetime parsed from a string.\n\n"
5982+
">>> datetime.datetime.strptime('2012/07/20 10:40am', "
5983+
"'%Y/%m/%d %H:%M%p')\n"
5984+
"datetime.datetime(2012, 7, 20, 10, 40)")},
58965985

58975986
{"combine", (PyCFunction)datetime_combine,
58985987
METH_VARARGS | METH_KEYWORDS | METH_CLASS,

0 commit comments

Comments
 (0)