From 1a8a6a4e107df4364745815d9cd13a1094de8d63 Mon Sep 17 00:00:00 2001 From: mola <229602071+Mola-maker@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:35:25 +0800 Subject: [PATCH] Support Python 3.14 unparenthesized exception lists --- CHANGELOG.md | 2 + .../yapf_third_party/_ylib2to3/Grammar.txt | 3 +- yapf/yapflib/logical_line.py | 4 +- yapftests/pytree_utils_test.py | 9 ++++ yapftests/reformatter_python3_test.py | 48 +++++++++++++++++++ 5 files changed, 63 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccc8726c9..8dd567a75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ## (0.41.0) UNRELEASED ### Added +- Support Python 3.14 unparenthesized exception lists in `except` and `except*` + clauses (PEP 758). - New `DISABLE_SPLIT_LIST_WITH_COMMENT` flag. `DISABLE_SPLIT_LIST_WITH_COMMENT` is a new knob that changes the behavior of splitting a list when a comment is present inside the list. diff --git a/third_party/yapf_third_party/_ylib2to3/Grammar.txt b/third_party/yapf_third_party/_ylib2to3/Grammar.txt index bd8a452a3..e1c20dbfa 100644 --- a/third_party/yapf_third_party/_ylib2to3/Grammar.txt +++ b/third_party/yapf_third_party/_ylib2to3/Grammar.txt @@ -119,7 +119,8 @@ try_stmt: ('try' ':' suite with_stmt: 'with' asexpr_test (',' asexpr_test)* ':' suite # NB compile.c makes sure that the default except clause is last -except_clause: 'except' ['*'] [test [(',' | 'as') test]] +# PEP 758 allows an unparenthesized exception list without an 'as' target. +except_clause: 'except' ['*'] [test ('as' test | (',' test)* [','])] suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT # Backward compatibility cruft to support: diff --git a/yapf/yapflib/logical_line.py b/yapf/yapflib/logical_line.py index 4433276f7..005a18c1a 100644 --- a/yapf/yapflib/logical_line.py +++ b/yapf/yapflib/logical_line.py @@ -297,8 +297,8 @@ def _SpaceRequiredBetween(left, right, is_line_disabled): # Spaces between keyword, string, number, and identifier tokens. return True if lval == ',' and rval == ':': - # We do want a space between a comma and colon. - return True + # Keep a space in slices, but not after a trailing exception-list comma. + return pytree_utils.NodeName(left.node.parent) != 'except_clause' if style.Get('SPACE_INSIDE_BRACKETS'): # Supersede the "no space before a colon or comma" check. if left.OpensScope() and rval == ':': diff --git a/yapftests/pytree_utils_test.py b/yapftests/pytree_utils_test.py index fe31eb83c..725db8577 100644 --- a/yapftests/pytree_utils_test.py +++ b/yapftests/pytree_utils_test.py @@ -72,6 +72,15 @@ def testClassNotLocal(self): with self.assertRaises(SyntaxError): pytree_utils.ParseCodeToTree('class nonlocal: pass\n') + def testUnparenthesizedExceptClausesWithAs(self): + for keyword in ('except', 'except*'): + for exceptions in ('ValueError, TypeError', 'ValueError,'): + with self.subTest(keyword=keyword, exceptions=exceptions): + code = 'try:\n pass\n{} {} as error:\n pass\n'.format( + keyword, exceptions) + with self.assertRaises(SyntaxError): + pytree_utils.ParseCodeToTree(code) + class InsertNodesBeforeAfterTest(yapf_test_helper.YAPFTest): diff --git a/yapftests/reformatter_python3_test.py b/yapftests/reformatter_python3_test.py index f5741b313..005095a63 100644 --- a/yapftests/reformatter_python3_test.py +++ b/yapftests/reformatter_python3_test.py @@ -19,6 +19,7 @@ from yapf.yapflib import reformatter from yapf.yapflib import style +from yapf.yapflib import yapf_api from yapftests import yapf_test_helper @@ -30,6 +31,53 @@ class TestsForPython3Code(yapf_test_helper.YAPFTest): def setUpClass(cls): # pylint: disable=g-missing-super-call style.SetGlobalStyle(style.CreatePEP8Style()) + def testUnparenthesizedExceptClauses(self): + # https://github.com/google/yapf/issues/1319 + for keyword in ('except', 'except*'): + for exceptions, formatted_exceptions in ( + ('ZeroDivisionError,ValueError,IndexError', + 'ZeroDivisionError, ValueError, IndexError'), + ('ValueError,TypeError,IndexError,KeyError', + 'ValueError, TypeError, IndexError, KeyError'), + ('ValueError,', 'ValueError,'), + ('ValueError,TypeError,', 'ValueError, TypeError,'), + ): + with self.subTest(keyword=keyword, exceptions=exceptions): + unformatted_code = ('try:\n x=1/0\n{} {}:\n pass\n'.format( + keyword, exceptions)) + expected_formatted_code = ( + 'try:\n x = 1 / 0\n{} {}:\n pass\n'.format( + keyword.replace('*', ' *'), formatted_exceptions)) + formatted_code, changed = yapf_api.FormatCode( + unformatted_code, style_config='pep8') + self.assertTrue(changed) + self.assertCodeEqual(expected_formatted_code, formatted_code) + self.assertEqual( + (formatted_code, False), + yapf_api.FormatCode(formatted_code, style_config='pep8')) + + def testLongUnparenthesizedExceptClauses(self): + # Without parentheses, splitting the exception list would be invalid. + for keyword in ('except', 'except *'): + with self.subTest(keyword=keyword): + code = ('try:\n pass\n' + '{} module.FirstException, module.SecondException, ' + 'module.ThirdException, module.FourthException:\n' + ' pass\n').format(keyword) + self.assertEqual((code, False), + yapf_api.FormatCode(code, style_config='pep8')) + + def testExistingExceptClauses(self): + for keyword in ('except', 'except *'): + for exceptions in ('ValueError', 'ValueError as error', + '(ValueError, TypeError)', + '(ValueError, TypeError) as error'): + with self.subTest(keyword=keyword, exceptions=exceptions): + code = 'try:\n pass\n{} {}:\n pass\n'.format( + keyword, exceptions) + self.assertEqual((code, False), + yapf_api.FormatCode(code, style_config='pep8')) + def testTypedNames(self): unformatted_code = textwrap.dedent("""\ def x(aaaaaaaaaaaaaaa:int,bbbbbbbbbbbbbbbb:str,ccccccccccccccc:dict,eeeeeeeeeeeeee:set={1, 2, 3})->bool: