Skip to content

Commit 96742aa

Browse files
committed
gh-158252: Move the C parser to Python/_strptime.c
Keep strptime parsing separate from the clock and timestamp helpers in pytime.c. Update the Unix and Windows builds, including _freeze_module. The parser is unchanged in this commit, so the move can be reverted independently of the review fixes.
1 parent c7f5313 commit 96742aa

7 files changed

Lines changed: 257 additions & 246 deletions

File tree

‎Makefile.pre.in‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,7 @@ PARSER_HEADERS= \
434434

435435
PYTHON_OBJS= \
436436
Python/_contextvars.o \
437+
Python/_strptime.o \
437438
Python/_warnings.o \
438439
Python/Python-ast.o \
439440
Python/Python-tokenize.o \

‎PCbuild/_freeze_module.vcxproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@
194194
<ClCompile Include="..\PC\invalid_parameter_handler.c" />
195195
<ClCompile Include="..\PC\msvcrtmodule.c" />
196196
<ClCompile Include="..\PC\winreg.c" />
197+
<ClCompile Include="..\Python\_strptime.c" />
197198
<ClCompile Include="..\Python\_warnings.c" />
198199
<ClCompile Include="..\Python\asdl.c" />
199200
<ClCompile Include="..\Python\assemble.c" />

‎PCbuild/_freeze_module.vcxproj.filters‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
<ClCompile Include="..\Modules\_tracemalloc.c">
2020
<Filter>Source Files</Filter>
2121
</ClCompile>
22+
<ClCompile Include="..\Python\_strptime.c">
23+
<Filter>Source Files</Filter>
24+
</ClCompile>
2225
<ClCompile Include="..\Python\_warnings.c">
2326
<Filter>Source Files</Filter>
2427
</ClCompile>

‎PCbuild/pythoncore.vcxproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,7 @@
614614
<ClCompile Include="..\PC\msvcrtmodule.c" />
615615
<ClCompile Include="..\Python\pyhash.c" />
616616
<ClCompile Include="..\Python\_contextvars.c" />
617+
<ClCompile Include="..\Python\_strptime.c" />
617618
<ClCompile Include="..\Python\_warnings.c" />
618619
<ClCompile Include="..\Python\asdl.c" />
619620
<ClCompile Include="..\Python\assemble.c" />

‎PCbuild/pythoncore.vcxproj.filters‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,6 +1394,9 @@
13941394
<ClCompile Include="..\Python\_contextvars.c">
13951395
<Filter>Python</Filter>
13961396
</ClCompile>
1397+
<ClCompile Include="..\Python\_strptime.c">
1398+
<Filter>Python</Filter>
1399+
</ClCompile>
13971400
<ClCompile Include="..\Python\_warnings.c">
13981401
<Filter>Python</Filter>
13991402
</ClCompile>

