[3.13] gh-131434: Improve error reporting for incorrect format in strptime() (GH-131568) (GH-132309)

In particularly, fix regression in detecting stray % at the end of the
format string.
(cherry picked from commit 3feac7a093)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
Miss Islington (bot) 2025-04-09 12:50:24 +02:00 committed by GitHub
parent 8e97bd7ef4
commit e2d13b42ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 23 additions and 23 deletions

View File

@ -365,7 +365,7 @@ def repl(m):
nonlocal day_of_month_in_format nonlocal day_of_month_in_format
day_of_month_in_format = True day_of_month_in_format = True
return self[format_char] return self[format_char]
format = re_sub(r'%(O?.)', repl, format) format = re_sub(r'%([OE]?\\?.?)', repl, format)
if day_of_month_in_format and not year_in_format: if day_of_month_in_format and not year_in_format:
import warnings import warnings
warnings.warn("""\ warnings.warn("""\
@ -439,14 +439,13 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
# \\, in which case it was a stray % but with a space after it # \\, in which case it was a stray % but with a space after it
except KeyError as err: except KeyError as err:
bad_directive = err.args[0] bad_directive = err.args[0]
if bad_directive == "\\":
bad_directive = "%"
del err del err
bad_directive = bad_directive.replace('\\s', '')
if not bad_directive:
raise ValueError("stray %% in format '%s'" % format) from None
bad_directive = bad_directive.replace('\\', '', 1)
raise ValueError("'%s' is a bad directive in format '%s'" % raise ValueError("'%s' is a bad directive in format '%s'" %
(bad_directive, format)) from None (bad_directive, format)) from None
# IndexError only occurs when the format string is "%"
except IndexError:
raise ValueError("stray %% in format '%s'" % format) from None
_regex_cache[format] = format_regex _regex_cache[format] = format_regex
found = format_regex.match(data_string) found = format_regex.match(data_string)
if not found: if not found:

View File

@ -223,16 +223,16 @@ def test_ValueError(self):
# Make sure ValueError is raised when match fails or format is bad # Make sure ValueError is raised when match fails or format is bad
self.assertRaises(ValueError, _strptime._strptime_time, data_string="%d", self.assertRaises(ValueError, _strptime._strptime_time, data_string="%d",
format="%A") format="%A")
for bad_format in ("%", "% ", "%e"): for bad_format in ("%", "% ", "%\n"):
try: with self.assertRaisesRegex(ValueError, "stray % in format "):
_strptime._strptime_time("2005", bad_format)
for bad_format in ("%e", "%Oe", "%O", "%O ", "%Ee", "%E", "%E ",
"%.", "%+", "%_", "%~", "%\\",
"%O.", "%O+", "%O_", "%O~", "%O\\"):
directive = bad_format[1:].rstrip()
with self.assertRaisesRegex(ValueError,
f"'{re.escape(directive)}' is a bad directive in format "):
_strptime._strptime_time("2005", bad_format) _strptime._strptime_time("2005", bad_format)
except ValueError:
continue
except Exception as err:
self.fail("'%s' raised %s, not ValueError" %
(bad_format, err.__class__.__name__))
else:
self.fail("'%s' did not raise ValueError" % bad_format)
msg_week_no_year_or_weekday = r"ISO week directive '%V' must be used with " \ msg_week_no_year_or_weekday = r"ISO week directive '%V' must be used with " \
r"the ISO year directive '%G' and a weekday directive " \ r"the ISO year directive '%G' and a weekday directive " \
@ -288,11 +288,11 @@ def test_strptime_exception_context(self):
# check that this doesn't chain exceptions needlessly (see #17572) # check that this doesn't chain exceptions needlessly (see #17572)
with self.assertRaises(ValueError) as e: with self.assertRaises(ValueError) as e:
_strptime._strptime_time('', '%D') _strptime._strptime_time('', '%D')
self.assertIs(e.exception.__suppress_context__, True) self.assertTrue(e.exception.__suppress_context__)
# additional check for IndexError branch (issue #19545) # additional check for stray % branch
with self.assertRaises(ValueError) as e: with self.assertRaises(ValueError) as e:
_strptime._strptime_time('19', '%Y %') _strptime._strptime_time('%', '%')
self.assertIsNone(e.exception.__context__) self.assertTrue(e.exception.__suppress_context__)
def test_unconverteddata(self): def test_unconverteddata(self):
# Check ValueError is raised when there is unconverted data # Check ValueError is raised when there is unconverted data

View File

@ -339,11 +339,11 @@ def test_strptime_exception_context(self):
# check that this doesn't chain exceptions needlessly (see #17572) # check that this doesn't chain exceptions needlessly (see #17572)
with self.assertRaises(ValueError) as e: with self.assertRaises(ValueError) as e:
time.strptime('', '%D') time.strptime('', '%D')
self.assertIs(e.exception.__suppress_context__, True) self.assertTrue(e.exception.__suppress_context__)
# additional check for IndexError branch (issue #19545) # additional check for stray % branch
with self.assertRaises(ValueError) as e: with self.assertRaises(ValueError) as e:
time.strptime('19', '%Y %') time.strptime('%', '%')
self.assertIsNone(e.exception.__context__) self.assertTrue(e.exception.__suppress_context__)
def test_strptime_leap_year(self): def test_strptime_leap_year(self):
# GH-70647: warns if parsing a format with a day and no year. # GH-70647: warns if parsing a format with a day and no year.

View File

@ -0,0 +1 @@
Improve error reporting for incorrect format in :func:`time.strptime`.