Skip to content

Commit bf4f1dd

Browse files
committed
gh-158252: Use the C parser for all strptime entry points
Construct date, time, and struct_time results from the shared numeric fields. Preserve subclass constructors and Python fallback behavior, including leap seconds, discarded fields, and timezone metadata.
1 parent ec3bb6e commit bf4f1dd

5 files changed

Lines changed: 193 additions & 3 deletions

File tree

‎Lib/test/datetimetester.py‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,99 @@ def __divmod__(self, other):
11041104
#############################################################################
11051105
# date tests
11061106

1107+
class TestStrptime(unittest.TestCase):
1108+
def test_numeric_fields(self):
1109+
cases = (
1110+
('2024-02-29 12:34:56.123', '%Y-%m-%d %H:%M:%S.%f',
1111+
datetime(2024, 2, 29, 12, 34, 56, 123000)),
1112+
('2024-02-29T12:34:56.123+0530', '%Y-%m-%dT%H:%M:%S.%f%z',
1113+
datetime(2024, 2, 29, 12, 34, 56, 123000,
1114+
timezone(timedelta(hours=5, minutes=30)))),
1115+
('12:34:56Z', '%H:%M:%S%z', datetime(1900, 1, 1, 12, 34, 56, tzinfo=UTC)),
1116+
('12:34:56-03:30', '%H:%M:%S%z',
1117+
datetime(1900, 1, 1, 12, 34, 56,
1118+
tzinfo=timezone(-timedelta(hours=3, minutes=30)))),
1119+
('12:34:56', '%H:%M:%S%z', datetime(1900, 1, 1, 12, 34, 56)),
1120+
('', '', datetime(1900, 1, 1)),
1121+
('2024111', '%Y%m%d', datetime(2024, 11, 1)),
1122+
('2024131', '%Y%m%d', datetime(2024, 1, 31)),
1123+
('2024\0-02-29', '%Y\0-%m-%d', datetime(2024, 2, 29)),
1124+
('٢٠٢٤-02-29', '%Y-%m-%d', datetime(2024, 2, 29)),
1125+
('2024\u200302\u200329', '%Y %m %d', datetime(2024, 2, 29)),
1126+
('2024t02t29', '%YT%mt%d', datetime(2024, 2, 29)),
1127+
('24 2025-02-01', '%y %Y-%m-%d', datetime(2025, 2, 1)),
1128+
)
1129+
for text, fmt, expected in cases:
1130+
for cls, result in ((datetime, expected), (date, expected.date()),
1131+
(time, expected.timetz())):
1132+
with self.subTest(text=text, fmt=fmt, cls=cls):
1133+
actual = cls.strptime(text, fmt)
1134+
self.assertEqual(actual, result)
1135+
self.assertIs(type(actual), cls)
1136+
with self.subTest(text=text, fmt=fmt, cls=_time.struct_time):
1137+
actual = _time.strptime(text, fmt)
1138+
self.assertEqual(actual, expected.timetuple())
1139+
self.assertIsNone(actual.tm_zone)
1140+
offset = expected.utcoffset()
1141+
self.assertEqual(actual.tm_gmtoff,
1142+
None if offset is None else offset.total_seconds())
1143+
1144+
def test_invalid_fields(self):
1145+
cases = (
1146+
('2024-02-30', '%Y-%m-%d'),
1147+
('1900-02-29', '%Y-%m-%d'),
1148+
('0000-01-01', '%Y-%m-%d'),
1149+
('2024-01- 12', '%Y-%m-%d'),
1150+
('2024\0ignored', '%Y'),
1151+
('24:00:00', '%H:%M:%S'),
1152+
('23:60:00', '%H:%M:%S'),
1153+
('23:59:62', '%H:%M:%S'),
1154+
)
1155+
for text, fmt in cases:
1156+
for parse in (datetime.strptime, date.strptime, time.strptime,
1157+
_time.strptime):
1158+
with self.subTest(text=text, fmt=fmt, parse=parse):
1159+
with self.assertRaises(ValueError):
1160+
parse(text, fmt)
1161+
1162+
def test_leap_seconds(self):
1163+
for second in (60, 61):
1164+
text = f'2024-02-29 23:59:{second}'
1165+
fmt = '%Y-%m-%d %H:%M:%S'
1166+
with self.subTest(second=second):
1167+
self.assertEqual(date.strptime(text, fmt), date(2024, 2, 29))
1168+
self.assertEqual(_time.strptime(text, fmt),
1169+
(2024, 2, 29, 23, 59, second, 3, 60, -1))
1170+
for cls in (datetime, time):
1171+
with self.assertRaises(ValueError):
1172+
cls.strptime(text, fmt)
1173+
1174+
def test_offset_outside_datetime_range(self):
1175+
# date discards the offset; struct_time doesn't construct a timezone.
1176+
self.assertEqual(date.strptime('+2400', '%z'), date(1900, 1, 1))
1177+
self.assertEqual(_time.strptime('+2400', '%z').tm_gmtoff, 86400)
1178+
for cls in (datetime, time):
1179+
with self.subTest(cls=cls):
1180+
with self.assertRaises(ValueError):
1181+
cls.strptime('+2400', '%z')
1182+
1183+
def test_subclass_constructor(self):
1184+
for cls, args in ((date, (2024, 2, 29)),
1185+
(time, (12, 34, 56, 123000)),
1186+
(datetime, (2024, 2, 29, 12, 34, 56, 123000))):
1187+
class Capture(cls):
1188+
def __new__(cls, *args, **kwargs):
1189+
return args, kwargs
1190+
1191+
text = '2024-02-29 12:34:56.123'
1192+
fmt = '%Y-%m-%d %H:%M:%S.%f'
1193+
with self.subTest(cls=cls):
1194+
self.assertEqual(Capture.strptime(text, fmt), (args, {}))
1195+
aware_args = args if cls is date else args + (UTC,)
1196+
self.assertEqual(Capture.strptime(text + 'Z', fmt + '%z'),
1197+
(aware_args, {}))
1198+
1199+
11071200
class TestDateOnly(unittest.TestCase):
11081201
# Tests here won't pass if also run on datetime objects, so don't
11091202
# subclass this to test datetimes too.

