From 3b5329aa2022d97972d2f58eefe61b759b8e56c1 Mon Sep 17 00:00:00 2001 From: martin <48778384+drinkmorewaterr@users.noreply.github.com> Date: Tue, 9 Sep 2025 11:16:49 -0400 Subject: [PATCH] feat: add support for PEP758 (#1401) PEP758 removes the requirement for parentheses to surround exceptions in except and except* expressions when 'as' is not present. This pr implements support for parsing these types of statements --- libcst/_nodes/tests/test_try.py | 60 +++++++++++++++++++ native/libcst/src/parser/grammar.rs | 37 +++++++++--- .../libcst/tests/fixtures/terrible_tries.py | 22 +++++++ 3 files changed, 110 insertions(+), 9 deletions(-) diff --git a/libcst/_nodes/tests/test_try.py b/libcst/_nodes/tests/test_try.py index 5704d098..c5ae2462 100644 --- a/libcst/_nodes/tests/test_try.py +++ b/libcst/_nodes/tests/test_try.py @@ -344,6 +344,34 @@ class TryTest(CSTNodeTest): ), "code": "try: pass\nexcept foo()as bar: pass\n", }, + # PEP758 - Multiple exceptions with no parentheses + { + "node": cst.Try( + cst.SimpleStatementSuite((cst.Pass(),)), + handlers=[ + cst.ExceptHandler( + cst.SimpleStatementSuite((cst.Pass(),)), + type=cst.Tuple( + elements=[ + cst.Element( + value=cst.Name( + value="ValueError", + ), + ), + cst.Element( + value=cst.Name( + value="RuntimeError", + ), + ), + ], + lpar=[], + rpar=[], + ), + ) + ], + ), + "code": "try: pass\nexcept ValueError, RuntimeError: pass\n", + }, ) ) def test_valid(self, **kwargs: Any) -> None: @@ -576,6 +604,38 @@ class TryStarTest(CSTNodeTest): "parser": native_parse_statement, "expected_position": CodeRange((1, 0), (5, 13)), }, + # PEP758 - Multiple exceptions with no parentheses + { + "node": cst.TryStar( + cst.SimpleStatementSuite((cst.Pass(),)), + handlers=[ + cst.ExceptStarHandler( + cst.SimpleStatementSuite((cst.Pass(),)), + type=cst.Tuple( + elements=[ + cst.Element( + value=cst.Name( + value="ValueError", + ), + comma=cst.Comma( + whitespace_after=cst.SimpleWhitespace(" ") + ), + ), + cst.Element( + value=cst.Name( + value="RuntimeError", + ), + ), + ], + lpar=[], + rpar=[], + ), + ) + ], + ), + "code": "try: pass\nexcept* ValueError, RuntimeError: pass\n", + "parser": native_parse_statement, + }, ) ) def test_valid(self, **kwargs: Any) -> None: diff --git a/native/libcst/src/parser/grammar.rs b/native/libcst/src/parser/grammar.rs index 76920d66..86823961 100644 --- a/native/libcst/src/parser/grammar.rs +++ b/native/libcst/src/parser/grammar.rs @@ -554,12 +554,21 @@ parser! { } // Except statement - rule except_block() -> ExceptHandler<'input, 'a> = kw:lit("except") e:expression() a:(k:lit("as") n:name() {(k, n)})? col:lit(":") b:block() { make_except(kw, Some(e), a, col, b) } + / kw:lit("except") e:expression() other:(c:comma() ex:expression() {(c, ex)})+ tc:(c:comma())? + col:lit(":") b:block() { + let tuple = Expression::Tuple(Box::new(Tuple { + elements: comma_separate(expr_to_element(e), other.into_iter().map(|(comma, expr)| (comma, expr_to_element(expr))).collect(), tc), + lpar: vec![], + rpar: vec![], + })); + + make_except(kw, Some(tuple), None, col, b) + } / kw:lit("except") col:lit(":") b:block() { make_except(kw, None, None, col, b) } @@ -569,6 +578,16 @@ parser! { a:(k:lit("as") n:name() {(k, n)})? col:lit(":") b:block() { make_except_star(kw, star, e, a, col, b) } + / kw:lit("except") star:lit("*") e:expression() other:(c:comma() ex:expression() {(c, ex)})+ tc:(c:comma())? + col:lit(":") b:block() { + let tuple = Expression::Tuple(Box::new(Tuple { + elements: comma_separate(expr_to_element(e), other.into_iter().map(|(comma, expr)| (comma, expr_to_element(expr))).collect(), tc), + lpar: vec![], + rpar: vec![], + })); + + make_except_star(kw, star, tuple, None, col, b) + } rule finally_block() -> Finally<'input, 'a> = kw:lit("finally") col:lit(":") b:block() { @@ -1550,22 +1569,22 @@ parser! { rule separated(el: rule, sep: rule) -> (El, Vec<(Sep, El)>) = e:el() rest:(s:sep() e:el() {(s, e)})* {(e, rest)} - rule traced(e: rule) -> T = - &(_* { + rule traced(e: rule) -> T = + &(_* { #[cfg(feature = "trace")] { println!("[PEG_INPUT_START]"); println!("{}", input); println!("[PEG_TRACE_START]"); } - }) - e:e()? {? + }) + e:e()? {? #[cfg(feature = "trace")] - println!("[PEG_TRACE_STOP]"); - e.ok_or("") - } + println!("[PEG_TRACE_STOP]"); + e.ok_or("") + } - } + } } #[allow(clippy::too_many_arguments)] diff --git a/native/libcst/tests/fixtures/terrible_tries.py b/native/libcst/tests/fixtures/terrible_tries.py index 91d6831e..eb5429cc 100644 --- a/native/libcst/tests/fixtures/terrible_tries.py +++ b/native/libcst/tests/fixtures/terrible_tries.py @@ -69,3 +69,25 @@ except foo: pass #9 + +try: + pass +except (foo, bar): + pass + +try: + pass +except foo, bar: + pass + +try: + pass +except (foo, bar), baz: + pass +else: + pass + +try: + pass +except* something, somethingelse: + pass \ No newline at end of file