‎Python/_strptime.c‎

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
#include "Python.h"
2+
#include "pycore_time.h"
3+
4+
5+
/* Implement common parts of strptime in C to improve performance. Callers
6+
* fall back to Lib/_strptime.py for a full parse when this doesn't match.
7+
*
8+
* Supported inputs are exact str objects containing only ASCII, with:
9+
* - numeric %Y, %y, %m, %d, %H, %M, %S, and %f fields;
10+
* - a terminal %z: empty, Z, or +/-HHMM or +/-HH:MM with hours below 24;
11+
* - literal characters, %%, and ASCII whitespace.
12+
* Duplicate directives, mixed %Y/%y, and day-of-month without a year use the
13+
* fallback. So do other directives (including locale-dependent names and
14+
* week/day-of-year calculations), and offsets with seconds or fractions.
15+
*/
16+
17+
static int
18+
strptime_digits(const unsigned char *data, Py_ssize_t length, Py_ssize_t pos,
19+
int minimum, int maximum, int *value)
20+
{
21+
int count = 0;
22+
*value = 0;
23+
while (count < maximum && pos + count < length) {
24+
unsigned char c = data[pos + count];
25+
if (c < '0' || c > '9') {
26+
break;
27+
}
28+
*value = *value * 10 + c - '0';
29+
count++;
30+
}
31+
return count >= minimum ? count : 0;
32+
}
33+
34+
static int
35+
strptime_offset(const unsigned char *data, Py_ssize_t length, Py_ssize_t pos,
36+
_PyTime_StrptimeFields *fields)
37+
{
38+
Py_ssize_t remaining = length - pos;
39+
if (remaining == 0) {
40+
return 1;
41+
}
42+
if (remaining == 1 && data[pos] == 'Z') {
43+
fields->gmtoff = 0;
44+
return 1;
45+
}
46+
/* Offsets containing seconds or fractions fall back to _strptime.py. */
47+
if ((remaining != 5 && remaining != 6) ||
48+
(data[pos] != '+' && data[pos] != '-')) {
49+
return 0;
50+
}
51+
int hours, minutes;
52+
int colon = remaining == 6;
53+
if ((colon && data[pos + 3] != ':') ||
54+
strptime_digits(data, length, pos + 1, 2, 2, &hours) != 2 ||
55+
strptime_digits(data, length, pos + 3 + colon, 2, 2, &minutes) != 2 ||
56+
hours > 23 || minutes > 59) {
57+
return 0;
58+
}
59+
fields->gmtoff = (hours * 3600 + minutes * 60) *
60+
(data[pos] == '-' ? -1 : 1);
61+
return 1;
62+
}
63+
64+
/* Return 1 for a complete numeric match, or 0 if fallback is required.
65+
*
66+
* Parsing doesn't allocate or set exceptions. Some mismatches require regex
67+
* backtracking, but even inputs known to be invalid use the Python parser:
68+
* it owns the exception messages and error precedence. For example, month 13
69+
* and empty input can't be repaired by backtracking, but diagnosing them here
70+
* would duplicate the Python parser's error handling. A return value of 0
71+
* therefore doesn't distinguish invalid input from an unsupported format.
72+
*/
73+
int
74+
_PyTime_Strptime(PyObject *string, PyObject *format, _PyTime_StrptimeFields *fields)
75+
{
76+
enum {
77+
YEAR = 1U << 0,
78+
SHORT_YEAR = 1U << 1,
79+
MONTH = 1U << 2,
80+
DAY = 1U << 3,
81+
HOUR = 1U << 4,
82+
MINUTE = 1U << 5,
83+
SECOND = 1U << 6,
84+
FRACTION = 1U << 7,
85+
OFFSET = 1U << 8,
86+
ANY_YEAR = YEAR | SHORT_YEAR,
87+
};
88+
static const unsigned short directive_bits[128] = {
89+
['Y'] = YEAR,
90+
['y'] = SHORT_YEAR,
91+
['m'] = MONTH,
92+
['d'] = DAY,
93+
['H'] = HOUR,
94+
['M'] = MINUTE,
95+
['S'] = SECOND,
96+
['f'] = FRACTION,
97+
['z'] = OFFSET,
98+
};
99+
/* String subclasses and non-ASCII input retain Python's behavior. */
100+
if (!PyUnicode_CheckExact(string) || !PyUnicode_CheckExact(format) ||
101+
!PyUnicode_IS_ASCII(string) || !PyUnicode_IS_ASCII(format)) {
102+
return 0;
103+
}
104+
const unsigned char *data = PyUnicode_1BYTE_DATA(string);
105+
const unsigned char *fmt = PyUnicode_1BYTE_DATA(format);
106+
Py_ssize_t length = PyUnicode_GET_LENGTH(string);
107+
Py_ssize_t fmt_length = PyUnicode_GET_LENGTH(format);
108+
*fields = (_PyTime_StrptimeFields){.year = 1900, .month = 1, .day = 1, .gmtoff = INT_MIN};
109+
unsigned int seen = 0;
110+
Py_ssize_t pos = 0;
111+
112+
for (Py_ssize_t i = 0; i < fmt_length; i++) {
113+
unsigned char c = fmt[i];
114+
if (Py_ISSPACE(c)) {
115+
if (pos == length || !Py_ISSPACE(data[pos])) {
116+
return 0;
117+
}
118+
while (i + 1 < fmt_length && Py_ISSPACE(fmt[i + 1])) {
119+
i++;
120+
}
121+
do {
122+
pos++;
123+
} while (pos < length && Py_ISSPACE(data[pos]));
124+
continue;
125+
}
126+
if (c != '%') {
127+
if (pos == length || data[pos++] != c) {
128+
return 0;
129+
}
130+
continue;
131+
}
132+
if (++i == fmt_length) {
133+
return 0;
134+
}
135+
c = fmt[i];
136+
if (c == '%') {
137+
if (pos == length || data[pos++] != '%') {
138+
return 0;
139+
}
140+
continue;
141+
}
142+
143+
/* Reject duplicate groups, aliases, and locale-dependent directives. */
144+
unsigned int bit = directive_bits[c];
145+
if (bit == 0) {
146+
return 0;
147+
}
148+
if (seen & bit) {
149+
return 0;
150+
}
151+
seen |= bit;
152+
153+
if (c == 'z') {
154+
if (i != fmt_length - 1 ||
155+
!strptime_offset(data, length, pos, fields)) {
156+
return 0;
157+
}
158+
pos = length;
159+
continue;
160+
}
161+
162+
int minimum = 1;
163+
int maximum = 2;
164+
if (c == 'Y') {
165+
minimum = maximum = 4;
166+
}
167+
else if (c == 'y') {
168+
minimum = maximum = 2;
169+
}
170+
else if (c == 'f') {
171+
maximum = 6;
172+
}
173+
else if ((c == 'd' || c == 'H') && pos < length && data[pos] == ' ') {
174+
pos++;
175+
maximum = 1;
176+
}
177+
int value;
178+
int count = strptime_digits(data, length, pos, minimum, maximum, &value);
179+
if (count == 0) {
180+
return 0;
181+
}
182+
pos += count;
183+
switch (c) {
184+
case 'Y':
185+
fields->year = value;
186+
break;
187+
case 'y':
188+
fields->year = value + (value <= 68 ? 2000 : 1900);
189+
break;
190+
case 'm':
191+
if (value < 1 || value > 12) {
192+
return 0;
193+
}
194+
fields->month = value;
195+
break;
196+
case 'd':
197+
if (value < 1 || value > 31) {
198+
return 0;
199+
}
200+
fields->day = value;
201+
break;
202+
case 'H':
203+
if (value > 23) {
204+
return 0;
205+
}
206+
fields->hour = value;
207+
break;
208+
case 'M':
209+
if (value > 59) {
210+
return 0;
211+
}
212+
fields->minute = value;
213+
break;
214+
case 'S':
215+
if (value > 61) {
216+
return 0;
217+
}
218+
fields->second = value;
219+
break;
220+
case 'f':
221+
for (; count < 6; count++) {
222+
value *= 10;
223+
}
224+
fields->fraction = value;
225+
break;
226+
}
227+
}
228+
static const int days_before_month[] = {
229+
0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365
230+
};
231+
int leap = fields->year % 4 == 0 &&
232+
(fields->year % 100 != 0 || fields->year % 400 == 0);
233+
int month_days = days_before_month[fields->month + 1] -
234+
days_before_month[fields->month];
235+
if (fields->month == 2) {
236+
month_days += leap;
237+
}
238+
/* Mixed %Y/%y and day-without-year diagnostics belong to Python. */
239+
if (pos != length || (seen & ANY_YEAR) == ANY_YEAR ||
240+
((seen & DAY) && !(seen & ANY_YEAR)) ||
241+
fields->year < 1 ||
242+
fields->day > month_days) {
243+
return 0;
244+
}
245+
fields->yday = days_before_month[fields->month] + fields->day +
246+
(fields->month > 2 && leap);
247+
return 1;
248+
}

0 commit comments

Comments
 (0)