diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 16ed3215bc2c1a..d062c113763f04 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2631,6 +2631,10 @@ differences between platforms in handling of unsupported format specifiers. .. versionadded:: 3.12 ``%:z`` was added. +.. versionchanged:: next + Non-ASCII digits are now rejected by ``strptime`` for non-locale-specific + numerical directives. + Technical Detail ^^^^^^^^^^^^^^^^ @@ -2670,7 +2674,8 @@ Notes: Because the format depends on the current locale, care should be taken when making assumptions about the output value. Field orderings will vary (for example, "month/day/year" versus "day/month/year"), and the output may - contain non-ASCII characters. + contain non-ASCII characters. :meth:`~.datetime.strptime` rejects non-ASCII + digits for non-locale-specific numeric format codes (e.g. ``%Y``, ``%H``, etc). (2) The :meth:`~.datetime.strptime` method can parse years in the full [1, 9999] range, but diff --git a/Doc/library/time.rst b/Doc/library/time.rst index 542493a82af94d..caa454fb6d5e87 100644 --- a/Doc/library/time.rst +++ b/Doc/library/time.rst @@ -567,6 +567,10 @@ Functions When used with the :func:`strptime` function, ``%U`` and ``%W`` are only used in calculations when the day of the week and the year are specified. + (5) + The :func:`strptime` function does not accept non-ASCII digits for + non-locale-specific numeric format codes. + Here is an example, a format for dates compatible with that specified in the :rfc:`2822` Internet email standard. [1]_ :: @@ -590,9 +594,8 @@ Functions .. function:: strptime(string[, format]) - Parse a string representing a time according to a format. The return value - is a :class:`struct_time` as returned by :func:`gmtime` or - :func:`localtime`. + Parse a string representing a time according to a format string. The return + value is a :class:`struct_time` as returned by :func:`gmtime` or :func:`localtime`. The *format* parameter uses the same directives as those used by :func:`strftime`; it defaults to ``"%a %b %d %H:%M:%S %Y"`` which matches the @@ -620,6 +623,10 @@ Functions and thus does not necessarily support all directives available that are not documented as supported. +.. versionchanged:: next + Non-ASCII digits are now rejected by ``strptime`` for non-locale-specific + format codes. + .. class:: struct_time diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index cbca720b75e96c..c345407e0dd780 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -1370,6 +1370,7 @@ datetime * Add :meth:`datetime.time.strptime` and :meth:`datetime.date.strptime`. (Contributed by Wannes Boeykens in :gh:`41431`.) + decimal ------- diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 9f327cf904da1b..4458ca450ffb4d 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -89,6 +89,16 @@ New modules Improved modules ================ +datetime +-------- + +* When using digits in :meth:`datetime.date.strptime`, + :meth:`datetime.datetime.strptime`, or :meth:`datetime.time.strptime`, a + :exc:`ValueError` is raised if non-ASCII digits are specified for + non-locale-specific numeric format codes. + (Contributed by Stan Ulbrych in :gh:`131008`.) + + dbm --- @@ -171,6 +181,14 @@ tarfile and :cve:`2025-4435`.) +time +---- + +* When using digits in :func:`time.strptime`, a :exc:`ValueError` is raised if + non-ASCII digits are specified for non-locale-specific numeric format codes. + (Contributed by Stan Ulbrych in :gh:`131008`.) + + zlib ---- @@ -181,6 +199,7 @@ zlib (Contributed by Bénédikt Tran in :gh:`134635`.) + .. Add improved modules above alphabetically, not here at the end. Optimizations diff --git a/Lib/_strptime.py b/Lib/_strptime.py index ae67949626d460..90a21756c3dfc5 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -286,23 +286,23 @@ def __init__(self, locale_time=None): base = super() mapping = { # The " [1-9]" part of the regex is to make %c from ANSI C work - 'd': r"(?P3[0-1]|[1-2]\d|0[1-9]|[1-9]| [1-9])", + 'd': r"(?P3[0-1]|[1-2][0-9]|0[1-9]|[1-9]| [1-9])", 'f': r"(?P[0-9]{1,6})", - 'H': r"(?P2[0-3]|[0-1]\d|\d)", + 'H': r"(?P2[0-3]|[0-1][0-9]|[0-9])", 'I': r"(?P1[0-2]|0[1-9]|[1-9]| [1-9])", - 'G': r"(?P\d\d\d\d)", - 'j': r"(?P36[0-6]|3[0-5]\d|[1-2]\d\d|0[1-9]\d|00[1-9]|[1-9]\d|0[1-9]|[1-9])", + 'G': r"(?P[0-9][0-9][0-9][0-9])", + 'j': r"(?P36[0-6]|3[0-5][0-9]|[1-2][0-9][0-9]|0[1-9][0-9]|00[1-9]|[1-9][0-9]|0[1-9]|[1-9])", 'm': r"(?P1[0-2]|0[1-9]|[1-9])", - 'M': r"(?P[0-5]\d|\d)", - 'S': r"(?P6[0-1]|[0-5]\d|\d)", - 'U': r"(?P5[0-3]|[0-4]\d|\d)", + 'M': r"(?P[0-5][0-9]|[0-9])", + 'S': r"(?P6[0-1]|[0-5][0-9]|[0-9])", + 'U': r"(?P5[0-3]|[0-4][0-9]|[0-9])", 'w': r"(?P[0-6])", 'u': r"(?P[1-7])", - 'V': r"(?P5[0-3]|0[1-9]|[1-4]\d|\d)", + 'V': r"(?P5[0-3]|0[1-9]|[1-4][0-9]|[0-9])", # 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))?", + 'y': r"(?P[0-9][0-9])", + 'Y': r"(?P[0-9]{4})", + 'z': r"(?P([+-][0-9][0-9]:?[0-5][0-9](:?[0-5][0-9](\.[0-9]{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'), diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 93b3382b9c654e..aa95ab8b04a1f1 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2924,6 +2924,14 @@ def test_strptime(self): with self.assertRaises(ValueError): strptime("-000", "%z") with self.assertRaises(ValueError): strptime("z", "%z") + # test only ascii is allowed + with self.assertRaises(ValueError): strptime('٢025-0٢-٢9', '%Y-%m-%d') + with self.assertRaises(ValueError): strptime('1٢:02:٢7', '%H:%M:%S') + with self.assertRaises(ValueError): strptime('٢5', '%y') + with self.assertRaises(ValueError): strptime('٢555', '%G') + with self.assertRaises(ValueError): strptime('٢/0٢ 0٢a٢', '%j/%y %I%p:%M:%S') + with self.assertRaises(ValueError): strptime('0٢/٢/200٢', '%U/%V') + def test_strptime_single_digit(self): # bpo-34903: Check that single digit dates and times are allowed. @@ -4070,7 +4078,7 @@ def test_strptime_tz(self): self.assertEqual(strptime("UTC", "%Z").tzinfo, None) def test_strptime_errors(self): - for tzstr in ("-2400", "-000", "z"): + for tzstr in ("-2400", "-000", "z", "٢"): with self.assertRaises(ValueError): self.theclass.strptime(tzstr, "%z") diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index 5312faa50774ec..37d12d5d502242 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -357,6 +357,10 @@ 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_non_ascii(self): + with self.assertRaises(ValueError): + time.strptime('٢025', '%Y') + def test_asctime(self): time.asctime(time.gmtime(self.t)) diff --git a/Misc/NEWS.d/next/Library/2025-03-09-11-01-00.gh-issue-83461.auwd13.rst b/Misc/NEWS.d/next/Library/2025-03-09-11-01-00.gh-issue-83461.auwd13.rst new file mode 100644 index 00000000000000..46a88a06b51f42 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-03-09-11-01-00.gh-issue-83461.auwd13.rst @@ -0,0 +1,3 @@ +Digits passed to :meth:`datetime.date.strptime`, :meth:`datetime.datetime.strptime`, +:meth:`datetime.time.strptime` and :meth:`time.strptime` must now be ASCII in +non-locale-specific numeric format codes.