Skip to content

Commit c7f5313

Browse files
committed
gh-158252: Use explicit directive flags in strptime
Replace positional flags with a lookup table and named masks, so year and day checks do not depend on the order of a directive string. Use new_datetime_ex for the exact-type fast path and Py_ISSPACE for ASCII whitespace. Add tests covering Unicode whitespace in both the input and format across all four public entry points.
1 parent 510c622 commit c7f5313

4 files changed

Lines changed: 77 additions & 20 deletions

File tree

‎Lib/test/datetimetester.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,6 +1271,16 @@ def test_strptime(self):
12711271
self.assertEqual(expected, got)
12721272
self.assertIs(type(got), date)
12731273

1274+
def test_strptime_whitespace(self):
1275+
whitespace = (' ', '\t\n\r\v\f', '\x1c', '\xa0', '\u2003', '\u202f')
1276+
for input_space in whitespace:
1277+
for format_space in whitespace:
1278+
with self.subTest(input_space=input_space,
1279+
format_space=format_space):
1280+
text = input_space.join(('2024', '02', '29'))
1281+
fmt = format_space.join(('%Y', '%m', '%d'))
1282+
self.assertEqual(date.strptime(text, fmt), date(2024, 2, 29))
1283+
12741284
def test_strptime_single_digit(self):
12751285
# bpo-34903: Check that single digit dates are allowed.
12761286
strptime = date.strptime
@@ -3181,6 +3191,17 @@ def test_strptime(self):
31813191
with self.assertRaises(ValueError): strptime("-000", "%z")
31823192
with self.assertRaises(ValueError): strptime("z", "%z")
31833193

