diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 558900dd3b9a4d..c19797e9bdec13 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2538,7 +2538,21 @@ differences between platforms in handling of unsupported format specifiers. ``%G``, ``%u`` and ``%V`` were added. .. versionadded:: 3.12 - ``%:z`` was added. + ``%:z`` was added for :meth:`~.datetime.strftime` + +.. versionadded:: 3.13 + ``%:z`` was added for :meth:`~.datetime.strptime` + +.. warning:: + + Since version 3.12, when ``%z`` directive is used in :meth:`~.datetime.strptime`, + strings formatted according ``%z`` directive are accepted and parsed correctly, + as well as strings formatted according to ``%:z``. + The later part of the behavior is unintended but it's still kept for backwards + compatibility. + Nonetheless, it's encouraged to use ``%z`` directive only to parse strings + formatted according to ``%z`` directive, while using ``%:z`` directive + for strings formatted according to ``%:z``. Technical Detail ^^^^^^^^^^^^^^^^ diff --git a/Lib/_strptime.py b/Lib/_strptime.py index e42af75af74bf5..9e85d94c6da8ee 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -202,7 +202,10 @@ def __init__(self, locale_time=None): #XXX: Does 'Y' need to worry about having less or more than # 4 digits? 'Y': r"(?P\d\d\d\d)", + # "z" shouldn't support colons. Both ":?" should be removed. However, for backwards + # compatibility, we must keep them (see gh-121237) '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'), @@ -254,13 +257,16 @@ def pattern(self, format): year_in_format = False day_of_month_in_format = False while '%' in format: - directive_index = format.index('%')+1 - format_char = format[directive_index] + directive_index = format.index('%') + 1 + directive = format[directive_index] + if directive == ":": + # Special case for "%:z", which has an extra character + directive += format[directive_index + 1] processed_format = "%s%s%s" % (processed_format, - format[:directive_index-1], - self[format_char]) - format = format[directive_index+1:] - match format_char: + format[:directive_index - 1], + self[directive]) + format = format[directive_index + len(directive):] + match directive: case 'Y' | 'y' | 'G': year_in_format = True case 'd': @@ -446,8 +452,8 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): week_of_year_start = 0 elif group_key == 'V': iso_week = int(found_dict['V']) - elif group_key == 'z': - z = found_dict['z'] + elif group_key in ('z', 'colon_z'): + z = found_dict[group_key] if z == 'Z': gmtoff = 0 else: @@ -455,7 +461,7 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): z = z[:3] + z[4:] if len(z) > 5: if z[5] != ':': - msg = f"Inconsistent use of : in {found_dict['z']}" + msg = f"Inconsistent use of : in {found_dict[group_key]}" raise ValueError(msg) z = z[:5] + z[6:] hours = int(z[1:3]) diff --git a/Lib/test/support/itertools.py b/Lib/test/support/itertools.py new file mode 100644 index 00000000000000..8994d810f124ba --- /dev/null +++ b/Lib/test/support/itertools.py @@ -0,0 +1,12 @@ +# from more_itertools v8.13.0 +def always_iterable(obj, base_type=(str, bytes)): + if obj is None: + return iter(()) + + if (base_type is not None) and isinstance(obj, base_type): + return iter((obj,)) + + try: + return iter(obj) + except TypeError: + return iter((obj,)) diff --git a/Lib/test/test_zipfile/_path/_test_params.py b/Lib/test/support/parameterize.py similarity index 91% rename from Lib/test/test_zipfile/_path/_test_params.py rename to Lib/test/support/parameterize.py index bc95b4ebf4a168..1a116d75906001 100644 --- a/Lib/test/test_zipfile/_path/_test_params.py +++ b/Lib/test/support/parameterize.py @@ -1,39 +1,39 @@ -import types -import functools - -from ._itertools import always_iterable - - -def parameterize(names, value_groups): - """ - Decorate a test method to run it as a set of subtests. - - Modeled after pytest.parametrize. - """ - - def decorator(func): - @functools.wraps(func) - def wrapped(self): - for values in value_groups: - resolved = map(Invoked.eval, always_iterable(values)) - params = dict(zip(always_iterable(names), resolved)) - with self.subTest(**params): - func(self, **params) - - return wrapped - - return decorator - - -class Invoked(types.SimpleNamespace): - """ - Wrap a function to be invoked for each usage. - """ - - @classmethod - def wrap(cls, func): - return cls(func=func) - - @classmethod - def eval(cls, cand): - return cand.func() if isinstance(cand, cls) else cand +import types +import functools + +from .itertools import always_iterable + + +def parameterize(names, value_groups): + """ + Decorate a test method to run it as a set of subtests. + + Modeled after pytest.parametrize. + """ + + def decorator(func): + @functools.wraps(func) + def wrapped(self): + for values in value_groups: + resolved = map(Invoked.eval, always_iterable(values)) + params = dict(zip(always_iterable(names), resolved)) + with self.subTest(**params): + func(self, **params) + + return wrapped + + return decorator + + +class Invoked(types.SimpleNamespace): + """ + Wrap a function to be invoked for each usage. + """ + + @classmethod + def wrap(cls, func): + return cls(func=func) + + @classmethod + def eval(cls, cand): + return cand.func() if isinstance(cand, cls) else cand diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py index 038746e26c24ad..5ec50f2a406cdb 100644 --- a/Lib/test/test_strptime.py +++ b/Lib/test/test_strptime.py @@ -8,10 +8,12 @@ import sys from test import support from test.support import skip_if_buggy_ucrt_strfptime, warnings_helper +from test.support.parameterize import parameterize from datetime import date as datetime_date import _strptime + class getlang_Tests(unittest.TestCase): """Test _getlang""" def test_basic(self): @@ -354,7 +356,7 @@ def test_julian(self): # Test julian directives self.helper('j', 7) - def test_offset(self): + def test_z_directive_offset(self): one_hour = 60 * 60 half_hour = 30 * 60 half_minute = 30 @@ -370,22 +372,39 @@ def test_offset(self): (*_, offset), _, offset_fraction = _strptime._strptime("-013030.000001", "%z") self.assertEqual(offset, -(one_hour + half_hour + half_minute)) self.assertEqual(offset_fraction, -1) - (*_, offset), _, offset_fraction = _strptime._strptime("+01:00", "%z") + + @parameterize( + ["directive"], + [ + ("%z",), + ("%:z",), + ] + ) + def test_iso_offset(self, directive: str): + """ + Tests offset for the '%:z' directive from ISO 8601. + Since '%z' directive also accepts '%:z'-formatted strings for backwards compatibility, + we're testing that here too. + """ + one_hour = 60 * 60 + half_hour = 30 * 60 + half_minute = 30 + (*_, offset), _, offset_fraction = _strptime._strptime("+01:00", directive) self.assertEqual(offset, one_hour) self.assertEqual(offset_fraction, 0) - (*_, offset), _, offset_fraction = _strptime._strptime("-01:30", "%z") + (*_, offset), _, offset_fraction = _strptime._strptime("-01:30", directive) self.assertEqual(offset, -(one_hour + half_hour)) self.assertEqual(offset_fraction, 0) - (*_, offset), _, offset_fraction = _strptime._strptime("-01:30:30", "%z") + (*_, offset), _, offset_fraction = _strptime._strptime("-01:30:30", directive) self.assertEqual(offset, -(one_hour + half_hour + half_minute)) self.assertEqual(offset_fraction, 0) - (*_, offset), _, offset_fraction = _strptime._strptime("-01:30:30.000001", "%z") + (*_, offset), _, offset_fraction = _strptime._strptime("-01:30:30.000001", directive) self.assertEqual(offset, -(one_hour + half_hour + half_minute)) self.assertEqual(offset_fraction, -1) - (*_, offset), _, offset_fraction = _strptime._strptime("+01:30:30.001", "%z") + (*_, offset), _, offset_fraction = _strptime._strptime("+01:30:30.001", directive) self.assertEqual(offset, one_hour + half_hour + half_minute) self.assertEqual(offset_fraction, 1000) - (*_, offset), _, offset_fraction = _strptime._strptime("Z", "%z") + (*_, offset), _, offset_fraction = _strptime._strptime("Z", directive) self.assertEqual(offset, 0) self.assertEqual(offset_fraction, 0) diff --git a/Lib/test/test_zipfile/_path/_itertools.py b/Lib/test/test_zipfile/_path/_itertools.py index f735dd21733006..d52402c755f21d 100644 --- a/Lib/test/test_zipfile/_path/_itertools.py +++ b/Lib/test/test_zipfile/_path/_itertools.py @@ -29,20 +29,6 @@ def __next__(self): return result -# from more_itertools v8.13.0 -def always_iterable(obj, base_type=(str, bytes)): - if obj is None: - return iter(()) - - if (base_type is not None) and isinstance(obj, base_type): - return iter((obj,)) - - try: - return iter(obj) - except TypeError: - return iter((obj,)) - - # from more_itertools v9.0.0 def consume(iterator, n=None): """Advance *iterable* by *n* steps. If *n* is ``None``, consume it diff --git a/Lib/test/test_zipfile/_path/test_path.py b/Lib/test/test_zipfile/_path/test_path.py index 99842ffd63a64e..fc784dd7c55f3e 100644 --- a/Lib/test/test_zipfile/_path/test_path.py +++ b/Lib/test/test_zipfile/_path/test_path.py @@ -10,11 +10,11 @@ import zipfile._path from test.support.os_helper import temp_dir, FakePath +from test.support.parameterize import parameterize, Invoked from ._functools import compose from ._itertools import Counter -from ._test_params import parameterize, Invoked class jaraco: diff --git a/Misc/NEWS.d/next/Library/2024-07-22-23-18-57.gh-issue-121237.WkbIpy.rst b/Misc/NEWS.d/next/Library/2024-07-22-23-18-57.gh-issue-121237.WkbIpy.rst new file mode 100644 index 00000000000000..6d9a0c04c8f0a5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-07-22-23-18-57.gh-issue-121237.WkbIpy.rst @@ -0,0 +1 @@ +Accept "%:z" in strptime pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy