From 09958ccb0f9fec75e5fe924c7ee5ee9b8a3476c9 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 9 Mar 2025 10:40:05 +0000 Subject: [PATCH 1/7] Initial --- Lib/_pydatetime.py | 4 ++-- .../Library/2025-03-09-10-37-00.gh-issue-89157.qg3r138.rst | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-03-09-10-37-00.gh-issue-89157.qg3r138.rst diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index fcf4416f331092..739875cd359244 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -1051,8 +1051,8 @@ def fromordinal(cls, n): @classmethod def fromisoformat(cls, date_string): """Construct a date from a string in ISO 8601 format.""" - if not isinstance(date_string, str): - raise TypeError('fromisoformat: argument must be str') + if not isinstance(date_string, str) or not date_string.isascii(): + raise TypeError('fromisoformat: argument must be an ASCII str') if len(date_string) not in (7, 8, 10): raise ValueError(f'Invalid isoformat string: {date_string!r}') diff --git a/Misc/NEWS.d/next/Library/2025-03-09-10-37-00.gh-issue-89157.qg3r138.rst b/Misc/NEWS.d/next/Library/2025-03-09-10-37-00.gh-issue-89157.qg3r138.rst new file mode 100644 index 00000000000000..9405c5da0fc26f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-03-09-10-37-00.gh-issue-89157.qg3r138.rst @@ -0,0 +1,4 @@ +The pure Python implementation of :func:`datetime.date.fromisoformat`, +:func:`_pydatetime.date.fromisoformat` now only accepts ASCII strings for +consistency with the C implementation. + From 37140a98f989faad26f20d1f2b94e957c86b29ff Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 9 Mar 2025 11:18:03 +0000 Subject: [PATCH 2/7] Update test --- Lib/test/datetimetester.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index ceeac9435dcb85..71f7d4b3d6ec12 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3534,7 +3534,7 @@ def test_fromisoformat_fails_datetime(self): '2009-04-19T03:15:4500:00', # Bad time zone separator '2009-04-19T03:15:45.123456+24:30', # Invalid time zone offset '2009-04-19T03:15:45.123456-24:30', # Invalid negative offset - '2009-04-10ᛇᛇᛇᛇᛇ12:15', # Too many unicode separators + '2009-04-10ᛇᛇᛇᛇᛇ12:15', # Unicode chars '2009-04\ud80010T12:15', # Surrogate char in date '2009-04-10T12\ud80015', # Surrogate char in time '2009-04-19T1', # Incomplete hours @@ -3560,7 +3560,7 @@ def test_fromisoformat_fails_datetime(self): for bad_str in bad_strs: with self.subTest(bad_str=bad_str): - with self.assertRaises(ValueError): + with self.assertRaises(ValueError, TypeError): self.theclass.fromisoformat(bad_str) def test_fromisoformat_fails_datetime_valueerror(self): From 2cf9d18715723a7fdc51fcea47752b9cd5c3b8a8 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 9 Mar 2025 11:20:15 +0000 Subject: [PATCH 3/7] Clean up test --- Lib/test/datetimetester.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 71f7d4b3d6ec12..f733e350f6a327 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3560,7 +3560,8 @@ def test_fromisoformat_fails_datetime(self): for bad_str in bad_strs: with self.subTest(bad_str=bad_str): - with self.assertRaises(ValueError, TypeError): + errors = (ValueError, TypeError) + with self.assertRaises(errors): self.theclass.fromisoformat(bad_str) def test_fromisoformat_fails_datetime_valueerror(self): From 1a6d229f548d24d6fd12700c8f71f35271ee7f7e Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 9 Mar 2025 11:31:10 +0000 Subject: [PATCH 4/7] Fix test --- Lib/test/datetimetester.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index f733e350f6a327..940543d553fcb9 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3560,8 +3560,7 @@ def test_fromisoformat_fails_datetime(self): for bad_str in bad_strs: with self.subTest(bad_str=bad_str): - errors = (ValueError, TypeError) - with self.assertRaises(errors): + with self.assertRaises((ValueError, TypeError)): self.theclass.fromisoformat(bad_str) def test_fromisoformat_fails_datetime_valueerror(self): From 644ab29f1993f39233d48de6ef768628894d5a79 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 9 Mar 2025 18:18:04 +0000 Subject: [PATCH 5/7] Fix tests --- Lib/_pydatetime.py | 8 ++++-- Lib/test/datetimetester.py | 52 +++++++++++++++++++++++++------------- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 739875cd359244..b88d2a2bfdac69 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -1051,8 +1051,12 @@ def fromordinal(cls, n): @classmethod def fromisoformat(cls, date_string): """Construct a date from a string in ISO 8601 format.""" - if not isinstance(date_string, str) or not date_string.isascii(): - raise TypeError('fromisoformat: argument must be an ASCII str') + + if not isinstance(date_string, str): + raise TypeError('Argument must be a str') + + if not date_string.isascii(): + raise ValueError('Argument must be an ASCII str') if len(date_string) not in (7, 8, 10): raise ValueError(f'Invalid isoformat string: {date_string!r}') diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 940543d553fcb9..0844e8cb68106e 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2068,22 +2068,40 @@ class DateSubclass(self.theclass): def test_fromisoformat_fails(self): # Test that fromisoformat() fails on invalid values bad_strs = [ - '', # Empty string - '\ud800', # bpo-34454: Surrogate code point - '009-03-04', # Not 10 characters - '123456789', # Not a date - '200a-12-04', # Invalid character in year - '2009-1a-04', # Invalid character in month - '2009-12-0a', # Invalid character in day - '2009-01-32', # Invalid day - '2009-02-29', # Invalid leap day - '2019-W53-1', # No week 53 in 2019 - '2020-W54-1', # No week 54 - '0000-W25-1', # Invalid year - '10000-W25-1', # Invalid year - '2020-W25-0', # Invalid day-of-week - '2020-W25-8', # Invalid day-of-week - '2009\ud80002\ud80028', # Separators are surrogate codepoints + '', # Empty string + '\ud800', # bpo-34454: Surrogate code point + '2009.04-19T03', # Wrong first separator + '2009-04.19T03', # Wrong second separator + '2009-04-19T0a', # Invalid hours + '2009-04-19T03:1a:45', # Invalid minutes + '2009-04-19T03:15:4a', # Invalid seconds + '2009-04-19T03;15:45', # Bad first time separator + '2009-04-19T03:15;45', # Bad second time separator + '2009-04-19T03:15:4500:00', # Bad time zone separator + '2009-04-19T03:15:45.123456+24:30', # Invalid time zone offset + '2009-04-19T03:15:45.123456-24:30', # Invalid negative offset + '2009-04-10ᛇᛇᛇᛇᛇ12:15', # Unicode chars + '2009-04\ud80010T12:15', # Surrogate char in date + '2009-04-10T12\ud80015', # Surrogate char in time + '2009-04-19T1', # Incomplete hours + '2009-04-19T12:3', # Incomplete minutes + '2009-04-19T12:30:4', # Incomplete seconds + '2009-04-19T12:', # Ends with time separator + '2009-04-19T12:30:', # Ends with time separator + '2009-04-19T12:30:45.', # Ends with time separator + '2009-04-19T12:30:45.123456+', # Ends with timezone separator + '2009-04-19T12:30:45.123456-', # Ends with timezone separator + '2009-04-19T12:30:45.123456-05:00a', # Extra text + '2009-04-19T12:30:45.123-05:00a', # Extra text + '2009-04-19T12:30:45-05:00a', # Extra text + '2009-04-19T24:00:00.000001', # Has non-zero microseconds on 24:00 + '2009-04-19T24:00:01.000000', # Has non-zero seconds on 24:00 + '2009-04-19T24:01:00.000000', # Has non-zero minutes on 24:00 + '2009-04-32T24:00:00.000000', # Day is invalid before wrapping due to 24:00 + '2009-13-01T24:00:00.000000', # Month is invalid before wrapping due to 24:00 + '9999-12-31T24:00:00.000000', # Year is invalid after wrapping due to 24:00 + '2009-04-19T12:30Z12:00', # Extra time zone info after Z + '2009-04-19T12:30:45:334034', # Invalid microsecond separator ] for bad_str in bad_strs: @@ -3560,7 +3578,7 @@ def test_fromisoformat_fails_datetime(self): for bad_str in bad_strs: with self.subTest(bad_str=bad_str): - with self.assertRaises((ValueError, TypeError)): + with self.assertRaises(ValueError): self.theclass.fromisoformat(bad_str) def test_fromisoformat_fails_datetime_valueerror(self): From d7424720b1d116ae3047bf74fa1f30d61b8c7881 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 9 Mar 2025 18:28:44 +0000 Subject: [PATCH 6/7] Fix tests --- Lib/test/datetimetester.py | 51 +++++++++++++------------------------- 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 0844e8cb68106e..803049f021b28d 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2068,40 +2068,23 @@ class DateSubclass(self.theclass): def test_fromisoformat_fails(self): # Test that fromisoformat() fails on invalid values bad_strs = [ - '', # Empty string - '\ud800', # bpo-34454: Surrogate code point - '2009.04-19T03', # Wrong first separator - '2009-04.19T03', # Wrong second separator - '2009-04-19T0a', # Invalid hours - '2009-04-19T03:1a:45', # Invalid minutes - '2009-04-19T03:15:4a', # Invalid seconds - '2009-04-19T03;15:45', # Bad first time separator - '2009-04-19T03:15;45', # Bad second time separator - '2009-04-19T03:15:4500:00', # Bad time zone separator - '2009-04-19T03:15:45.123456+24:30', # Invalid time zone offset - '2009-04-19T03:15:45.123456-24:30', # Invalid negative offset - '2009-04-10ᛇᛇᛇᛇᛇ12:15', # Unicode chars - '2009-04\ud80010T12:15', # Surrogate char in date - '2009-04-10T12\ud80015', # Surrogate char in time - '2009-04-19T1', # Incomplete hours - '2009-04-19T12:3', # Incomplete minutes - '2009-04-19T12:30:4', # Incomplete seconds - '2009-04-19T12:', # Ends with time separator - '2009-04-19T12:30:', # Ends with time separator - '2009-04-19T12:30:45.', # Ends with time separator - '2009-04-19T12:30:45.123456+', # Ends with timezone separator - '2009-04-19T12:30:45.123456-', # Ends with timezone separator - '2009-04-19T12:30:45.123456-05:00a', # Extra text - '2009-04-19T12:30:45.123-05:00a', # Extra text - '2009-04-19T12:30:45-05:00a', # Extra text - '2009-04-19T24:00:00.000001', # Has non-zero microseconds on 24:00 - '2009-04-19T24:00:01.000000', # Has non-zero seconds on 24:00 - '2009-04-19T24:01:00.000000', # Has non-zero minutes on 24:00 - '2009-04-32T24:00:00.000000', # Day is invalid before wrapping due to 24:00 - '2009-13-01T24:00:00.000000', # Month is invalid before wrapping due to 24:00 - '9999-12-31T24:00:00.000000', # Year is invalid after wrapping due to 24:00 - '2009-04-19T12:30Z12:00', # Extra time zone info after Z - '2009-04-19T12:30:45:334034', # Invalid microsecond separator + '', # Empty string + '\ud800', # bpo-34454: Surrogate code point + '009-03-04', # Not 10 characters + '123456789', # Not a date + '200a-12-04', # Invalid character in year + '2009-1a-04', # Invalid character in month + '2009-12-0a', # Invalid character in day + '2009-01-32', # Invalid day + '2009-02-29', # Invalid leap day + '2019-W53-1', # No week 53 in 2019 + '2020-W54-1', # No week 54 + '0000-W25-1', # Invalid year + '10000-W25-1', # Invalid year + '2020-W25-0', # Invalid day-of-week + '2020-W25-8', # Invalid day-of-week + '٢025-03-09' # Unicode characters + '2009\ud80002\ud80028', # Separators are surrogate codepoints ] for bad_str in bad_strs: From 9ff8a9ef9a70d00cdbc8edcaafe933fcc0213cf5 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 9 Mar 2025 18:47:26 +0000 Subject: [PATCH 7/7] NEWS fix --- .../Library/2025-03-09-10-37-00.gh-issue-89157.qg3r138.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2025-03-09-10-37-00.gh-issue-89157.qg3r138.rst b/Misc/NEWS.d/next/Library/2025-03-09-10-37-00.gh-issue-89157.qg3r138.rst index 9405c5da0fc26f..0031721f89b719 100644 --- a/Misc/NEWS.d/next/Library/2025-03-09-10-37-00.gh-issue-89157.qg3r138.rst +++ b/Misc/NEWS.d/next/Library/2025-03-09-10-37-00.gh-issue-89157.qg3r138.rst @@ -1,4 +1,2 @@ -The pure Python implementation of :func:`datetime.date.fromisoformat`, -:func:`_pydatetime.date.fromisoformat` now only accepts ASCII strings for -consistency with the C implementation. - +Make the pure Python implementation of :func:`datetime.date.fromisoformat`, +only accept ASCII strings for consistency with the C implementation.