From 827db4591600305dc1a686ea6a71606366573cc3 Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Mon, 30 Sep 2024 04:00:20 +0200 Subject: [PATCH 01/23] gh-124529: Fix _strptime to make %c/%x accept year with fewer digits --- Lib/_strptime.py | 30 +++++++-- Lib/test/datetimetester.py | 131 +++++++++++++++++++++++++++++++++++++ Lib/test/test_strptime.py | 129 ++++++++++++++++++++++++++++++++++-- 3 files changed, 282 insertions(+), 8 deletions(-) diff --git a/Lib/_strptime.py b/Lib/_strptime.py index a3f8bb544d518d..c3dda4aed0acab 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -199,8 +199,6 @@ def __init__(self, locale_time=None): 'V': r"(?P5[0-3]|0[1-9]|[1-4]\d|\d)", # W is set below by using 'U' 'y': r"(?P\d\d)", - #XXX: Does 'Y' need to worry about having less or more than - # 4 digits? 'Y': r"(?P\d\d\d\d)", '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'), @@ -213,8 +211,10 @@ def __init__(self, locale_time=None): 'Z'), '%': '%'}) base.__setitem__('W', base.__getitem__('U').replace('U', 'W')) - base.__setitem__('c', self.pattern(self.locale_time.LC_date_time)) - base.__setitem__('x', self.pattern(self.locale_time.LC_date)) + base.__setitem__( + 'c', self.__pattern_with_lax_year(self.locale_time.LC_date_time)) + base.__setitem__( + 'x', self.__pattern_with_lax_year(self.locale_time.LC_date)) base.__setitem__('X', self.pattern(self.locale_time.LC_time)) def __seqToRE(self, to_convert, directive): @@ -236,6 +236,26 @@ def __seqToRE(self, to_convert, directive): regex = '(?P<%s>%s' % (directive, regex) return '%s)' % regex + def __pattern_with_lax_year(self, format): + """Like pattern(), but making %Y and %y accept also fewer digits. + + Necessary to ensure that strptime() is able to parse strftime()'s + output when %c or %x is used -- considering that for some locales + and platforms (e.g., 'C.UTF-8' on Linux), formatting with either + %c or %x may produce a year number representation that is shorter + than the usual four or two digits, if the number is small enough + (e.g., '999' instead of `0999', or '9' instead of '09'). + + Note that this helper is not used to generate the regex patterns + for %Y and %y (these two still match, respectively, only four or + two digits, exactly). + + """ + pattern = self.pattern(format) + pattern = pattern.replace(self['Y'], r"(?P\d{1,4})") + pattern = pattern.replace(self['y'], r"(?P\d{1,2})") + return pattern + def pattern(self, format): """Return regex pattern for the format string. @@ -374,6 +394,7 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): # U, W # worthless without day of the week if group_key == 'y': + # 1 or 2 digits (1 only for directive c or x; see TimeRE.__init__) year = int(found_dict['y']) # Open Group specification for strptime() states that a %y #value in the range of [00, 68] is in the century 2000, while @@ -383,6 +404,7 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): else: year += 1900 elif group_key == 'Y': + # 1-4 digits (1-3 only for directive c or x; see TimeRE.__init__) year = int(found_dict['Y']) elif group_key == 'G': iso_year = int(found_dict['G']) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index c81408b344968d..4aedf739f82179 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1185,6 +1185,40 @@ def test_strptime_leap_year(self): date.strptime('20-03-14', '%y-%m-%d') date.strptime('02-29,2024', '%m-%d,%Y') + def test_strftime_strptime_roundtrip(self): + for fmt in [ + '%c', + '%x', + '%Y%m%d', + 'm:%m d:%d y:%y H:%H M:%M S:%S f:%f and some text', + ]: + with self.subTest(fmt=fmt): + sample = date(1999, 3, 17).strftime(fmt) + if '1999' in sample: + year_seq = [ + 1, 9, 10, 99, 100, 999, # <- gh-124529 (ad %c/%x) + 1000, 1410, 1989, 2024, 2095, 9999] + elif '99' in sample: + year_seq = [ + 1969, 1999, + 2000, 2001, 2009, # <- gh-124529 (ad %c/%x) + 2068] + else: + self.skipTest(f"these subtests need locale for which " + f"{fmt!r} includes year in some variant") + for year in year_seq: + for instance in [ + date(year, 1, 1), + date(year, 6, 4), + date(year, 12, 31), + ]: + reason = (f'strftime/strptime roundtrip ' + f'for {fmt=} and {year=}') + with self.subTest(reason=reason, instance=instance): + formatted = instance.strftime(fmt) + parsed = date.strptime(formatted, fmt) + self.assertEqual(parsed, instance, msg=reason) + class SubclassDate(date): sub_var = 1 @@ -2124,6 +2158,35 @@ def test_fromisocalendar_type_errors(self): with self.assertRaises(TypeError): self.theclass.fromisocalendar(*isocal) + def test_strptime_accepting_year_with_fewer_digits(self): # gh-124529 + concerned_formats = '%c', '%x' + + def run_subtest(): + reason = (f'strptime accepting year with fewer ' + f'digits for {fmt=} and {input_string=}') + with self.subTest(reason=reason): + expected = prototype_inst.replace(year=year) + parsed = self.theclass.strptime(input_string, fmt) + self.assertEqual(parsed, expected, msg=reason) + + prototype_inst = self.theclass.strptime('1999', '%Y') + for fmt in concerned_formats: + with self.subTest(fmt=fmt): + sample = prototype_inst.strftime(fmt) + if (sample_4digits := '1999') in sample: + for year in [1, 9, 10, 99, 100, 999]: + y_digits = str(year) + input_string = sample.replace(sample_4digits, y_digits) + run_subtest() + elif (sample_2digits := '99') in sample: + for year in [2000, 2001, 2009]: + y_digits = str(year - 2000) + input_string = sample.replace(sample_2digits, y_digits) + run_subtest() + else: + self.skipTest(f"these subtests need locale for which " + f"{fmt!r} includes year in some variant") + ############################################################################# # datetime tests @@ -2955,6 +3018,48 @@ def test_more_strftime(self): except UnicodeEncodeError: pass + def test_strftime_strptime_roundtrip(self): + for tz in [ + None, + UTC, + timezone(timedelta(hours=2)), + timezone(timedelta(hours=-7)), + ]: + fmt_suffix = '' if tz is None else ' %z' + for fmt in [ + '%c %f', + '%x %X %f', + '%Y%m%d%H%M%S%f', + 'm:%m d:%d y:%y H:%H M:%M S:%S f:%f and some text', + ]: + fmt += fmt_suffix + with self.subTest(fmt=fmt): + sample = self.theclass(1999, 3, 17, 0, 0).strftime(fmt) + if '1999' in sample: + year_seq = [ + 1, 9, 10, 99, 100, 999, # <- gh-124529 (ad %c/%x) + 1000, 1410, 1989, 2024, 2095, 9999] + elif '99' in sample: + year_seq = [ + 1969, 1999, + 2000, 2001, 2009, # <- gh-124529 (ad %c/%x) + 2068] + else: + self.skipTest(f"these subtests need locale for which " + f"{fmt!r} includes year in some variant") + for year in year_seq: + for instance in [ + self.theclass(year, 1, 1, 0, 0, 0, tzinfo=tz), + self.theclass(year, 6, 4, 1, 42, 7, 99, tzinfo=tz), + self.theclass(year, 12, 31, 23, 59, 59, tzinfo=tz), + ]: + reason = (f'strftime/strptime roundtrip ' + f'for {fmt=} and {year=}') + with self.subTest(reason=reason, instance=instance): + formatted = instance.strftime(fmt) + parsed = self.theclass.strptime(formatted, fmt) + self.assertEqual(parsed, instance, msg=reason) + def test_extract(self): dt = self.theclass(2002, 3, 4, 18, 45, 3, 1234) self.assertEqual(dt.date(), date(2002, 3, 4)) @@ -3901,6 +4006,32 @@ def test_strptime_single_digit(self): newdate = self.theclass.strptime(string, format) self.assertEqual(newdate, target, msg=reason) + def test_strftime_strptime_roundtrip(self): + for tz in [ + None, + UTC, + timezone(timedelta(hours=2)), + timezone(timedelta(hours=-7)), + ]: + fmt_suffix = '' if tz is None else ' %z' + for fmt in [ + '%c %f', + '%X %f', + '%H%M%S%f', + 'm:%m d:%d y:%y H:%H M:%M S:%S f:%f and some text', + ]: + fmt += fmt_suffix + for instance in [ + self.theclass(0, 0, 0, tzinfo=tz), + self.theclass(1, 42, 7, tzinfo=tz), + self.theclass(23, 59, 59, 654321, tzinfo=tz), + ]: + reason = f'strftime/strptime round trip for {fmt=}' + with self.subTest(reason=reason, instance=instance): + formatted = instance.strftime(fmt) + parsed = self.theclass.strptime(formatted, fmt) + self.assertEqual(parsed, instance, msg=reason) + def test_bool(self): # time is always True. cls = self.theclass diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py index 038746e26c24ad..ab7373f4638e58 100644 --- a/Lib/test/test_strptime.py +++ b/Lib/test/test_strptime.py @@ -161,11 +161,42 @@ def test_compile(self): for directive in ('a','A','b','B','c','d','G','H','I','j','m','M','p', 'S','u','U','V','w','W','x','X','y','Y','Z','%'): fmt = "%d %Y" if directive == 'd' else "%" + directive + input_string = time.strftime(fmt) compiled = self.time_re.compile(fmt) - found = compiled.match(time.strftime(fmt)) - self.assertTrue(found, "Matching failed on '%s' using '%s' regex" % - (time.strftime(fmt), - compiled.pattern)) + found = compiled.match(input_string) + self.assertTrue(found, + (f"Matching failed on '{input_string}' " + f"using '{compiled.pattern}' regex")) + for directive in ('c', 'x'): + fmt = "%" + directive + with self.subTest(f"{fmt!r} should match input containing " + f"year with fewer digits than usual"): + # gh-124529 + params = _input_str_and_expected_year_for_few_digits_year(fmt) + if params is None: + self.skipTest(f"this subtest needs locale for which " + f"{fmt!r} includes year in some variant") + input_string, _ = params + compiled = self.time_re.compile(fmt) + found = compiled.match(input_string) + self.assertTrue(found, + (f"Matching failed on '{input_string}' " + f"using '{compiled.pattern}' regex")) + for directive in ('y', 'Y'): + fmt = "%" + directive + with self.subTest(f"{fmt!r} should not match input containing " + f"year with fewer digits than usual"): + params = _input_str_and_expected_year_for_few_digits_year(fmt) + if params is None: + self.skipTest(f"this subtest needs locale for which " + f"{fmt!r} includes year in some variant") + input_string, _ = params + compiled = self.time_re.compile(fmt) + found = compiled.match(input_string) + self.assertFalse(found, + (f"Matching unexpectedly succeeded " + f"on '{input_string}' using " + f"'{compiled.pattern}' regex")) def test_blankpattern(self): # Make sure when tuple or something has no values no regex is generated. @@ -299,6 +330,25 @@ def helper(self, directive, position): (directive, strf_output, strp_output[position], self.time_tuple[position])) + def helper_for_directives_accepting_few_digits_year(self, directive): + fmt = "%" + directive + params = _input_str_and_expected_year_for_few_digits_year(fmt) + if params is None: + self.skipTest(f"test needs locale for which {fmt!r} " + f"includes year in some variant") + input_string, expected_year = params + try: + output_year = _strptime._strptime(input_string, fmt)[0][0] + except ValueError as exc: + # See: gh-124529 + self.fail(f"testing of {directive!r} directive failed; " + f"{input_string!r} -> exception: {exc!r}") + else: + self.assertEqual(output_year, expected_year, + (f"testing of {directive!r} directive failed; " + f"{input_string!r} -> output including year " + f"{output_year!r} != {expected_year!r}")) + def test_year(self): # Test that the year is handled properly for directive in ('y', 'Y'): @@ -312,6 +362,17 @@ def test_year(self): "'y' test failed; passed in '%s' " "and returned '%s'" % (bound, strp_output[0])) + def test_bad_year(self): + for directive, bad_inputs in ( + ('y', ('9', '100', 'ni')), + ('Y', ('7', '42', '999', '10000', 'SPAM')), + ): + fmt = "%" + directive + for input_val in bad_inputs: + with self.subTest(directive=directive, input_val=input_val): + with self.assertRaises(ValueError): + _strptime._strptime_time(input_val, fmt) + def test_month(self): # Test for month directives for directive in ('B', 'b', 'm'): @@ -454,11 +515,21 @@ def test_date_time(self): for position in range(6): self.helper('c', position) + def test_date_time_accepting_few_digits_year(self): # gh-124529 + # Test %c directive with input containing year + # number consisting of fewer digits than usual + self.helper_for_directives_accepting_few_digits_year('c') + def test_date(self): # Test %x directive for position in range(0,3): self.helper('x', position) + def test_date_accepting_few_digits_year(self): # gh-124529 + # Test %x directive with input containing year + # number consisting of fewer digits than usual + self.helper_for_directives_accepting_few_digits_year('x') + def test_time(self): # Test %X directive for position in range(3,6): @@ -769,5 +840,55 @@ def test_TimeRE_recreation_timezone(self): _strptime._strptime_time(oldtzname[1], '%Z') +def _input_str_and_expected_year_for_few_digits_year(fmt): + # This helper, for the given format string (fmt), returns a 2-tuple: + # (, ) + # where: + # * -- is a `strftime(fmt)`-result-like str + # containing a year number which is *shorter* than the usual four + # or two digits (namely: the contained year number consist of just + # one digit: 7; the choice of this particular digit is arbitrary); + # * -- is an int representing the year number that + # is expected to be part of the result of a `strptime(, fmt)` call (namely: either 7 or 2007, depending + # on the given format string and current locale...); however, it + # is None if does *not* contain the year + # part (for the given format string and current locale). + + # 1. Prepare auxiliary *magic* time data (note that the magic values + # we use here are guaranteed to be compatible with `time.strftime()` + # and also well distinguishable within a formatted string, thanks to + # the fact that the amount of overloaded numbers is minimized, as in + # `_strptime.LocaleTime.__calc_date_time()`...): + magic_year = 1999 + magic_tt = (magic_year, 3, 17, 22, 44, 55, 2, 76, 0) + magic_4digits = str(magic_year) + magic_2digits = magic_4digits[-2:] + + # 2. Pick our example year whose representation + # is shorter than the usual four or two digits: + input_year_str = '7' + + # 3. Determine the part of the return value: + input_string = time.strftime(fmt, magic_tt) + if (index_4digits := input_string.find(magic_4digits)) != -1: + # `input_string` contains up-to-4-digit year representation + input_string = input_string.replace(magic_4digits, input_year_str) + if (index_2digits := input_string.find(magic_2digits)) != -1: + # `input_string` contains up-to-2-digit year representation + input_string = input_string.replace(magic_2digits, input_year_str) + + # 4. Determine the part of the return value: + if index_4digits > index_2digits: + expected_year = int(input_year_str) + elif index_4digits < index_2digits: + expected_year = 2000 + int(input_year_str) + else: + assert index_4digits == index_2digits == -1 + expected_year = None + + return input_string, expected_year + + if __name__ == '__main__': unittest.main() From 1d916a0392cc7957c74bcb851bec27a771383dd0 Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Mon, 30 Sep 2024 04:50:07 +0200 Subject: [PATCH 02/23] Add the blurb --- .../2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst diff --git a/Misc/NEWS.d/next/Library/2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst b/Misc/NEWS.d/next/Library/2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst new file mode 100644 index 00000000000000..d3916af63e516f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst @@ -0,0 +1,9 @@ +Fix :meth:`datetime.datetime.strptime`, :meth:`datetime.date.strptime` as +well as :func:`time.strptime` (by modifying :class:`_strptime.TimeRE`) to +make ``%c`` and ``%x`` accept year number representations consisting of +fewer digits than the usual four or two (note that nothing changes for +``%Y`` and ``%y``). Thanks to that, certain ``strftime/strptime`` round +trips (such as ``datetime.strptime(dt.strftime("%c"), "%c"))`` for +``dt.year`` less than 1000) no longer raise ``ValueError`` for some +locales/platforms (this was the case, e.g., on Linux -- for various +locales, including ``C``/``C.UTF-8``). From a4ba3c9b866ea5b1f29aac08f5bb692a0cf51292 Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Mon, 30 Sep 2024 16:27:16 +0200 Subject: [PATCH 03/23] Fix/improve (and shorten) the blurb --- ...024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst b/Misc/NEWS.d/next/Library/2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst index d3916af63e516f..8d3e3651c689dc 100644 --- a/Misc/NEWS.d/next/Library/2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst +++ b/Misc/NEWS.d/next/Library/2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst @@ -1,9 +1,7 @@ -Fix :meth:`datetime.datetime.strptime`, :meth:`datetime.date.strptime` as -well as :func:`time.strptime` (by modifying :class:`_strptime.TimeRE`) to -make ``%c`` and ``%x`` accept year number representations consisting of -fewer digits than the usual four or two (note that nothing changes for -``%Y`` and ``%y``). Thanks to that, certain ``strftime/strptime`` round -trips (such as ``datetime.strptime(dt.strftime("%c"), "%c"))`` for -``dt.year`` less than 1000) no longer raise ``ValueError`` for some -locales/platforms (this was the case, e.g., on Linux -- for various -locales, including ``C``/``C.UTF-8``). +Fix :meth:`datetime.datetime.strptime`, :meth:`datetime.date.strptime` +and :func:`time.strptime` (by altering the underlying mechanism) to make +``%c``/``%x`` accept year numbers with fewer digits than the usual 4 or +2 (not zero-padded), so that some ``strftime/strptime`` round trip cases +(e.g., ``date.strptime(d.strftime('%c'),'%c'))`` for ``d.year < 1000``, +with C-like locales on Linux) no longer raise :exc:`!ValueError`. Patch +by Jan Kaliszewski. From cef07220d1118268d0aabad171e83d50d32f96f8 Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Mon, 30 Sep 2024 20:56:57 +0200 Subject: [PATCH 04/23] Fix/improve/refactor tests and restrict their scope --- Lib/_strptime.py | 2 +- Lib/test/datetimetester.py | 172 ++++++++++++------------------------- Lib/test/test_strptime.py | 42 ++++----- 3 files changed, 76 insertions(+), 140 deletions(-) diff --git a/Lib/_strptime.py b/Lib/_strptime.py index c3dda4aed0acab..52aea231c953c2 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -246,7 +246,7 @@ def __pattern_with_lax_year(self, format): than the usual four or two digits, if the number is small enough (e.g., '999' instead of `0999', or '9' instead of '09'). - Note that this helper is not used to generate the regex patterns + Note that this helper is *not* used to prepare the regex patterns for %Y and %y (these two still match, respectively, only four or two digits, exactly). diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 4aedf739f82179..c9011413765d30 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1185,40 +1185,6 @@ def test_strptime_leap_year(self): date.strptime('20-03-14', '%y-%m-%d') date.strptime('02-29,2024', '%m-%d,%Y') - def test_strftime_strptime_roundtrip(self): - for fmt in [ - '%c', - '%x', - '%Y%m%d', - 'm:%m d:%d y:%y H:%H M:%M S:%S f:%f and some text', - ]: - with self.subTest(fmt=fmt): - sample = date(1999, 3, 17).strftime(fmt) - if '1999' in sample: - year_seq = [ - 1, 9, 10, 99, 100, 999, # <- gh-124529 (ad %c/%x) - 1000, 1410, 1989, 2024, 2095, 9999] - elif '99' in sample: - year_seq = [ - 1969, 1999, - 2000, 2001, 2009, # <- gh-124529 (ad %c/%x) - 2068] - else: - self.skipTest(f"these subtests need locale for which " - f"{fmt!r} includes year in some variant") - for year in year_seq: - for instance in [ - date(year, 1, 1), - date(year, 6, 4), - date(year, 12, 31), - ]: - reason = (f'strftime/strptime roundtrip ' - f'for {fmt=} and {year=}') - with self.subTest(reason=reason, instance=instance): - formatted = instance.strftime(fmt) - parsed = date.strptime(formatted, fmt) - self.assertEqual(parsed, instance, msg=reason) - class SubclassDate(date): sub_var = 1 @@ -2158,34 +2124,72 @@ def test_fromisocalendar_type_errors(self): with self.assertRaises(TypeError): self.theclass.fromisocalendar(*isocal) - def test_strptime_accepting_year_with_fewer_digits(self): # gh-124529 - concerned_formats = '%c', '%x' + def test_strftime_strptime_roundtrip_concerning_locale_specific_year(self): + concerned_formats = '%c', '%x' # gh-124529 def run_subtest(): - reason = (f'strptime accepting year with fewer ' - f'digits for {fmt=} and {input_string=}') + reason = (f'test strftime/strptime roundtrip concerning ' + f'locale-specific year representation ' + f'- for {fmt=} and {year=}') + initial = expected = self.theclass.strptime(f'{year:04}', '%Y') with self.subTest(reason=reason): - expected = prototype_inst.replace(year=year) - parsed = self.theclass.strptime(input_string, fmt) + formatted = initial.strftime(fmt) + parsed = self.theclass.strptime(formatted, fmt) self.assertEqual(parsed, expected, msg=reason) - prototype_inst = self.theclass.strptime('1999', '%Y') + sample = self.theclass.strptime(f'1999-03-17', '%Y-%m-%d') for fmt in concerned_formats: with self.subTest(fmt=fmt): - sample = prototype_inst.strftime(fmt) - if (sample_4digits := '1999') in sample: + sample_str = sample.strftime(fmt) + if '1999' in sample_str: + for year in [ + 1, 9, 10, 99, 100, 999, # <- gh-124529 + 1000, 1410, 1989, 2024, 2095, 9999, + ]: + run_subtest() + elif '99' in sample_str: + for year in [ + 1969, 1999, + 2000, 2001, 2009, # <- gh-124529 + 2068, + ]: + run_subtest() + else: + self.fail(f"it seems that {sample.strftime(fmt)=} " + f"does not include year={sample.year!r} " + f"in any variant (is there something " + f"severely wrong with current locale?)") + + def test_strptime_accepting_locale_specific_year_with_fewer_digits(self): + concerned_formats = '%c', '%x' # gh-124529 + + def run_subtest(): + input_str = sample_str.replace(sample_digits, year_digits) + reason = (f'test strptime accepting locale-specific ' + f'year representation with fewer digits ' + f'- for {fmt=} and {input_str=} ({year=})') + expected = sample.replace(year=year) + with self.subTest(reason=reason): + parsed = self.theclass.strptime(input_str, fmt) + self.assertEqual(parsed, expected, msg=reason) + + sample = self.theclass.strptime(f'1999-03-17', '%Y-%m-%d') + for fmt in concerned_formats: + with self.subTest(fmt=fmt): + sample_str = sample.strftime(fmt) + if (sample_digits := '1999') in sample_str: for year in [1, 9, 10, 99, 100, 999]: - y_digits = str(year) - input_string = sample.replace(sample_4digits, y_digits) + year_digits = str(year) run_subtest() - elif (sample_2digits := '99') in sample: + elif (sample_digits := '99') in sample_str: for year in [2000, 2001, 2009]: - y_digits = str(year - 2000) - input_string = sample.replace(sample_2digits, y_digits) + year_digits = str(year - 2000) run_subtest() else: - self.skipTest(f"these subtests need locale for which " - f"{fmt!r} includes year in some variant") + self.fail(f"it seems that {sample.strftime(fmt)=} " + f"does not include year={sample.year!r} " + f"in any variant (is there something " + f"severely wrong with current locale?)") ############################################################################# @@ -3018,48 +3022,6 @@ def test_more_strftime(self): except UnicodeEncodeError: pass - def test_strftime_strptime_roundtrip(self): - for tz in [ - None, - UTC, - timezone(timedelta(hours=2)), - timezone(timedelta(hours=-7)), - ]: - fmt_suffix = '' if tz is None else ' %z' - for fmt in [ - '%c %f', - '%x %X %f', - '%Y%m%d%H%M%S%f', - 'm:%m d:%d y:%y H:%H M:%M S:%S f:%f and some text', - ]: - fmt += fmt_suffix - with self.subTest(fmt=fmt): - sample = self.theclass(1999, 3, 17, 0, 0).strftime(fmt) - if '1999' in sample: - year_seq = [ - 1, 9, 10, 99, 100, 999, # <- gh-124529 (ad %c/%x) - 1000, 1410, 1989, 2024, 2095, 9999] - elif '99' in sample: - year_seq = [ - 1969, 1999, - 2000, 2001, 2009, # <- gh-124529 (ad %c/%x) - 2068] - else: - self.skipTest(f"these subtests need locale for which " - f"{fmt!r} includes year in some variant") - for year in year_seq: - for instance in [ - self.theclass(year, 1, 1, 0, 0, 0, tzinfo=tz), - self.theclass(year, 6, 4, 1, 42, 7, 99, tzinfo=tz), - self.theclass(year, 12, 31, 23, 59, 59, tzinfo=tz), - ]: - reason = (f'strftime/strptime roundtrip ' - f'for {fmt=} and {year=}') - with self.subTest(reason=reason, instance=instance): - formatted = instance.strftime(fmt) - parsed = self.theclass.strptime(formatted, fmt) - self.assertEqual(parsed, instance, msg=reason) - def test_extract(self): dt = self.theclass(2002, 3, 4, 18, 45, 3, 1234) self.assertEqual(dt.date(), date(2002, 3, 4)) @@ -4006,32 +3968,6 @@ def test_strptime_single_digit(self): newdate = self.theclass.strptime(string, format) self.assertEqual(newdate, target, msg=reason) - def test_strftime_strptime_roundtrip(self): - for tz in [ - None, - UTC, - timezone(timedelta(hours=2)), - timezone(timedelta(hours=-7)), - ]: - fmt_suffix = '' if tz is None else ' %z' - for fmt in [ - '%c %f', - '%X %f', - '%H%M%S%f', - 'm:%m d:%d y:%y H:%H M:%M S:%S f:%f and some text', - ]: - fmt += fmt_suffix - for instance in [ - self.theclass(0, 0, 0, tzinfo=tz), - self.theclass(1, 42, 7, tzinfo=tz), - self.theclass(23, 59, 59, 654321, tzinfo=tz), - ]: - reason = f'strftime/strptime round trip for {fmt=}' - with self.subTest(reason=reason, instance=instance): - formatted = instance.strftime(fmt) - parsed = self.theclass.strptime(formatted, fmt) - self.assertEqual(parsed, instance, msg=reason) - def test_bool(self): # time is always True. cls = self.theclass diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py index ab7373f4638e58..62cde7c17ac52d 100644 --- a/Lib/test/test_strptime.py +++ b/Lib/test/test_strptime.py @@ -846,8 +846,8 @@ def _input_str_and_expected_year_for_few_digits_year(fmt): # where: # * -- is a `strftime(fmt)`-result-like str # containing a year number which is *shorter* than the usual four - # or two digits (namely: the contained year number consist of just - # one digit: 7; the choice of this particular digit is arbitrary); + # or two digits (namely: here the contained year number consist of + # one digit: 7; that's an arbitrary choice); # * -- is an int representing the year number that # is expected to be part of the result of a `strptime(, fmt)` call (namely: either 7 or 2007, depending @@ -856,36 +856,36 @@ def _input_str_and_expected_year_for_few_digits_year(fmt): # part (for the given format string and current locale). # 1. Prepare auxiliary *magic* time data (note that the magic values - # we use here are guaranteed to be compatible with `time.strftime()` - # and also well distinguishable within a formatted string, thanks to - # the fact that the amount of overloaded numbers is minimized, as in - # `_strptime.LocaleTime.__calc_date_time()`...): + # we use here are guaranteed to be compatible with `time.strftime()`, + # and are also intended to be well distinguishable within a formatted + # string, thanks to the fact that the amount of overloaded numbers is + # minimized, as in `_strptime.LocaleTime.__calc_date_time()`): magic_year = 1999 magic_tt = (magic_year, 3, 17, 22, 44, 55, 2, 76, 0) - magic_4digits = str(magic_year) - magic_2digits = magic_4digits[-2:] - # 2. Pick our example year whose representation - # is shorter than the usual four or two digits: + # 2. Pick an arbitrary year number representation that + # is always *shorter* than the usual four or two digits: input_year_str = '7' - # 3. Determine the part of the return value: + # 3. Obtain the resultant 2-tuple: + input_string = time.strftime(fmt, magic_tt) - if (index_4digits := input_string.find(magic_4digits)) != -1: + expected_year = None + + magic_4digits = str(magic_year) + if found_4digits := (magic_4digits in input_string): # `input_string` contains up-to-4-digit year representation input_string = input_string.replace(magic_4digits, input_year_str) - if (index_2digits := input_string.find(magic_2digits)) != -1: + expected_year = int(input_year_str) + + magic_2digits = str(magic_year)[-2:] + if magic_2digits in input_string: # `input_string` contains up-to-2-digit year representation + if found_4digits: + raise RuntimeError(f'case not supported by this helper: {fmt=} ' + f'(includes both 2-digit and 4-digit year)') input_string = input_string.replace(magic_2digits, input_year_str) - - # 4. Determine the part of the return value: - if index_4digits > index_2digits: - expected_year = int(input_year_str) - elif index_4digits < index_2digits: expected_year = 2000 + int(input_year_str) - else: - assert index_4digits == index_2digits == -1 - expected_year = None return input_string, expected_year From 6556bdff6f0408d5f407d3566641f890c4c86802 Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Mon, 30 Sep 2024 21:06:39 +0200 Subject: [PATCH 05/23] Test fail message fixes --- Lib/test/datetimetester.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index c9011413765d30..140b8425799a24 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2156,8 +2156,8 @@ def run_subtest(): run_subtest() else: self.fail(f"it seems that {sample.strftime(fmt)=} " - f"does not include year={sample.year!r} " - f"in any variant (is there something " + f"does not include year={sample.year!r} in " + f"any expected format (is there something " f"severely wrong with current locale?)") def test_strptime_accepting_locale_specific_year_with_fewer_digits(self): @@ -2187,8 +2187,8 @@ def run_subtest(): run_subtest() else: self.fail(f"it seems that {sample.strftime(fmt)=} " - f"does not include year={sample.year!r} " - f"in any variant (is there something " + f"does not include year={sample.year!r} in " + f"any expected format (is there something " f"severely wrong with current locale?)") From 56c84dc69f7886b8926faf57aea59c9fbab56216 Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Mon, 30 Sep 2024 22:39:57 +0200 Subject: [PATCH 06/23] Continue enhancing/improving tests --- Lib/test/datetimetester.py | 34 ++++++++++++++++++++++------------ Lib/test/test_strptime.py | 20 +++++++++++++------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 140b8425799a24..8a84c408050a29 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2125,17 +2125,22 @@ def test_fromisocalendar_type_errors(self): self.theclass.fromisocalendar(*isocal) def test_strftime_strptime_roundtrip_concerning_locale_specific_year(self): - concerned_formats = '%c', '%x' # gh-124529 + concerned_formats = '%c', '%x' def run_subtest(): - reason = (f'test strftime/strptime roundtrip concerning ' - f'locale-specific year representation ' - f'- for {fmt=} and {year=}') + reason = (f"test strftime/strptime roundtrip concerning " + f"locale-specific year representation " + f"- for {fmt=} and {year=}") + fail_msg = f"{reason} - failed" initial = expected = self.theclass.strptime(f'{year:04}', '%Y') with self.subTest(reason=reason): formatted = initial.strftime(fmt) - parsed = self.theclass.strptime(formatted, fmt) - self.assertEqual(parsed, expected, msg=reason) + try: + parsed = self.theclass.strptime(formatted, fmt) + except ValueError as exc: + # gh-124529 + self.fail(f"{fail_msg}; parsing error: {exc!r}") + self.assertEqual(parsed, expected, fail_msg) sample = self.theclass.strptime(f'1999-03-17', '%Y-%m-%d') for fmt in concerned_formats: @@ -2161,17 +2166,22 @@ def run_subtest(): f"severely wrong with current locale?)") def test_strptime_accepting_locale_specific_year_with_fewer_digits(self): - concerned_formats = '%c', '%x' # gh-124529 + concerned_formats = '%c', '%x' def run_subtest(): input_str = sample_str.replace(sample_digits, year_digits) - reason = (f'test strptime accepting locale-specific ' - f'year representation with fewer digits ' - f'- for {fmt=} and {input_str=} ({year=})') + reason = (f"test strptime accepting locale-specific " + f"year representation with fewer digits " + f"- for {fmt=} and {input_str=} ({year=})") + fail_msg = f"{reason} - failed" expected = sample.replace(year=year) with self.subTest(reason=reason): - parsed = self.theclass.strptime(input_str, fmt) - self.assertEqual(parsed, expected, msg=reason) + try: + parsed = self.theclass.strptime(input_str, fmt) + except ValueError as exc: + # gh-124529 + self.fail(f"{fail_msg}; parsing error: {exc!r}") + self.assertEqual(parsed, expected, fail_msg) sample = self.theclass.strptime(f'1999-03-17', '%Y-%m-%d') for fmt in concerned_formats: diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py index 62cde7c17ac52d..70629d52340267 100644 --- a/Lib/test/test_strptime.py +++ b/Lib/test/test_strptime.py @@ -174,22 +174,26 @@ def test_compile(self): # gh-124529 params = _input_str_and_expected_year_for_few_digits_year(fmt) if params is None: - self.skipTest(f"this subtest needs locale for which " - f"{fmt!r} includes year in some variant") + self.fail(f"it seems that using {fmt=} results in value " + f"which does not include year representation " + f"in any expected format (is there something " + f"severely wrong with current locale?)") input_string, _ = params compiled = self.time_re.compile(fmt) found = compiled.match(input_string) self.assertTrue(found, (f"Matching failed on '{input_string}' " f"using '{compiled.pattern}' regex")) - for directive in ('y', 'Y'): + for directive in ('y', 'Y', 'G'): fmt = "%" + directive with self.subTest(f"{fmt!r} should not match input containing " f"year with fewer digits than usual"): params = _input_str_and_expected_year_for_few_digits_year(fmt) if params is None: - self.skipTest(f"this subtest needs locale for which " - f"{fmt!r} includes year in some variant") + self.fail(f"it seems that using {fmt=} results in value " + f"which does not include year representation " + f"in any expected format (is there something " + f"severely wrong with current locale?)") input_string, _ = params compiled = self.time_re.compile(fmt) found = compiled.match(input_string) @@ -334,8 +338,10 @@ def helper_for_directives_accepting_few_digits_year(self, directive): fmt = "%" + directive params = _input_str_and_expected_year_for_few_digits_year(fmt) if params is None: - self.skipTest(f"test needs locale for which {fmt!r} " - f"includes year in some variant") + self.fail(f"it seems that using {fmt=} results in value " + f"which does not include year representation " + f"in any expected format (is there something " + f"severely wrong with current locale?)") input_string, expected_year = params try: output_year = _strptime._strptime(input_string, fmt)[0][0] From b42783cb541de081740a8f8d1fc47a98c107f2a6 Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Mon, 30 Sep 2024 22:58:53 +0200 Subject: [PATCH 07/23] Improve docstring --- Lib/_strptime.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/_strptime.py b/Lib/_strptime.py index 52aea231c953c2..bd46b346267ed0 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -244,11 +244,11 @@ def __pattern_with_lax_year(self, format): and platforms (e.g., 'C.UTF-8' on Linux), formatting with either %c or %x may produce a year number representation that is shorter than the usual four or two digits, if the number is small enough - (e.g., '999' instead of `0999', or '9' instead of '09'). + (e.g., '999' rather than `0999', or '9' rather than of '09'). - Note that this helper is *not* used to prepare the regex patterns - for %Y and %y (these two still match, respectively, only four or - two digits, exactly). + Note that this helper is intended to be used to prepare only the + regex patterns for the `%c` and `%x` format codes (and *not* for + `%y`, `%Y` or `%G`). """ pattern = self.pattern(format) From 97b523e560749cb0d9a787488f2cf89a37baec9c Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Tue, 1 Oct 2024 00:39:57 +0200 Subject: [PATCH 08/23] Add a suitable mention in the docs + improve blurb --- Doc/library/datetime.rst | 10 ++++++++++ .../2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 59e2dbd6847538..b6933d0012f85b 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2634,6 +2634,16 @@ Notes: example, "month/day/year" versus "day/month/year"), and the output may contain non-ASCII characters. + .. versionchanged:: 3.14 + The :meth:`~.datetime.strptime` method, if used with the ``%c`` + or ``%x`` format code, no longer requires the *year* part of the + input to be zero-padded to the usual width (which is either 4 or + 2 digits, depending on the format code and current locale). In + previous versions, a :exc:`ValueError` was raised if a shorter + *year* was part of the input (and it is worth noting that, + depending on the platform/locale, such inputs may be produced + by :meth:`~.datetime.strftime` invoked with ``%c`` or ``%x``). + (2) The :meth:`~.datetime.strptime` method can parse years in the full [1, 9999] range, but years < 1000 must be zero-filled to 4-digit width. diff --git a/Misc/NEWS.d/next/Library/2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst b/Misc/NEWS.d/next/Library/2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst index 8d3e3651c689dc..d6a3e974e6d26b 100644 --- a/Misc/NEWS.d/next/Library/2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst +++ b/Misc/NEWS.d/next/Library/2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst @@ -1,7 +1,7 @@ Fix :meth:`datetime.datetime.strptime`, :meth:`datetime.date.strptime` and :func:`time.strptime` (by altering the underlying mechanism) to make ``%c``/``%x`` accept year numbers with fewer digits than the usual 4 or -2 (not zero-padded), so that some ``strftime/strptime`` round trip cases -(e.g., ``date.strptime(d.strftime('%c'),'%c'))`` for ``d.year < 1000``, +2 (not zero-padded), so that certain ``strftime/strptime`` round trips +(e.g., ``date.strptime(d.strftime('%c'), '%c'))`` for ``d.year < 1000``, with C-like locales on Linux) no longer raise :exc:`!ValueError`. Patch by Jan Kaliszewski. From 6c2addf193279709dc81c209a68e9864bd15eaad Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Tue, 1 Oct 2024 00:45:51 +0200 Subject: [PATCH 09/23] Docstring edit --- Lib/_strptime.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/_strptime.py b/Lib/_strptime.py index bd46b346267ed0..4e95780619cbe9 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -246,8 +246,8 @@ def __pattern_with_lax_year(self, format): than the usual four or two digits, if the number is small enough (e.g., '999' rather than `0999', or '9' rather than of '09'). - Note that this helper is intended to be used to prepare only the - regex patterns for the `%c` and `%x` format codes (and *not* for + Note that this helper is intended to be used to prepare regex + patterns only for the `%c` and `%x` format codes (and *not* for `%y`, `%Y` or `%G`). """ From fb32c030f05c82cb535e8166f2bb007da0ea3537 Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Tue, 1 Oct 2024 00:58:58 +0200 Subject: [PATCH 10/23] Edit the mention in the docs --- Doc/library/datetime.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index b6933d0012f85b..2648e94ab82884 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2640,9 +2640,10 @@ Notes: input to be zero-padded to the usual width (which is either 4 or 2 digits, depending on the format code and current locale). In previous versions, a :exc:`ValueError` was raised if a shorter - *year* was part of the input (and it is worth noting that, - depending on the platform/locale, such inputs may be produced - by :meth:`~.datetime.strftime` invoked with ``%c`` or ``%x``). + *year*, not zero-padded to the 2- or 4-digit width as appropriate, + was part of the input (and it is worth noting that, depending + on the platform/locale, such inputs may be produced by + :meth:`~.datetime.strftime` invoked with ``%c`` or ``%x``). (2) The :meth:`~.datetime.strptime` method can parse years in the full [1, 9999] range, but From 722efe488553aed74c639451c85d66792741cc30 Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Tue, 1 Oct 2024 01:08:38 +0200 Subject: [PATCH 11/23] Minor docs edit --- Doc/library/datetime.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 2648e94ab82884..21cff45ab3d39f 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2642,8 +2642,8 @@ Notes: previous versions, a :exc:`ValueError` was raised if a shorter *year*, not zero-padded to the 2- or 4-digit width as appropriate, was part of the input (and it is worth noting that, depending - on the platform/locale, such inputs may be produced by - :meth:`~.datetime.strftime` invoked with ``%c`` or ``%x``). + on the platform/locale, such inputs may be produced by using + :meth:`~.datetime.strftime` with ``%c`` or ``%x``). (2) The :meth:`~.datetime.strptime` method can parse years in the full [1, 9999] range, but From 7b46a48f5f7bd8e2dbec6dc8ec7b4442fd5426e6 Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Tue, 1 Oct 2024 04:29:43 +0200 Subject: [PATCH 12/23] Fix/improve tests --- Lib/test/test_strptime.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py index 70629d52340267..b2a7da1b62ea4e 100644 --- a/Lib/test/test_strptime.py +++ b/Lib/test/test_strptime.py @@ -168,17 +168,18 @@ def test_compile(self): (f"Matching failed on '{input_string}' " f"using '{compiled.pattern}' regex")) for directive in ('c', 'x'): + # gh-124529 fmt = "%" + directive with self.subTest(f"{fmt!r} should match input containing " f"year with fewer digits than usual"): - # gh-124529 - params = _input_str_and_expected_year_for_few_digits_year(fmt) - if params is None: + (input_string, + year) = _get_data_to_test_strptime_with_few_digits_year(fmt) + if year is None: self.fail(f"it seems that using {fmt=} results in value " f"which does not include year representation " f"in any expected format (is there something " f"severely wrong with current locale?)") - input_string, _ = params + compiled = self.time_re.compile(fmt) found = compiled.match(input_string) self.assertTrue(found, @@ -188,13 +189,13 @@ def test_compile(self): fmt = "%" + directive with self.subTest(f"{fmt!r} should not match input containing " f"year with fewer digits than usual"): - params = _input_str_and_expected_year_for_few_digits_year(fmt) - if params is None: + (input_string, + year) = _get_data_to_test_strptime_with_few_digits_year(fmt) + if year is None: self.fail(f"it seems that using {fmt=} results in value " f"which does not include year representation " f"in any expected format (is there something " f"severely wrong with current locale?)") - input_string, _ = params compiled = self.time_re.compile(fmt) found = compiled.match(input_string) self.assertFalse(found, @@ -336,13 +337,13 @@ def helper(self, directive, position): def helper_for_directives_accepting_few_digits_year(self, directive): fmt = "%" + directive - params = _input_str_and_expected_year_for_few_digits_year(fmt) - if params is None: + (input_string, + expected_year) = _get_data_to_test_strptime_with_few_digits_year(fmt) + if expected_year is None: self.fail(f"it seems that using {fmt=} results in value " f"which does not include year representation " f"in any expected format (is there something " f"severely wrong with current locale?)") - input_string, expected_year = params try: output_year = _strptime._strptime(input_string, fmt)[0][0] except ValueError as exc: @@ -846,7 +847,7 @@ def test_TimeRE_recreation_timezone(self): _strptime._strptime_time(oldtzname[1], '%Z') -def _input_str_and_expected_year_for_few_digits_year(fmt): +def _get_data_to_test_strptime_with_few_digits_year(fmt): # This helper, for the given format string (fmt), returns a 2-tuple: # (, ) # where: From 3d4116a44391cca33a8e322b69cf2ccba4a5a581 Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Tue, 1 Oct 2024 04:39:21 +0200 Subject: [PATCH 13/23] Shorten blurb again --- .../2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst b/Misc/NEWS.d/next/Library/2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst index d6a3e974e6d26b..cd6525ffdf2850 100644 --- a/Misc/NEWS.d/next/Library/2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst +++ b/Misc/NEWS.d/next/Library/2024-09-30-04-49-22.gh-issue-124529.hDDlMH.rst @@ -1,7 +1,5 @@ Fix :meth:`datetime.datetime.strptime`, :meth:`datetime.date.strptime` -and :func:`time.strptime` (by altering the underlying mechanism) to make -``%c``/``%x`` accept year numbers with fewer digits than the usual 4 or -2 (not zero-padded), so that certain ``strftime/strptime`` round trips -(e.g., ``date.strptime(d.strftime('%c'), '%c'))`` for ``d.year < 1000``, -with C-like locales on Linux) no longer raise :exc:`!ValueError`. Patch -by Jan Kaliszewski. +and :func:`time.strptime` to make ``%c`` and ``%x`` accept year numbers +with fewer digits than the usual 4 or 2 (not zero-padded). This prevents +:exc:`ValueError` in certain cases of ``strftime/strptime`` round trips. +Patch by Jan Kaliszewski. From 8eac1067da9826b8318a6ce6ff4e598f9d86c4d2 Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Tue, 1 Oct 2024 16:09:29 +0200 Subject: [PATCH 14/23] Minor docs edit --- Doc/library/datetime.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 21cff45ab3d39f..aa45ef1ff5ce25 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2640,9 +2640,9 @@ Notes: input to be zero-padded to the usual width (which is either 4 or 2 digits, depending on the format code and current locale). In previous versions, a :exc:`ValueError` was raised if a shorter - *year*, not zero-padded to the 2- or 4-digit width as appropriate, - was part of the input (and it is worth noting that, depending - on the platform/locale, such inputs may be produced by using + *year* (not zero-padded to the 2- or 4-digit width as appropriate) + was part of the input. It is worth noting that, depending on the + platform/locale, such inputs may be produced by using :meth:`~.datetime.strftime` with ``%c`` or ``%x``). (2) From 195666217a8172b7de7906ef8b9b9e88d2f30f9f Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Tue, 1 Oct 2024 16:27:02 +0200 Subject: [PATCH 15/23] Docstring edit --- Lib/_strptime.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Lib/_strptime.py b/Lib/_strptime.py index 4e95780619cbe9..eb48180bf5f062 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -241,14 +241,15 @@ def __pattern_with_lax_year(self, format): Necessary to ensure that strptime() is able to parse strftime()'s output when %c or %x is used -- considering that for some locales - and platforms (e.g., 'C.UTF-8' on Linux), formatting with either - %c or %x may produce a year number representation that is shorter - than the usual four or two digits, if the number is small enough - (e.g., '999' rather than `0999', or '9' rather than of '09'). + and platforms (e.g., 'C.UTF-8' on Linux) formatting with either + %c or %x may produce a year number representation which is *not* + zero-padded and, consequently (if the number is small enough), + shorter than the usual four or two digits (e.g., '999' rather + than `0999', or '9' rather than of '09'). Note that this helper is intended to be used to prepare regex - patterns only for the `%c` and `%x` format codes (and *not* for - `%y`, `%Y` or `%G`). + patterns *only* for the `%c` and `%x` format codes (and *not* + for `%y`, `%Y` or `%G`). """ pattern = self.pattern(format) From a77c3da0f74f4f9372d85b39b8c5f8bc2cf7502d Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Tue, 1 Oct 2024 16:35:35 +0200 Subject: [PATCH 16/23] Docs edit --- Doc/library/datetime.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index aa45ef1ff5ce25..e87f7cd0aa3136 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2639,11 +2639,11 @@ Notes: or ``%x`` format code, no longer requires the *year* part of the input to be zero-padded to the usual width (which is either 4 or 2 digits, depending on the format code and current locale). In - previous versions, a :exc:`ValueError` was raised if a shorter - *year* (not zero-padded to the 2- or 4-digit width as appropriate) - was part of the input. It is worth noting that, depending on the - platform/locale, such inputs may be produced by using - :meth:`~.datetime.strftime` with ``%c`` or ``%x``). + previous versions, a :exc:`ValueError` was raised if a narrower + (i.e., not zero-padded) year representation was part of the input + (and it is worth noting that, depending on the platform/locale, + such inputs may be produced by using :meth:`~.datetime.strftime` + with ``%c`` or ``%x``). (2) The :meth:`~.datetime.strptime` method can parse years in the full [1, 9999] range, but From b3b161f16157f392cf534e281cda7568e5fa25f1 Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Tue, 1 Oct 2024 16:39:59 +0200 Subject: [PATCH 17/23] Docstring edit --- Lib/_strptime.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Lib/_strptime.py b/Lib/_strptime.py index eb48180bf5f062..d03feb7a810a9c 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -241,15 +241,15 @@ def __pattern_with_lax_year(self, format): Necessary to ensure that strptime() is able to parse strftime()'s output when %c or %x is used -- considering that for some locales - and platforms (e.g., 'C.UTF-8' on Linux) formatting with either - %c or %x may produce a year number representation which is *not* - zero-padded and, consequently (if the number is small enough), - shorter than the usual four or two digits (e.g., '999' rather - than `0999', or '9' rather than of '09'). + and platforms (e.g., 'C.UTF-8' on Linux) formatting with %c or %x + may produce a year number representation which is not zero-padded + and, consequently (if the number is small enough), shorter than + the usual four or two digits (e.g., '999' rather than `0999', or + '9' rather than of '09'). Note that this helper is intended to be used to prepare regex - patterns *only* for the `%c` and `%x` format codes (and *not* - for `%y`, `%Y` or `%G`). + patterns *only* for the %c and %x format codes (and *not* for + %y, %Y or %G). """ pattern = self.pattern(format) From 949e66a1f56173b6e9f80eaa26eb0ca0531996c7 Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Tue, 1 Oct 2024 22:11:24 +0200 Subject: [PATCH 18/23] Minor test improvements/refactoring --- Lib/test/datetimetester.py | 41 ++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 8a84c408050a29..05d22c08856bcf 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2125,6 +2125,7 @@ def test_fromisocalendar_type_errors(self): self.theclass.fromisocalendar(*isocal) def test_strftime_strptime_roundtrip_concerning_locale_specific_year(self): + # gh-124529 concerned_formats = '%c', '%x' def run_subtest(): @@ -2138,38 +2139,40 @@ def run_subtest(): try: parsed = self.theclass.strptime(formatted, fmt) except ValueError as exc: - # gh-124529 self.fail(f"{fail_msg}; parsing error: {exc!r}") self.assertEqual(parsed, expected, fail_msg) - sample = self.theclass.strptime(f'1999-03-17', '%Y-%m-%d') + sample = self.theclass.strptime('1999', '%Y') for fmt in concerned_formats: with self.subTest(fmt=fmt): sample_str = sample.strftime(fmt) if '1999' in sample_str: for year in [ - 1, 9, 10, 99, 100, 999, # <- gh-124529 1000, 1410, 1989, 2024, 2095, 9999, + # gh-124529: + 1, 9, 10, 99, 100, 999, ]: run_subtest() elif '99' in sample_str: for year in [ - 1969, 1999, - 2000, 2001, 2009, # <- gh-124529 - 2068, + 1969, 1999, 2068, + # gh-124529: + 2000, 2001, 2009, ]: run_subtest() else: - self.fail(f"it seems that {sample.strftime(fmt)=} " - f"does not include year={sample.year!r} in " - f"any expected format (is there something " - f"severely wrong with current locale?)") + self.fail(f"it seems that sample.strftime({fmt!r})=" + f"{sample_str!r} does not include year=" + f"{sample.year!r} in any expected format " + f"(is there something severely wrong with " + f"current locale?)") def test_strptime_accepting_locale_specific_year_with_fewer_digits(self): + # gh-124529 concerned_formats = '%c', '%x' def run_subtest(): - input_str = sample_str.replace(sample_digits, year_digits) + input_str = sample_str.replace(sample_year_digits, year_digits) reason = (f"test strptime accepting locale-specific " f"year representation with fewer digits " f"- for {fmt=} and {input_str=} ({year=})") @@ -2179,27 +2182,27 @@ def run_subtest(): try: parsed = self.theclass.strptime(input_str, fmt) except ValueError as exc: - # gh-124529 self.fail(f"{fail_msg}; parsing error: {exc!r}") self.assertEqual(parsed, expected, fail_msg) - sample = self.theclass.strptime(f'1999-03-17', '%Y-%m-%d') + sample = self.theclass.strptime('1999-03-17', '%Y-%m-%d') for fmt in concerned_formats: with self.subTest(fmt=fmt): sample_str = sample.strftime(fmt) - if (sample_digits := '1999') in sample_str: + if (sample_year_digits := '1999') in sample_str: for year in [1, 9, 10, 99, 100, 999]: year_digits = str(year) run_subtest() - elif (sample_digits := '99') in sample_str: + elif (sample_year_digits := '99') in sample_str: for year in [2000, 2001, 2009]: year_digits = str(year - 2000) run_subtest() else: - self.fail(f"it seems that {sample.strftime(fmt)=} " - f"does not include year={sample.year!r} in " - f"any expected format (is there something " - f"severely wrong with current locale?)") + self.fail(f"it seems that sample.strftime({fmt!r})=" + f"{sample_str!r} does not include year=" + f"{sample.year!r} in any expected format " + f"(is there something severely wrong with " + f"current locale?)") ############################################################################# From 059d4c5cacc40708ed75056a45ca8cb8c6d4acd3 Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Tue, 1 Oct 2024 22:24:31 +0200 Subject: [PATCH 19/23] Add `time.strptime()`-dedicated test --- Lib/test/test_time.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index 530c317a852e77..e8f7cb6ad4eb47 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -307,6 +307,43 @@ def test_strptime_leap_year(self): r'.*day of month without a year.*'): time.strptime('02-07 18:28', '%m-%d %H:%M') + def test_strptime_accepting_locale_specific_year_with_fewer_digits(self): + # GH-124529 + concerned_formats = '%c', '%x' + + def run_subtest(): + input_str = sample_str.replace(sample_year_digits, year_digits) + reason = (f"test strptime accepting locale-specific " + f"year representation with fewer digits " + f"- for {fmt=} and {input_str=} ({year=})") + fail_msg = f"{reason} - failed" + expected = (year,) + sample_tt[1:6] + with self.subTest(reason=reason): + try: + parsed_tt = time.strptime(input_str, fmt) + except ValueError as exc: + self.fail(f"{fail_msg}; parsing error: {exc!r}") + self.assertEqual(parsed_tt[:6], expected, fail_msg) + + sample_tt = (1999, 3, 17) + (0,) * 6 + for fmt in concerned_formats: + with self.subTest(fmt=fmt): + sample_str = time.strftime(fmt, sample_tt) + if (sample_year_digits := '1999') in sample_str: + for year in [1, 9, 10, 99, 100, 999]: + year_digits = str(year) + run_subtest() + elif (sample_year_digits := '99') in sample_str: + for year in [2000, 2001, 2009]: + year_digits = str(year - 2000) + run_subtest() + else: + self.fail(f"it seems that time.strftime(fmt, ...)=" + f"{sample_str!r} does not include year=" + f"{sample_tt[0]!r} in any expected format " + f"(is there something severely wrong with " + f"current locale?)") + def test_asctime(self): time.asctime(time.gmtime(self.t)) From 95753acf1e4113171a431cfacc384e145b9b4b3f Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Tue, 1 Oct 2024 22:42:12 +0200 Subject: [PATCH 20/23] Minor test improvements/refactoring --- Lib/test/datetimetester.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 05d22c08856bcf..07caff787d2a70 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2129,20 +2129,20 @@ def test_strftime_strptime_roundtrip_concerning_locale_specific_year(self): concerned_formats = '%c', '%x' def run_subtest(): + input_ = sample.replace(year=year) reason = (f"test strftime/strptime roundtrip concerning " f"locale-specific year representation " f"- for {fmt=} and {year=}") fail_msg = f"{reason} - failed" - initial = expected = self.theclass.strptime(f'{year:04}', '%Y') with self.subTest(reason=reason): - formatted = initial.strftime(fmt) + formatted = input_.strftime(fmt) try: parsed = self.theclass.strptime(formatted, fmt) except ValueError as exc: self.fail(f"{fail_msg}; parsing error: {exc!r}") - self.assertEqual(parsed, expected, fail_msg) + self.assertEqual(parsed, input_, fail_msg) - sample = self.theclass.strptime('1999', '%Y') + sample = self.theclass.strptime('1999-03-17', '%Y-%m-%d') for fmt in concerned_formats: with self.subTest(fmt=fmt): sample_str = sample.strftime(fmt) From 990b0751074e5c35f8c782e212d7e8396e7f79dd Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Tue, 1 Oct 2024 22:51:55 +0200 Subject: [PATCH 21/23] Minor test improvement --- Lib/test/datetimetester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 07caff787d2a70..5b368d1101b2e5 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2132,7 +2132,7 @@ def run_subtest(): input_ = sample.replace(year=year) reason = (f"test strftime/strptime roundtrip concerning " f"locale-specific year representation " - f"- for {fmt=} and {year=}") + f"- for {fmt=} and {input_=}") fail_msg = f"{reason} - failed" with self.subTest(reason=reason): formatted = input_.strftime(fmt) From ecc359de010d06fed3c65197280a33ef46ac31dd Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Wed, 2 Oct 2024 00:24:58 +0200 Subject: [PATCH 22/23] Test improvements/refactoring --- Lib/test/datetimetester.py | 26 +++++---- Lib/test/test_strptime.py | 106 ++++++++++++++++++------------------- Lib/test/test_time.py | 9 ++-- 3 files changed, 69 insertions(+), 72 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 5b368d1101b2e5..76d6783a6de2e4 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2129,18 +2129,18 @@ def test_strftime_strptime_roundtrip_concerning_locale_specific_year(self): concerned_formats = '%c', '%x' def run_subtest(): - input_ = sample.replace(year=year) + input_obj = sample.replace(year=year) reason = (f"test strftime/strptime roundtrip concerning " f"locale-specific year representation " - f"- for {fmt=} and {input_=}") + f"- for {fmt=} and {input_obj=}") fail_msg = f"{reason} - failed" with self.subTest(reason=reason): - formatted = input_.strftime(fmt) + formatted = input_obj.strftime(fmt) try: parsed = self.theclass.strptime(formatted, fmt) except ValueError as exc: self.fail(f"{fail_msg}; parsing error: {exc!r}") - self.assertEqual(parsed, input_, fail_msg) + self.assertEqual(parsed, input_obj, fail_msg) sample = self.theclass.strptime('1999-03-17', '%Y-%m-%d') for fmt in concerned_formats: @@ -2161,11 +2161,10 @@ def run_subtest(): ]: run_subtest() else: - self.fail(f"it seems that sample.strftime({fmt!r})=" - f"{sample_str!r} does not include year=" - f"{sample.year!r} in any expected format " - f"(is there something severely wrong with " - f"current locale?)") + self.fail(f"{sample!r}.strftime({fmt!r})={sample_str!r} " + f"does not include year={sample.year!r} in " + f"any expected format (is there something " + f"severely wrong with the current locale?)") def test_strptime_accepting_locale_specific_year_with_fewer_digits(self): # gh-124529 @@ -2198,11 +2197,10 @@ def run_subtest(): year_digits = str(year - 2000) run_subtest() else: - self.fail(f"it seems that sample.strftime({fmt!r})=" - f"{sample_str!r} does not include year=" - f"{sample.year!r} in any expected format " - f"(is there something severely wrong with " - f"current locale?)") + self.fail(f"{sample!r}.strftime({fmt!r})={sample_str!r} " + f"does not include year={sample.year!r} in " + f"any expected format (is there something " + f"severely wrong with the current locale?)") ############################################################################# diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py index b2a7da1b62ea4e..175843c26ea9fa 100644 --- a/Lib/test/test_strptime.py +++ b/Lib/test/test_strptime.py @@ -172,14 +172,11 @@ def test_compile(self): fmt = "%" + directive with self.subTest(f"{fmt!r} should match input containing " f"year with fewer digits than usual"): - (input_string, - year) = _get_data_to_test_strptime_with_few_digits_year(fmt) - if year is None: - self.fail(f"it seems that using {fmt=} results in value " - f"which does not include year representation " - f"in any expected format (is there something " - f"severely wrong with current locale?)") - + try: + (input_string, + _) = _get_data_to_test_strptime_with_few_digits_year(fmt) + except AssertionError as exc: + self.fail(str(exc)) compiled = self.time_re.compile(fmt) found = compiled.match(input_string) self.assertTrue(found, @@ -189,13 +186,11 @@ def test_compile(self): fmt = "%" + directive with self.subTest(f"{fmt!r} should not match input containing " f"year with fewer digits than usual"): - (input_string, - year) = _get_data_to_test_strptime_with_few_digits_year(fmt) - if year is None: - self.fail(f"it seems that using {fmt=} results in value " - f"which does not include year representation " - f"in any expected format (is there something " - f"severely wrong with current locale?)") + try: + (input_string, + _) = _get_data_to_test_strptime_with_few_digits_year(fmt) + except AssertionError as exc: + self.fail(str(exc)) compiled = self.time_re.compile(fmt) found = compiled.match(input_string) self.assertFalse(found, @@ -337,13 +332,14 @@ def helper(self, directive, position): def helper_for_directives_accepting_few_digits_year(self, directive): fmt = "%" + directive - (input_string, - expected_year) = _get_data_to_test_strptime_with_few_digits_year(fmt) - if expected_year is None: - self.fail(f"it seems that using {fmt=} results in value " - f"which does not include year representation " - f"in any expected format (is there something " - f"severely wrong with current locale?)") + + try: + (input_string, + expected_year, + ) = _get_data_to_test_strptime_with_few_digits_year(fmt) + except AssertionError as exc: + self.fail(str(exc)) + try: output_year = _strptime._strptime(input_string, fmt)[0][0] except ValueError as exc: @@ -853,47 +849,51 @@ def _get_data_to_test_strptime_with_few_digits_year(fmt): # where: # * -- is a `strftime(fmt)`-result-like str # containing a year number which is *shorter* than the usual four - # or two digits (namely: here the contained year number consist of - # one digit: 7; that's an arbitrary choice); + # or two digits, because the number is small and *not* 0-padded + # (namely: here the contained year number consist of one digit: 7 + # -- that's an arbitrary choice); # * -- is an int representing the year number that # is expected to be part of the result of a `strptime(, fmt)` call (namely: either 7 or 2007, depending - # on the given format string and current locale...); however, it - # is None if does *not* contain the year - # part (for the given format string and current locale). + # on the given format string and current locale...). + # Note: AssertionError (with an appropriate failure message) is + # raised if does *not* contain the year + # part (for the given format string and current locale). - # 1. Prepare auxiliary *magic* time data (note that the magic values + # 1. Prepare auxiliary sample time data (note that the magic values # we use here are guaranteed to be compatible with `time.strftime()`, - # and are also intended to be well distinguishable within a formatted - # string, thanks to the fact that the amount of overloaded numbers is - # minimized, as in `_strptime.LocaleTime.__calc_date_time()`): - magic_year = 1999 - magic_tt = (magic_year, 3, 17, 22, 44, 55, 2, 76, 0) + # and are also intended to be well distinguishable within formatted + # strings, thanks to the fact that the amount of overloaded numbers + # is minimized, as in `_strptime.LocaleTime.__calc_date_time()`): + sample_year = 1999 + sample_tt = (sample_year, 3, 17, 22, 44, 55, 2, 76, 0) + sample_str = time.strftime(fmt, sample_tt) + sample_year_4digits = str(sample_year) + sample_year_2digits = str(sample_year)[-2:] # 2. Pick an arbitrary year number representation that # is always *shorter* than the usual four or two digits: - input_year_str = '7' + year_1digit = '7' # 3. Obtain the resultant 2-tuple: - - input_string = time.strftime(fmt, magic_tt) - expected_year = None - - magic_4digits = str(magic_year) - if found_4digits := (magic_4digits in input_string): - # `input_string` contains up-to-4-digit year representation - input_string = input_string.replace(magic_4digits, input_year_str) - expected_year = int(input_year_str) - - magic_2digits = str(magic_year)[-2:] - if magic_2digits in input_string: - # `input_string` contains up-to-2-digit year representation - if found_4digits: - raise RuntimeError(f'case not supported by this helper: {fmt=} ' - f'(includes both 2-digit and 4-digit year)') - input_string = input_string.replace(magic_2digits, input_year_str) - expected_year = 2000 + int(input_year_str) - + if sample_year_4digits in sample_str: + input_string = sample_str.replace(sample_year_4digits, year_1digit) + if sample_year_2digits in input_string: + raise RuntimeError(f"the {fmt!r} format is not supported by " + f"this helper (a {fmt!r}-formatted string, " + f"{sample_str!r}, seems to include both a " + f"2-digit and 4-digit year number)") + expected_year = int(year_1digit) + elif sample_year_2digits in sample_str: + input_string = sample_str.replace(sample_year_2digits, year_1digit) + expected_year = 2000 + int(year_1digit) + else: + raise AssertionError(f"time.strftime({fmt!r}, ...)={sample_str!r} " + f"does not include the year {sample_year!r} in " + f"any expected format (are {fmt!r}-formatted " + f"strings supposed to include a year number? " + f"if they are, isn't there something severely " + f"wrong with the current locale?)") return input_string, expected_year diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index e8f7cb6ad4eb47..b81c5201e93ac1 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -338,11 +338,10 @@ def run_subtest(): year_digits = str(year - 2000) run_subtest() else: - self.fail(f"it seems that time.strftime(fmt, ...)=" - f"{sample_str!r} does not include year=" - f"{sample_tt[0]!r} in any expected format " - f"(is there something severely wrong with " - f"current locale?)") + self.fail(f"time.strftime({fmt!r}, ...)={sample_str!r} " + f"does not include year={sample_tt[0]!r} in " + f"any expected format (is there something " + f"severely wrong with the current locale?)") def test_asctime(self): time.asctime(time.gmtime(self.t)) From 6d690ca826c76d82058d61083ec43019f3d2c4f6 Mon Sep 17 00:00:00 2001 From: Jan Kaliszewski Date: Wed, 2 Oct 2024 00:28:21 +0200 Subject: [PATCH 23/23] Minor refactoring --- Lib/test/test_strptime.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py index 175843c26ea9fa..85bedcad41cc76 100644 --- a/Lib/test/test_strptime.py +++ b/Lib/test/test_strptime.py @@ -867,7 +867,7 @@ def _get_data_to_test_strptime_with_few_digits_year(fmt): # is minimized, as in `_strptime.LocaleTime.__calc_date_time()`): sample_year = 1999 sample_tt = (sample_year, 3, 17, 22, 44, 55, 2, 76, 0) - sample_str = time.strftime(fmt, sample_tt) + sample_string = time.strftime(fmt, sample_tt) sample_year_4digits = str(sample_year) sample_year_2digits = str(sample_year)[-2:] @@ -876,19 +876,19 @@ def _get_data_to_test_strptime_with_few_digits_year(fmt): year_1digit = '7' # 3. Obtain the resultant 2-tuple: - if sample_year_4digits in sample_str: - input_string = sample_str.replace(sample_year_4digits, year_1digit) + if sample_year_4digits in sample_string: + input_string = sample_string.replace(sample_year_4digits, year_1digit) if sample_year_2digits in input_string: raise RuntimeError(f"the {fmt!r} format is not supported by " f"this helper (a {fmt!r}-formatted string, " - f"{sample_str!r}, seems to include both a " + f"{sample_string!r}, seems to include both a " f"2-digit and 4-digit year number)") expected_year = int(year_1digit) - elif sample_year_2digits in sample_str: - input_string = sample_str.replace(sample_year_2digits, year_1digit) + elif sample_year_2digits in sample_string: + input_string = sample_string.replace(sample_year_2digits, year_1digit) expected_year = 2000 + int(year_1digit) else: - raise AssertionError(f"time.strftime({fmt!r}, ...)={sample_str!r} " + raise AssertionError(f"time.strftime({fmt!r}, ...)={sample_string!r} " f"does not include the year {sample_year!r} in " f"any expected format (are {fmt!r}-formatted " f"strings supposed to include a year number? "