‎Lib/test/test_time.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,45 @@ def test_strptime(self):
371371
self.fail("conversion specifier %r failed with '%s' input." %
372372
(format, strf_output))
373373

374+
def test_strptime_numeric_calendar(self):
375+
cases = (
376+
('0001-01-01', 1, 1, 1, 0, 1),
377+
('1900-03-01', 1900, 3, 1, 3, 60),
378+
('2000-02-29', 2000, 2, 29, 1, 60),
379+
('2000-03-01', 2000, 3, 1, 2, 61),
380+
('2024-12-31', 2024, 12, 31, 1, 366),
381+
('2100-03-01', 2100, 3, 1, 0, 60),
382+
('9999-12-31', 9999, 12, 31, 4, 365),
383+
)
384+
for text, year, month, day, weekday, yday in cases:
385+
with self.subTest(text=text):
386+
result = time.strptime(text, '%Y-%m-%d')
387+
self.assertIs(type(result), time.struct_time)
388+
self.assertEqual(result,
389+
(year, month, day, 0, 0, 0, weekday, yday, -1))
390+
self.assertIsNone(result.tm_zone)
391+
self.assertIsNone(result.tm_gmtoff)
392+
393+
def test_strptime_numeric_offset(self):
394+
for text, offset in (('', None), ('Z', 0), ('+0000', 0),
395+
('-00:00', 0), ('+0530', 19800),
396+
('-03:30', -12600), ('+2359', 86340),
397+
('+01:02:03.456', 3723)):
398+
with self.subTest(text=text):
399+
result = time.strptime('12:34:56.123' + text, '%H:%M:%S.%f%z')
400+
self.assertEqual(result, (1900, 1, 1, 12, 34, 56, 0, 1, -1))
401+
self.assertIsNone(result.tm_zone)
402+
self.assertEqual(result.tm_gmtoff, offset)
403+
404+
@support.run_with_locale('LC_TIME', 'C')
405+
def test_strptime_default_format(self):
406+
self.assertEqual(time.strptime('Thu Feb 29 12:34:56 2024'),
407+
(2024, 2, 29, 12, 34, 56, 3, 60, -1))
408+
for args in ((), ('2024', '%Y', 'extra'), (None, '%Y')):
409+
with self.subTest(args=args):
410+
with self.assertRaises(TypeError):
411+
time.strptime(*args)
412+
374413
def test_strptime_bytes(self):
375414
# Make sure only strings are accepted as arguments to strptime.
376415
self.assertRaises(TypeError, time.strptime, b'2009', "%Y")
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
Speed up :meth:`datetime.datetime.strptime` for common numeric formats by
2-
parsing fields and constructing the result in C. Other inputs continue to use
3-
the Python parser.
1+
Speed up :func:`time.strptime`, :meth:`datetime.date.strptime`,
2+
:meth:`datetime.time.strptime`, and :meth:`datetime.datetime.strptime` for
3+
common numeric formats by parsing fields and constructing the result in C.
4+
Other inputs continue to use the Python parser.

