From 9a93927b6bd5d85ab1c8a7d51b9a6b8c4eb6e8b4 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Fri, 25 Apr 2025 11:50:27 +0100 Subject: [PATCH 1/2] commit --- Lib/_strptime.py | 45 ++++++++++--------- Lib/test/datetimetester.py | 6 +++ ...-04-25-11-48-00.gh-issue-122781.ajsdns.rst | 2 + 3 files changed, 31 insertions(+), 22 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-04-25-11-48-00.gh-issue-122781.ajsdns.rst diff --git a/Lib/_strptime.py b/Lib/_strptime.py index aa63933a49d16d..ae67949626d460 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -302,7 +302,7 @@ def __init__(self, locale_time=None): # W is set below by using 'U' 'y': r"(?P\d\d)", 'Y': r"(?P\d\d\d\d)", - 'z': r"(?P[+-]\d\d:?[0-5]\d(:?[0-5]\d(\.\d{1,6})?)?|(?-i:Z))", + 'z': r"(?P([+-]\d\d:?[0-5]\d(:?[0-5]\d(\.\d{1,6})?)?)|(?-i:Z))?", 'A': self.__seqToRE(self.locale_time.f_weekday, 'A'), 'a': self.__seqToRE(self.locale_time.a_weekday, 'a'), 'B': self.__seqToRE(self.locale_time.f_month[1:], 'B'), @@ -548,27 +548,28 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): iso_week = int(found_dict['V']) elif group_key == 'z': z = found_dict['z'] - if z == 'Z': - gmtoff = 0 - else: - if z[3] == ':': - z = z[:3] + z[4:] - if len(z) > 5: - if z[5] != ':': - msg = f"Inconsistent use of : in {found_dict['z']}" - raise ValueError(msg) - z = z[:5] + z[6:] - hours = int(z[1:3]) - minutes = int(z[3:5]) - seconds = int(z[5:7] or 0) - gmtoff = (hours * 60 * 60) + (minutes * 60) + seconds - gmtoff_remainder = z[8:] - # Pad to always return microseconds. - gmtoff_remainder_padding = "0" * (6 - len(gmtoff_remainder)) - gmtoff_fraction = int(gmtoff_remainder + gmtoff_remainder_padding) - if z.startswith("-"): - gmtoff = -gmtoff - gmtoff_fraction = -gmtoff_fraction + if z: + if z == 'Z': + gmtoff = 0 + else: + if z[3] == ':': + z = z[:3] + z[4:] + if len(z) > 5: + if z[5] != ':': + msg = f"Inconsistent use of : in {found_dict['z']}" + raise ValueError(msg) + z = z[:5] + z[6:] + hours = int(z[1:3]) + minutes = int(z[3:5]) + seconds = int(z[5:7] or 0) + gmtoff = (hours * 60 * 60) + (minutes * 60) + seconds + gmtoff_remainder = z[8:] + # Pad to always return microseconds. + gmtoff_remainder_padding = "0" * (6 - len(gmtoff_remainder)) + gmtoff_fraction = int(gmtoff_remainder + gmtoff_remainder_padding) + if z.startswith("-"): + gmtoff = -gmtoff + gmtoff_fraction = -gmtoff_fraction elif group_key == 'Z': # Since -1 is default value only need to worry about setting tz if # it can be something other than -1. diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 55844ec35a90c9..7cd8dc952d6304 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2921,6 +2921,12 @@ def test_strptime(self): with self.assertRaises(ValueError): strptime("-000", "%z") with self.assertRaises(ValueError): strptime("z", "%z") + # gh-issue: 122781; empty %z should pass + string = '2025-04-25 11:42:47' + format = '%Y-%m-%d %H:%M:%S%z' + result = self.theclass.strptime(string, format) + self.assertEqual(result, self.theclass(2025, 4, 25, 11, 42, 47)) + def test_strptime_single_digit(self): # bpo-34903: Check that single digit dates and times are allowed. diff --git a/Misc/NEWS.d/next/Library/2025-04-25-11-48-00.gh-issue-122781.ajsdns.rst b/Misc/NEWS.d/next/Library/2025-04-25-11-48-00.gh-issue-122781.ajsdns.rst new file mode 100644 index 00000000000000..5a9a0cdf7986dc --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-25-11-48-00.gh-issue-122781.ajsdns.rst @@ -0,0 +1,2 @@ +Fix ``%z`` directive in :func:`datetime.datetime.strptime` to allow for no provided +offset as was documented. From ff751c8f7b937605b9cb75b95d6335bc5fe10621 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Tue, 20 May 2025 16:51:57 +0100 Subject: [PATCH 2/2] Move tests --- Lib/test/datetimetester.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 7cd8dc952d6304..352c1716b7605f 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2921,12 +2921,6 @@ def test_strptime(self): with self.assertRaises(ValueError): strptime("-000", "%z") with self.assertRaises(ValueError): strptime("z", "%z") - # gh-issue: 122781; empty %z should pass - string = '2025-04-25 11:42:47' - format = '%Y-%m-%d %H:%M:%S%z' - result = self.theclass.strptime(string, format) - self.assertEqual(result, self.theclass(2025, 4, 25, 11, 42, 47)) - def test_strptime_single_digit(self): # bpo-34903: Check that single digit dates and times are allowed. @@ -2975,6 +2969,17 @@ def test_strptime_leap_year(self): with self._assertNotWarns(DeprecationWarning): self.theclass.strptime('02-29,2024', '%m-%d,%Y') + def test_strptime_z_empty(self): + for directive in ('z',): + string = '2025-04-25 11:42:47' + format = f'%Y-%m-%d %H:%M:%S%{directive}' + target = self.theclass(2025, 4, 25, 11, 42, 47) + with self.subTest(string=string, + format=format, + target=target): + result = self.theclass.strptime(string, format) + self.assertEqual(result, target) + def test_more_timetuple(self): # This tests fields beyond those tested by the TestDate.test_timetuple. t = self.theclass(2004, 12, 31, 6, 22, 33)