diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index f3c4ef9199075c9..7f31dfe28e897db 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2721,6 +2721,10 @@ differences between platforms in handling of unsupported format specifiers. ``%D``, ``%F``, ``%n``, ``%t``, and ``%:z`` were added for :meth:`~.datetime.strptime`. +.. versionchanged:: next + Non-ASCII digits are now rejected by ``strptime`` for non-locale-specific + numerical directives. + Technical detail ^^^^^^^^^^^^^^^^ @@ -2802,7 +2806,9 @@ 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 (for example, ``%Y`` and + ``%H``). (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 a931134331f0a5c..544b9989409451e 100644 --- a/Doc/library/time.rst +++ b/Doc/library/time.rst @@ -583,6 +583,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:`5322` Internet email standard. [1]_ :: @@ -606,9 +610,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 @@ -636,6 +639,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.16.rst b/Doc/whatsnew/3.16.rst index c06e6930ac78bff..159a517b43ab34a 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -622,6 +622,15 @@ asyncio Use :func:`asyncio.run` or :class:`asyncio.Runner` with a custom *loop_factory* instead. +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`.) + functools --------- @@ -677,6 +686,13 @@ tarfile * The undocumented and unused :attr:`!tarfile.TarFile.tarfile` attribute has been deprecated since Python 3.13. +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`.) + .. Add removals above alphabetically, not here at the end. diff --git a/Lib/_strptime.py b/Lib/_strptime.py index 59ac96745aa15e2..9506dc81aa8e490 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -353,27 +353,27 @@ 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| \d)", - 'k': r"(?P2[0-3]|[0-1]\d|\d| \d)", + 'H': r"(?P2[0-3]|[0-1][0-9]|[0-9]| [0-9])", + 'k': r"(?P2[0-3]|[0-1][0-9]|[0-9]| [0-9])", 'I': r"(?P1[0-2]|0[1-9]|[1-9]| [1-9])", 'l': 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)", + 'y': r"(?P[0-9][0-9])", + 'Y': r"(?P[0-9]{4})", # See gh-121237: "z" must support colons for backwards compatibility. - '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))?", + 'z': r"(?P([+-][0-9][0-9]:?[0-5][0-9](:?[0-5][0-9](\.[0-9]{1,6})?)?)|(?-i:Z))?", + ':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(_fixmonths(self.locale_time.f_month[1:]), 'B'), diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index c11e9c068bed3bd..fa1b07d79bc2688 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3080,6 +3080,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_ampm(self): dt = datetime(1999, 3, 17, 0, 44, 55, 2) for hour in range(0, 24): @@ -4291,7 +4299,7 @@ def test_strptime_tz(self): self.assertEqual(strptime("UTC", "%Z").tzinfo, None) def test_strptime_errors(self): - for tzstr in ("-2400", "-000", "z", "24:00"): + for tzstr in ("-2400", "-000", "z", "24:00", "٢"): with self.assertRaises(ValueError): self.theclass.strptime(tzstr, "%z") with self.assertRaises(ValueError): diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index 985984b55123ce6..ab89e5827cd32c7 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -395,6 +395,10 @@ def test_strptime_leap_year(self): r'.*day of month without a year.*'): time.strptime('02-07 18:28', '%m-%e %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 000000000000000..46a88a06b51f42b --- /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.