‎Modules/_datetimemodule.c‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3514,6 +3514,11 @@ datetime_date_strptime_impl(PyTypeObject *type, PyObject *string,
35143514
PyObject *format)
35153515
/*[clinic end generated code: output=454d473bee2d5161 input=2db8f0b2b5242deb]*/
35163516
{
3517+
_PyTime_StrptimeFields fields;
3518+
if (type == DATE_TYPE(NO_STATE) &&
3519+
_PyTime_Strptime(string, format, &fields)) {
3520+
return new_date_ex(fields.year, fields.month, fields.day, type);
3521+
}
35173522
PyObject *result;
35183523

35193524
PyObject *module = PyImport_Import(&_Py_ID(_strptime));
@@ -4786,6 +4791,20 @@ datetime_time_strptime_impl(PyTypeObject *type, PyObject *string,
47864791
PyObject *format)
47874792
/*[clinic end generated code: output=ae05a9bc0241d3bf input=f01d0b9eb5383da5]*/
47884793
{
4794+
_PyTime_StrptimeFields fields;
4795+
if (type == TIME_TYPE(NO_STATE) &&
4796+
_PyTime_Strptime(string, format, &fields)) {
4797+
PyObject *tzinfo = tzinfo_from_isoformat_results(
4798+
fields.gmtoff != INT_MIN, fields.gmtoff, 0);
4799+
if (tzinfo == NULL) {
4800+
return NULL;
4801+
}
4802+
PyObject *result = new_time_ex(
4803+
fields.hour, fields.minute, fields.second, fields.fraction,
4804+
tzinfo, type);
4805+
Py_DECREF(tzinfo);
4806+
return result;
4807+
}
47894808
PyObject *result;
47904809

47914810
PyObject *module = PyImport_Import(&_Py_ID(_strptime));

‎Modules/timemodule.c‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,44 @@ is not present, current time as returned by localtime() is used.\n\
989989
static PyObject *
990990
time_strptime(PyObject *self, PyObject *args)
991991
{
992+
_PyTime_StrptimeFields fields;
993+
if (PyTuple_GET_SIZE(args) == 2 &&
994+
_PyTime_Strptime(PyTuple_GET_ITEM(args, 0),
995+
PyTuple_GET_ITEM(args, 1), &fields)) {
996+
time_module_state *state = get_time_state(self);
997+
PyObject *result = PyStructSequence_New(state->struct_time_type);
998+
if (result == NULL) {
999+
return NULL;
1000+
}
1001+
/* January 1 of year 1 was a Monday. */
1002+
int year = fields.year - 1;
1003+
int weekday = (365 * year + year / 4 - year / 100 + year / 400 +
1004+
fields.yday - 1) % 7;
1005+
const int values[] = {
1006+
fields.year, fields.month, fields.day,
1007+
fields.hour, fields.minute, fields.second,
1008+
weekday, fields.yday, -1
1009+
};
1010+
for (size_t i = 0; i < Py_ARRAY_LENGTH(values); i++) {
1011+
PyObject *value = PyLong_FromLong(values[i]);
1012+
if (value == NULL) {
1013+
Py_DECREF(result);
1014+
return NULL;
1015+
}
1016+
PyStructSequence_SET_ITEM(result, i, value);
1017+
}
1018+
/* Numeric offsets don't supply a timezone name or DST information. */
1019+
PyStructSequence_SET_ITEM(result, 9, Py_NewRef(Py_None));
1020+
PyObject *offset = fields.gmtoff == INT_MIN
1021+
? Py_NewRef(Py_None)
1022+
: PyLong_FromLong(fields.gmtoff);
1023+
if (offset == NULL) {
1024+
Py_DECREF(result);
1025+
return NULL;
1026+
}
1027+
PyStructSequence_SET_ITEM(result, 10, offset);
1028+
return result;
1029+
}
9921030
PyObject *func, *result;
9931031

9941032
func = PyImport_ImportModuleAttrString("_strptime", "_strptime_time");

0 commit comments

Comments
 (0)