Skip to content

Commit 3ae4449

Browse files
committed
gh-121237: Add %:z directive to strptime
1 parent a9bb3c7 commit 3ae4449

File tree

4 files changed

+58
-17
lines changed

4 files changed

+58
-17
lines changed

Doc/library/datetime.rst

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2538,7 +2538,21 @@ differences between platforms in handling of unsupported format specifiers.
25382538
``%G``, ``%u`` and ``%V`` were added.
25392539

25402540
.. versionadded:: 3.12
2541-
``%:z`` was added.
2541+
``%:z`` was added for :meth:`~.datetime.strftime`
2542+
2543+
.. versionadded:: 3.13
2544+
``%:z`` was added for :meth:`~.datetime.strptime`
2545+
2546+
.. warning::
2547+
2548+
Since version 3.12, when ``%z`` directive is used in :meth:`~.datetime.strptime`,
2549+
strings formatted according ``%z`` directive are accepted and parsed correctly,
2550+
as well as strings formatted according to ``%:z``.
2551+
The later part of the behavior is unintended but it's still kept for backwards
2552+
compatibility.
2553+
Nonetheless, it's encouraged to use ``%z`` directive only to parse strings
2554+
formatted according to ``%z`` directive, while using ``%:z`` directive
2555+
for strings formatted according to ``%:z``.
25422556

25432557
Technical Detail
25442558
^^^^^^^^^^^^^^^^

Lib/_strptime.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,10 @@ def __init__(self, locale_time=None):
202202
#XXX: Does 'Y' need to worry about having less or more than
203203
# 4 digits?
204204
'Y': r"(?P<Y>\d\d\d\d)",
205+
# "z" shouldn't support colons. Both ":?" should be removed. However, for backwards
206+
# compatibility, we must keep them (see gh-121237)
205207
'z': r"(?P<z>[+-]\d\d:?[0-5]\d(:?[0-5]\d(\.\d{1,6})?)?|(?-i:Z))",
208+
':z': r"(?P<colon_z>[+-]\d\d:[0-5]\d(:[0-5]\d(\.\d{1,6})?)?|(?-i:Z))",
206209
'A': self.__seqToRE(self.locale_time.f_weekday, 'A'),
207210
'a': self.__seqToRE(self.locale_time.a_weekday, 'a'),
208211
'B': self.__seqToRE(self.locale_time.f_month[1:], 'B'),
@@ -254,13 +257,16 @@ def pattern(self, format):
254257
year_in_format = False
255258
day_of_month_in_format = False
256259
while '%' in format:
257-
directive_index = format.index('%')+1
258-
format_char = format[directive_index]
260+
directive_index = format.index('%') + 1
261+
directive = format[directive_index]
262+
if directive == ":":
263+
# Special case for "%:z", which has an extra character
264+
directive += format[directive_index + 1]
259265
processed_format = "%s%s%s" % (processed_format,
260-
format[:directive_index-1],
261-
self[format_char])
262-
format = format[directive_index+1:]
263-
match format_char:
266+
format[:directive_index - 1],
267+
self[directive])
268+
format = format[directive_index + len(directive):]
269+
match directive:
264270
case 'Y' | 'y' | 'G':
265271
year_in_format = True
266272
case 'd':
@@ -446,16 +452,16 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
446452
week_of_year_start = 0
447453
elif group_key == 'V':
448454
iso_week = int(found_dict['V'])
449-
elif group_key == 'z':
450-
z = found_dict['z']
455+
elif group_key in ('z', 'colon_z'):
456+
z = found_dict[group_key]
451457
if z == 'Z':
452458
gmtoff = 0
453459
else:
454460
if z[3] == ':':
455461
z = z[:3] + z[4:]
456462
if len(z) > 5:
457463
if z[5] != ':':
458-
msg = f"Inconsistent use of : in {found_dict['z']}"
464+
msg = f"Inconsistent use of : in {z}"
459465
raise ValueError(msg)
460466
z = z[:5] + z[6:]
461467
hours = int(z[1:3])

Lib/test/test_strptime.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212

1313
import _strptime
1414

15+
from Lib.test.test_zipfile._path._test_params import parameterize
16+
17+
1518
class getlang_Tests(unittest.TestCase):
1619
"""Test _getlang"""
1720
def test_basic(self):
@@ -354,7 +357,7 @@ def test_julian(self):
354357
# Test julian directives
355358
self.helper('j', 7)
356359

357-
def test_offset(self):
360+
def test_z_directive_offset(self):
358361
one_hour = 60 * 60
359362
half_hour = 30 * 60
360363
half_minute = 30
@@ -370,22 +373,39 @@ def test_offset(self):
370373
(*_, offset), _, offset_fraction = _strptime._strptime("-013030.000001", "%z")
371374
self.assertEqual(offset, -(one_hour + half_hour + half_minute))
372375
self.assertEqual(offset_fraction, -1)
373-
(*_, offset), _, offset_fraction = _strptime._strptime("+01:00", "%z")
376+
377+
@parameterize(
378+
["directive"],
379+
[
380+
("%z",),
381+
("%:z",),
382+
]
383+
)
384+
def test_iso_offset(self, directive: str):
385+
"""
386+
Tests offset for the '%:z' directive from ISO 8601.
387+
Since '%z' directive also accepts '%:z'-formatted strings for backwards compatibility,
388+
we're testing that here too.
389+
"""
390+
one_hour = 60 * 60
391+
half_hour = 30 * 60
392+
half_minute = 30
393+
(*_, offset), _, offset_fraction = _strptime._strptime("+01:00", directive)
374394
self.assertEqual(offset, one_hour)
375395
self.assertEqual(offset_fraction, 0)
376-
(*_, offset), _, offset_fraction = _strptime._strptime("-01:30", "%z")
396+
(*_, offset), _, offset_fraction = _strptime._strptime("-01:30", directive)
377397
self.assertEqual(offset, -(one_hour + half_hour))
378398
self.assertEqual(offset_fraction, 0)
379-
(*_, offset), _, offset_fraction = _strptime._strptime("-01:30:30", "%z")
399+
(*_, offset), _, offset_fraction = _strptime._strptime("-01:30:30", directive)
380400
self.assertEqual(offset, -(one_hour + half_hour + half_minute))
381401
self.assertEqual(offset_fraction, 0)
382-
(*_, offset), _, offset_fraction = _strptime._strptime("-01:30:30.000001", "%z")
402+
(*_, offset), _, offset_fraction = _strptime._strptime("-01:30:30.000001", directive)
383403
self.assertEqual(offset, -(one_hour + half_hour + half_minute))
384404
self.assertEqual(offset_fraction, -1)
385-
(*_, offset), _, offset_fraction = _strptime._strptime("+01:30:30.001", "%z")
405+
(*_, offset), _, offset_fraction = _strptime._strptime("+01:30:30.001", directive)
386406
self.assertEqual(offset, one_hour + half_hour + half_minute)
387407
self.assertEqual(offset_fraction, 1000)
388-
(*_, offset), _, offset_fraction = _strptime._strptime("Z", "%z")
408+
(*_, offset), _, offset_fraction = _strptime._strptime("Z", directive)
389409
self.assertEqual(offset, 0)
390410
self.assertEqual(offset_fraction, 0)
391411

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Accept "%:z" in strptime

0 commit comments

Comments
 (0)