diff --git a/libcst/_nodes/statement.py b/libcst/_nodes/statement.py index 9a82c41be..b0ac35d3a 100644 --- a/libcst/_nodes/statement.py +++ b/libcst/_nodes/statement.py @@ -785,10 +785,15 @@ def _validate(self) -> None: raise CSTValidationError( "Must use a Name node for AsName name inside ExceptHandler." ) - if self.type is not None and self.whitespace_after_except.empty: - raise CSTValidationError( - "Must have at least one space after except when ExceptHandler has a type." - ) + type_ = self.type + if type_ is not None and self.whitespace_after_except.empty: + # Space is only required when the first char in `type` could start + # an identifier. In the most common cases, we want to allow + # grouping or tuple parens. + if isinstance(type_, Name) and not type_.lpar: + raise CSTValidationError( + "Must have at least one space after except when ExceptHandler has a type." + ) def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "ExceptHandler": return ExceptHandler( diff --git a/libcst/_nodes/tests/test_try.py b/libcst/_nodes/tests/test_try.py index 91aa18fc7..5e0d0f97e 100644 --- a/libcst/_nodes/tests/test_try.py +++ b/libcst/_nodes/tests/test_try.py @@ -281,6 +281,50 @@ class TryTest(CSTNodeTest): + " pass\n", "parser": None, }, + # No space when using grouping parens + { + "node": cst.Try( + cst.SimpleStatementSuite((cst.Pass(),)), + handlers=( + cst.ExceptHandler( + cst.SimpleStatementSuite((cst.Pass(),)), + whitespace_after_except=cst.SimpleWhitespace(""), + type=cst.Name( + "Exception", + lpar=(cst.LeftParen(),), + rpar=(cst.RightParen(),), + ), + ), + ), + ), + "code": "try: pass\nexcept(Exception): pass\n", + "parser": parse_statement, + }, + # No space when using tuple + { + "node": cst.Try( + cst.SimpleStatementSuite((cst.Pass(),)), + handlers=( + cst.ExceptHandler( + cst.SimpleStatementSuite((cst.Pass(),)), + whitespace_after_except=cst.SimpleWhitespace(""), + type=cst.Tuple( + [ + cst.Element( + cst.Name("IOError"), + comma=cst.Comma( + whitespace_after=cst.SimpleWhitespace(" ") + ), + ), + cst.Element(cst.Name("ImportError")), + ] + ), + ), + ), + ), + "code": "try: pass\nexcept(IOError, ImportError): pass\n", + "parser": parse_statement, + }, ) ) def test_valid(self, **kwargs: Any) -> None: diff --git a/libcst/_parser/conversions/params.py b/libcst/_parser/conversions/params.py index 18b18f4e6..1153790e5 100644 --- a/libcst/_parser/conversions/params.py +++ b/libcst/_parser/conversions/params.py @@ -90,7 +90,9 @@ ), version="<=3.5", ) -def convert_argslist(config: ParserConfig, children: Sequence[Any]) -> Any: +def convert_argslist( # noqa: C901 + config: ParserConfig, children: Sequence[Any] +) -> Any: posonly_params: List[Param] = [] posonly_ind: Union[ParamSlash, MaybeSentinel] = MaybeSentinel.DEFAULT params: List[Param] = []