3194+
def test_strptime_whitespace(self):
3195+
whitespace = (' ', '\t\n\r\v\f', '\x1c', '\xa0', '\u2003', '\u202f')
3196+
for input_space in whitespace:
3197+
for format_space in whitespace:
3198+
with self.subTest(input_space=input_space,
3199+
format_space=format_space):
3200+
text = input_space.join(('2024-02-29', '12:34:56'))
3201+
fmt = format_space.join(('%Y-%m-%d', '%H:%M:%S'))
3202+
self.assertEqual(self.theclass.strptime(text, fmt),
3203+
self.theclass(2024, 2, 29, 12, 34, 56))
3204+
31843205
def test_strptime_numeric_fallback(self):
31853206
cases = (
31863207
('2024\0-02-29', '%Y\0-%m-%d', (2024, 2, 29)),
@@ -4403,6 +4424,17 @@ def test_strptime(self):
44034424
self.assertEqual(expected, got)
44044425
self.assertIs(type(got), self.theclass)
44054426

4427+
def test_strptime_whitespace(self):
4428+
whitespace = (' ', '\t\n\r\v\f', '\x1c', '\xa0', '\u2003', '\u202f')
4429+
for input_space in whitespace:
4430+
for format_space in whitespace:
4431+
with self.subTest(input_space=input_space,
4432+
format_space=format_space):
4433+
text = input_space.join(('12', '34', '56'))
4434+
fmt = format_space.join(('%H', '%M', '%S'))
4435+
self.assertEqual(self.theclass.strptime(text, fmt),
4436+
self.theclass(12, 34, 56))
4437+
44064438
def test_strptime_tz(self):
44074439
strptime = self.theclass.strptime
44084440
self.assertEqual(strptime("+0002", "%z").utcoffset(), 2 * MINUTE)

‎Lib/test/test_time.py‎

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

374+
def test_strptime_whitespace(self):
375+
whitespace = (' ', '\t\n\r\v\f', '\x1c', '\xa0', '\u2003', '\u202f')
376+
for input_space in whitespace:
377+
for format_space in whitespace:
378+
with self.subTest(input_space=input_space,
379+
format_space=format_space):
380+
text = input_space.join(('2024-02-29', '12:34:56'))
381+
fmt = format_space.join(('%Y-%m-%d', '%H:%M:%S'))
382+
self.assertEqual(time.strptime(text, fmt),
383+
(2024, 2, 29, 12, 34, 56, 3, 60, -1))
384+
374385
def test_strptime_numeric_calendar(self):
375386
cases = (
376387
('0001-01-01', 1, 1, 1, 0, 1),

‎Modules/_datetimemodule.c‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5882,7 +5882,7 @@ datetime_datetime_strptime_impl(PyTypeObject *type, PyObject *string,
58825882
if (tzinfo == NULL) {
58835883
return NULL;
58845884
}
5885-
PyObject *result = new_datetime_subclass_ex(
5885+
PyObject *result = new_datetime_ex(
58865886
fields.year, fields.month, fields.day,
58875887
fields.hour, fields.minute, fields.second, fields.fraction,
58885888
tzinfo, type);

‎Python/pytime.c‎

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,13 +1447,6 @@ strptime_digits(const unsigned char *data, Py_ssize_t length, Py_ssize_t pos,
14471447
return count >= minimum ? count : 0;
14481448
}
14491449

1450-
static int
1451-
strptime_space(unsigned char c)
1452-
{
1453-
return c == ' ' || c == '\t' || c == '\n' || c == '\r' ||
1454-
c == '\f' || c == '\v';
1455-
}
1456-
14571450
static int
14581451
strptime_offset(const unsigned char *data, Py_ssize_t length, Py_ssize_t pos,
14591452
_PyTime_StrptimeFields *fields)
@@ -1466,7 +1459,7 @@ strptime_offset(const unsigned char *data, Py_ssize_t length, Py_ssize_t pos,
14661459
fields->gmtoff = 0;
14671460
return 1;
14681461
}
1469-
/* Offsets containing seconds or fractions use the Python parser. */
1462+
/* Offsets containing seconds or fractions fall back to _strptime.py. */
14701463
if ((remaining != 5 && remaining != 6) ||
14711464
(data[pos] != '+' && data[pos] != '-')) {
14721465
return 0;
@@ -1496,6 +1489,29 @@ strptime_offset(const unsigned char *data, Py_ssize_t length, Py_ssize_t pos,
14961489
int
14971490
_PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fields)
14981491
{
1492+
enum {
1493+
YEAR = 1U << 0,
1494+
SHORT_YEAR = 1U << 1,
1495+
MONTH = 1U << 2,
1496+
DAY = 1U << 3,
1497+
HOUR = 1U << 4,
1498+
MINUTE = 1U << 5,
1499+
SECOND = 1U << 6,
1500+
FRACTION = 1U << 7,
1501+
OFFSET = 1U << 8,
1502+
ANY_YEAR = YEAR | SHORT_YEAR,
1503+
};
1504+
static const unsigned short directive_bits[128] = {
1505+
['Y'] = YEAR,
1506+
['y'] = SHORT_YEAR,
1507+
['m'] = MONTH,
1508+
['d'] = DAY,
1509+
['H'] = HOUR,
1510+
['M'] = MINUTE,
1511+
['S'] = SECOND,
1512+
['f'] = FRACTION,
1513+
['z'] = OFFSET,
1514+
};
14991515
/* String subclasses and non-ASCII input retain Python's behavior. */
15001516
if (!PyUnicode_CheckExact(string) || !PyUnicode_CheckExact(format) ||
15011517
!PyUnicode_IS_ASCII(string) || !PyUnicode_IS_ASCII(format)) {
@@ -1511,16 +1527,16 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
15111527

15121528
for (Py_ssize_t i = 0; i < fmt_length; i++) {
15131529
unsigned char c = fmt[i];
1514-
if (strptime_space(c)) {
1515-
if (pos == length || !strptime_space(data[pos])) {
1530+
if (Py_ISSPACE(c)) {
1531+
if (pos == length || !Py_ISSPACE(data[pos])) {
15161532
return 0;
15171533
}
1518-
while (i + 1 < fmt_length && strptime_space(fmt[i + 1])) {
1534+
while (i + 1 < fmt_length && Py_ISSPACE(fmt[i + 1])) {
15191535
i++;
15201536
}
15211537
do {
15221538
pos++;
1523-
} while (pos < length && strptime_space(data[pos]));
1539+
} while (pos < length && Py_ISSPACE(data[pos]));
15241540
continue;
15251541
}
15261542
if (c != '%') {
@@ -1541,12 +1557,10 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
15411557
}
15421558

15431559
/* Reject duplicate groups, aliases, and locale-dependent directives. */
1544-
const char *directives = "YymdHMSfz";
1545-
const char *directive = strchr(directives, c);
1546-
if (directive == NULL || c == '\0') {
1560+
unsigned int bit = directive_bits[c];
1561+
if (bit == 0) {
15471562
return 0;
15481563
}
1549-
unsigned int bit = 1U << (directive - directives);
15501564
if (seen & bit) {
15511565
return 0;
15521566
}
@@ -1620,7 +1634,7 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
16201634
fields->second = value;
16211635
break;
16221636
case 'f':
1623-
while (count++ < 6) {
1637+
for (; count < 6; count++) {
16241638
value *= 10;
16251639
}
16261640
fields->fraction = value;
@@ -1638,8 +1652,8 @@ _PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fie
16381652
month_days += leap;
16391653
}
16401654
/* Mixed %Y/%y and day-without-year diagnostics belong to Python. */
1641-
if (pos != length || (seen & 3) == 3 ||
1642-
((seen & (1U << 3)) && !(seen & 3)) ||
1655+
if (pos != length || (seen & ANY_YEAR) == ANY_YEAR ||
1656+
((seen & DAY) && !(seen & ANY_YEAR)) ||
16431657
fields->year < 1 ||
16441658
fields->day > month_days) {
16451659
return 0;

0 commit comments

Comments
 (0)