Skip to content

Commit

Permalink
implement PEP-654: except* (#571)
Browse files Browse the repository at this point in the history
  • Loading branch information
zsol authored Dec 29, 2021
1 parent c44ff05 commit 67db039
Show file tree
Hide file tree
Showing 10 changed files with 1,063 additions and 5 deletions.
4 changes: 4 additions & 0 deletions libcst/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@
Del,
Else,
ExceptHandler,
ExceptStarHandler,
Expr,
Finally,
For,
Expand All @@ -171,6 +172,7 @@
SimpleStatementLine,
SimpleStatementSuite,
Try,
TryStar,
While,
With,
WithItem,
Expand Down Expand Up @@ -367,6 +369,7 @@
"Del",
"Else",
"ExceptHandler",
"ExceptStarHandler",
"Expr",
"Finally",
"For",
Expand All @@ -385,6 +388,7 @@
"SimpleStatementLine",
"SimpleStatementSuite",
"Try",
"TryStar",
"While",
"With",
"WithItem",
Expand Down
149 changes: 148 additions & 1 deletion libcst/_nodes/statement.py
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,82 @@ def _codegen_impl(self, state: CodegenState) -> None:
self.body._codegen(state)


@add_slots
@dataclass(frozen=True)
class ExceptStarHandler(CSTNode):
"""
An ``except*`` clause that appears after a :class:`TryStar` statement.
"""

#: The body of the except.
body: BaseSuite

#: The type of exception this catches. Can be a tuple in some cases.
type: BaseExpression

#: The optional name that a caught exception is assigned to.
name: Optional[AsName] = None

#: Sequence of empty lines appearing before this compound statement line.
leading_lines: Sequence[EmptyLine] = ()

#: The whitespace between the ``except`` keyword and the star.
whitespace_after_except: SimpleWhitespace = SimpleWhitespace.field("")

#: The whitespace between the star and the type.
whitespace_after_star: SimpleWhitespace = SimpleWhitespace.field(" ")

#: The whitespace after any type or name node (whichever comes last) and
#: the colon.
whitespace_before_colon: SimpleWhitespace = SimpleWhitespace.field("")

def _validate(self) -> None:
name = self.name
if name is not None and not isinstance(name.name, Name):
raise CSTValidationError(
"Must use a Name node for AsName name inside ExceptHandler."
)

def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "ExceptStarHandler":
return ExceptStarHandler(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
whitespace_after_except=visit_required(
self, "whitespace_after_except", self.whitespace_after_except, visitor
),
whitespace_after_star=visit_required(
self, "whitespace_after_star", self.whitespace_after_star, visitor
),
type=visit_required(self, "type", self.type, visitor),
name=visit_optional(self, "name", self.name, visitor),
whitespace_before_colon=visit_required(
self, "whitespace_before_colon", self.whitespace_before_colon, visitor
),
body=visit_required(self, "body", self.body, visitor),
)

def _codegen_impl(self, state: CodegenState) -> None:
for ll in self.leading_lines:
ll._codegen(state)
state.add_indent_tokens()

with state.record_syntactic_position(self, end_node=self.body):
state.add_token("except")
self.whitespace_after_except._codegen(state)
state.add_token("*")
self.whitespace_after_star._codegen(state)
typenode = self.type
if typenode is not None:
typenode._codegen(state)
namenode = self.name
if namenode is not None:
namenode._codegen(state)
self.whitespace_before_colon._codegen(state)
state.add_token(":")
self.body._codegen(state)


@add_slots
@dataclass(frozen=True)
class Finally(CSTNode):
Expand Down Expand Up @@ -873,7 +949,9 @@ def _codegen_impl(self, state: CodegenState) -> None:
@dataclass(frozen=True)
class Try(BaseCompoundStatement):
"""
A ``try`` statement.
A regular ``try`` statement that cannot contain :class:`ExceptStar` blocks. For
``try`` statements that can contain :class:`ExceptStar` blocks, see
:class:`TryStar`.
"""

#: The suite that is wrapped with a try statement.
Expand Down Expand Up @@ -948,6 +1026,75 @@ def _codegen_impl(self, state: CodegenState) -> None:
finalbody._codegen(state)


@add_slots
@dataclass(frozen=True)
class TryStar(BaseCompoundStatement):
"""
A ``try`` statement with ``except*`` blocks.
"""

#: The suite that is wrapped with a try statement.
body: BaseSuite

#: A list of one or more exception handlers.
handlers: Sequence[ExceptStarHandler]

#: An optional else case.
orelse: Optional[Else] = None

#: An optional finally case.
finalbody: Optional[Finally] = None

#: Sequence of empty lines appearing before this compound statement line.
leading_lines: Sequence[EmptyLine] = ()

#: The whitespace that appears after the ``try`` keyword but before
#: the colon.
whitespace_before_colon: SimpleWhitespace = SimpleWhitespace.field("")

def _validate(self) -> None:
if len(self.handlers) == 0:
raise CSTValidationError(
"A TryStar statement must have at least one ExceptHandler"
)

def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "TryStar":
return TryStar(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
whitespace_before_colon=visit_required(
self, "whitespace_before_colon", self.whitespace_before_colon, visitor
),
body=visit_required(self, "body", self.body, visitor),
handlers=visit_sequence(self, "handlers", self.handlers, visitor),
orelse=visit_optional(self, "orelse", self.orelse, visitor),
finalbody=visit_optional(self, "finalbody", self.finalbody, visitor),
)

def _codegen_impl(self, state: CodegenState) -> None:
for ll in self.leading_lines:
ll._codegen(state)
state.add_indent_tokens()

end_node = self.handlers[-1]
orelse = self.orelse
end_node = end_node if orelse is None else orelse
finalbody = self.finalbody
end_node = end_node if finalbody is None else finalbody
with state.record_syntactic_position(self, end_node=end_node):
state.add_token("try")
self.whitespace_before_colon._codegen(state)
state.add_token(":")
self.body._codegen(state)
for handler in self.handlers:
handler._codegen(state)
if orelse is not None:
orelse._codegen(state)
if finalbody is not None:
finalbody._codegen(state)


@add_slots
@dataclass(frozen=True)
class ImportAlias(CSTNode):
Expand Down
162 changes: 162 additions & 0 deletions libcst/_nodes/tests/test_try.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
import libcst as cst
from libcst import parse_statement
from libcst._nodes.tests.base import CSTNodeTest, DummyIndentedBlock
from libcst._parser.entrypoints import is_native
from libcst.metadata import CodeRange
from libcst.testing.utils import data_provider

native_parse_statement = parse_statement if is_native() else None


class TryTest(CSTNodeTest):
@data_provider(
Expand Down Expand Up @@ -407,3 +410,162 @@ def test_valid(self, **kwargs: Any) -> None:
)
def test_invalid(self, **kwargs: Any) -> None:
self.assert_invalid(**kwargs)


class TryStarTest(CSTNodeTest):
@data_provider(
(
# Try/except with a class
{
"node": cst.TryStar(
cst.SimpleStatementSuite((cst.Pass(),)),
handlers=(
cst.ExceptStarHandler(
cst.SimpleStatementSuite((cst.Pass(),)),
type=cst.Name("Exception"),
),
),
),
"code": "try: pass\nexcept* Exception: pass\n",
"parser": native_parse_statement,
},
# Try/except with a named class
{
"node": cst.TryStar(
cst.SimpleStatementSuite((cst.Pass(),)),
handlers=(
cst.ExceptStarHandler(
cst.SimpleStatementSuite((cst.Pass(),)),
type=cst.Name("Exception"),
name=cst.AsName(cst.Name("exc")),
),
),
),
"code": "try: pass\nexcept* Exception as exc: pass\n",
"parser": native_parse_statement,
"expected_position": CodeRange((1, 0), (2, 30)),
},
# Try/except with multiple clauses
{
"node": cst.TryStar(
cst.SimpleStatementSuite((cst.Pass(),)),
handlers=(
cst.ExceptStarHandler(
cst.SimpleStatementSuite((cst.Pass(),)),
type=cst.Name("TypeError"),
name=cst.AsName(cst.Name("e")),
),
cst.ExceptStarHandler(
cst.SimpleStatementSuite((cst.Pass(),)),
type=cst.Name("KeyError"),
name=cst.AsName(cst.Name("e")),
),
),
),
"code": "try: pass\n"
+ "except* TypeError as e: pass\n"
+ "except* KeyError as e: pass\n",
"parser": native_parse_statement,
"expected_position": CodeRange((1, 0), (3, 27)),
},
# Simple try/except/finally block
{
"node": cst.TryStar(
cst.SimpleStatementSuite((cst.Pass(),)),
handlers=(
cst.ExceptStarHandler(
cst.SimpleStatementSuite((cst.Pass(),)),
type=cst.Name("KeyError"),
whitespace_after_except=cst.SimpleWhitespace(""),
),
),
finalbody=cst.Finally(cst.SimpleStatementSuite((cst.Pass(),))),
),
"code": "try: pass\nexcept* KeyError: pass\nfinally: pass\n",
"parser": native_parse_statement,
"expected_position": CodeRange((1, 0), (3, 13)),
},
# Simple try/except/else block
{
"node": cst.TryStar(
cst.SimpleStatementSuite((cst.Pass(),)),
handlers=(
cst.ExceptStarHandler(
cst.SimpleStatementSuite((cst.Pass(),)),
type=cst.Name("KeyError"),
whitespace_after_except=cst.SimpleWhitespace(""),
),
),
orelse=cst.Else(cst.SimpleStatementSuite((cst.Pass(),))),
),
"code": "try: pass\nexcept* KeyError: pass\nelse: pass\n",
"parser": native_parse_statement,
"expected_position": CodeRange((1, 0), (3, 10)),
},
# Verify whitespace in various locations
{
"node": cst.TryStar(
leading_lines=(cst.EmptyLine(comment=cst.Comment("# 1")),),
body=cst.SimpleStatementSuite((cst.Pass(),)),
handlers=(
cst.ExceptStarHandler(
leading_lines=(cst.EmptyLine(comment=cst.Comment("# 2")),),
type=cst.Name("TypeError"),
name=cst.AsName(
cst.Name("e"),
whitespace_before_as=cst.SimpleWhitespace(" "),
whitespace_after_as=cst.SimpleWhitespace(" "),
),
whitespace_after_except=cst.SimpleWhitespace(" "),
whitespace_after_star=cst.SimpleWhitespace(""),
whitespace_before_colon=cst.SimpleWhitespace(" "),
body=cst.SimpleStatementSuite((cst.Pass(),)),
),
),
orelse=cst.Else(
leading_lines=(cst.EmptyLine(comment=cst.Comment("# 3")),),
body=cst.SimpleStatementSuite((cst.Pass(),)),
whitespace_before_colon=cst.SimpleWhitespace(" "),
),
finalbody=cst.Finally(
leading_lines=(cst.EmptyLine(comment=cst.Comment("# 4")),),
body=cst.SimpleStatementSuite((cst.Pass(),)),
whitespace_before_colon=cst.SimpleWhitespace(" "),
),
whitespace_before_colon=cst.SimpleWhitespace(" "),
),
"code": "# 1\ntry : pass\n# 2\nexcept *TypeError as e : pass\n# 3\nelse : pass\n# 4\nfinally : pass\n",
"parser": native_parse_statement,
"expected_position": CodeRange((2, 0), (8, 14)),
},
# Now all together
{
"node": cst.TryStar(
cst.SimpleStatementSuite((cst.Pass(),)),
handlers=(
cst.ExceptStarHandler(
cst.SimpleStatementSuite((cst.Pass(),)),
type=cst.Name("TypeError"),
name=cst.AsName(cst.Name("e")),
),
cst.ExceptStarHandler(
cst.SimpleStatementSuite((cst.Pass(),)),
type=cst.Name("KeyError"),
name=cst.AsName(cst.Name("e")),
),
),
orelse=cst.Else(cst.SimpleStatementSuite((cst.Pass(),))),
finalbody=cst.Finally(cst.SimpleStatementSuite((cst.Pass(),))),
),
"code": "try: pass\n"
+ "except* TypeError as e: pass\n"
+ "except* KeyError as e: pass\n"
+ "else: pass\n"
+ "finally: pass\n",
"parser": native_parse_statement,
"expected_position": CodeRange((1, 0), (5, 13)),
},
)
)
def test_valid(self, **kwargs: Any) -> None:
self.validate_node(**kwargs)
Loading

0 comments on commit 67db039

Please sign in to comment.