Skip to content

Commit 47faf97

Browse files
Add 'TryStar' nodes from Python 3.11 #1516 (#2028)
Co-authored-by: Jacob Walls <[email protected]>
1 parent 857232e commit 47faf97

File tree

4 files changed

+229
-0
lines changed

4 files changed

+229
-0
lines changed

astroid/nodes/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
Subscript,
8585
TryExcept,
8686
TryFinally,
87+
TryStar,
8788
Tuple,
8889
UnaryOp,
8990
Unknown,

astroid/nodes/node_classes.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4216,6 +4216,107 @@ def get_children(self):
42164216
yield from self.finalbody
42174217

42184218

4219+
class TryStar(_base_nodes.MultiLineWithElseBlockNode, _base_nodes.Statement):
4220+
"""Class representing an :class:`ast.TryStar` node."""
4221+
4222+
_astroid_fields = ("body", "handlers", "orelse", "finalbody")
4223+
_multi_line_block_fields = ("body", "handlers", "orelse", "finalbody")
4224+
4225+
def __init__(
4226+
self,
4227+
*,
4228+
lineno: int | None = None,
4229+
col_offset: int | None = None,
4230+
end_lineno: int | None = None,
4231+
end_col_offset: int | None = None,
4232+
parent: NodeNG | None = None,
4233+
) -> None:
4234+
"""
4235+
:param lineno: The line that this node appears on in the source code.
4236+
:param col_offset: The column that this node appears on in the
4237+
source code.
4238+
:param parent: The parent node in the syntax tree.
4239+
:param end_lineno: The last line this node appears on in the source code.
4240+
:param end_col_offset: The end column this node appears on in the
4241+
source code. Note: This is after the last symbol.
4242+
"""
4243+
self.body: list[NodeNG] = []
4244+
"""The contents of the block to catch exceptions from."""
4245+
4246+
self.handlers: list[ExceptHandler] = []
4247+
"""The exception handlers."""
4248+
4249+
self.orelse: list[NodeNG] = []
4250+
"""The contents of the ``else`` block."""
4251+
4252+
self.finalbody: list[NodeNG] = []
4253+
"""The contents of the ``finally`` block."""
4254+
4255+
super().__init__(
4256+
lineno=lineno,
4257+
col_offset=col_offset,
4258+
end_lineno=end_lineno,
4259+
end_col_offset=end_col_offset,
4260+
parent=parent,
4261+
)
4262+
4263+
def postinit(
4264+
self,
4265+
*,
4266+
body: list[NodeNG] | None = None,
4267+
handlers: list[ExceptHandler] | None = None,
4268+
orelse: list[NodeNG] | None = None,
4269+
finalbody: list[NodeNG] | None = None,
4270+
) -> None:
4271+
"""Do some setup after initialisation.
4272+
:param body: The contents of the block to catch exceptions from.
4273+
:param handlers: The exception handlers.
4274+
:param orelse: The contents of the ``else`` block.
4275+
:param finalbody: The contents of the ``finally`` block.
4276+
"""
4277+
if body:
4278+
self.body = body
4279+
if handlers:
4280+
self.handlers = handlers
4281+
if orelse:
4282+
self.orelse = orelse
4283+
if finalbody:
4284+
self.finalbody = finalbody
4285+
4286+
def _infer_name(self, frame, name):
4287+
return name
4288+
4289+
def block_range(self, lineno: int) -> tuple[int, int]:
4290+
"""Get a range from a given line number to where this node ends."""
4291+
if lineno == self.fromlineno:
4292+
return lineno, lineno
4293+
if self.body and self.body[0].fromlineno <= lineno <= self.body[-1].tolineno:
4294+
# Inside try body - return from lineno till end of try body
4295+
return lineno, self.body[-1].tolineno
4296+
for exhandler in self.handlers:
4297+
if exhandler.type and lineno == exhandler.type.fromlineno:
4298+
return lineno, lineno
4299+
if exhandler.body[0].fromlineno <= lineno <= exhandler.body[-1].tolineno:
4300+
return lineno, exhandler.body[-1].tolineno
4301+
if self.orelse:
4302+
if self.orelse[0].fromlineno - 1 == lineno:
4303+
return lineno, lineno
4304+
if self.orelse[0].fromlineno <= lineno <= self.orelse[-1].tolineno:
4305+
return lineno, self.orelse[-1].tolineno
4306+
if self.finalbody:
4307+
if self.finalbody[0].fromlineno - 1 == lineno:
4308+
return lineno, lineno
4309+
if self.finalbody[0].fromlineno <= lineno <= self.finalbody[-1].tolineno:
4310+
return lineno, self.finalbody[-1].tolineno
4311+
return lineno, self.tolineno
4312+
4313+
def get_children(self):
4314+
yield from self.body
4315+
yield from self.handlers
4316+
yield from self.orelse
4317+
yield from self.finalbody
4318+
4319+
42194320
class Tuple(BaseContainer):
42204321
"""Class representing an :class:`ast.Tuple` node.
42214322

astroid/rebuilder.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1822,6 +1822,22 @@ def visit_try(
18221822
return self.visit_tryexcept(node, parent)
18231823
return None
18241824

1825+
def visit_trystar(self, node: ast.TryStar, parent: NodeNG) -> nodes.TryStar:
1826+
newnode = nodes.TryStar(
1827+
lineno=node.lineno,
1828+
col_offset=node.col_offset,
1829+
end_lineno=getattr(node, "end_lineno", None),
1830+
end_col_offset=getattr(node, "end_col_offset", None),
1831+
parent=parent,
1832+
)
1833+
newnode.postinit(
1834+
body=[self.visit(n, newnode) for n in node.body],
1835+
handlers=[self.visit(n, newnode) for n in node.handlers],
1836+
orelse=[self.visit(n, newnode) for n in node.orelse],
1837+
finalbody=[self.visit(n, newnode) for n in node.finalbody],
1838+
)
1839+
return newnode
1840+
18251841
def visit_tuple(self, node: ast.Tuple, parent: NodeNG) -> nodes.Tuple:
18261842
"""Visit a Tuple node by returning a fresh instance of it."""
18271843
context = self._get_context(node)

tests/test_group_exceptions.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
2+
# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
3+
# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
4+
import textwrap
5+
6+
import pytest
7+
8+
from astroid import (
9+
AssignName,
10+
ExceptHandler,
11+
For,
12+
Name,
13+
TryExcept,
14+
Uninferable,
15+
bases,
16+
extract_node,
17+
)
18+
from astroid.const import PY311_PLUS
19+
from astroid.context import InferenceContext
20+
from astroid.nodes import Expr, Raise, TryStar
21+
22+
23+
@pytest.mark.skipif(not PY311_PLUS, reason="Requires Python 3.11 or higher")
24+
def test_group_exceptions() -> None:
25+
node = extract_node(
26+
textwrap.dedent(
27+
"""
28+
try:
29+
raise ExceptionGroup("group", [ValueError(654)])
30+
except ExceptionGroup as eg:
31+
for err in eg.exceptions:
32+
if isinstance(err, ValueError):
33+
print("Handling ValueError")
34+
elif isinstance(err, TypeError):
35+
print("Handling TypeError")"""
36+
)
37+
)
38+
assert isinstance(node, TryExcept)
39+
handler = node.handlers[0]
40+
exception_group_block_range = (1, 4)
41+
assert node.block_range(lineno=1) == exception_group_block_range
42+
assert node.block_range(lineno=2) == (2, 2)
43+
assert node.block_range(lineno=5) == (5, 9)
44+
assert isinstance(handler, ExceptHandler)
45+
assert handler.type.name == "ExceptionGroup"
46+
children = list(handler.get_children())
47+
assert len(children) == 3
48+
exception_group, short_name, for_loop = children
49+
assert isinstance(exception_group, Name)
50+
assert exception_group.block_range(1) == exception_group_block_range
51+
assert isinstance(short_name, AssignName)
52+
assert isinstance(for_loop, For)
53+
54+
55+
@pytest.mark.skipif(not PY311_PLUS, reason="Requires Python 3.11 or higher")
56+
def test_star_exceptions() -> None:
57+
node = extract_node(
58+
textwrap.dedent(
59+
"""
60+
try:
61+
raise ExceptionGroup("group", [ValueError(654)])
62+
except* ValueError:
63+
print("Handling ValueError")
64+
except* TypeError:
65+
print("Handling TypeError")
66+
else:
67+
sys.exit(127)
68+
finally:
69+
sys.exit(0)"""
70+
)
71+
)
72+
assert isinstance(node, TryStar)
73+
assert isinstance(node.body[0], Raise)
74+
assert node.block_range(1) == (1, 11)
75+
assert node.block_range(2) == (2, 2)
76+
assert node.block_range(3) == (3, 3)
77+
assert node.block_range(4) == (4, 4)
78+
assert node.block_range(5) == (5, 5)
79+
assert node.block_range(6) == (6, 6)
80+
assert node.block_range(7) == (7, 7)
81+
assert node.block_range(8) == (8, 8)
82+
assert node.block_range(9) == (9, 9)
83+
assert node.block_range(10) == (10, 10)
84+
assert node.block_range(11) == (11, 11)
85+
assert node.handlers
86+
handler = node.handlers[0]
87+
assert isinstance(handler, ExceptHandler)
88+
assert handler.type.name == "ValueError"
89+
orelse = node.orelse[0]
90+
assert isinstance(orelse, Expr)
91+
assert orelse.value.args[0].value == 127
92+
final = node.finalbody[0]
93+
assert isinstance(final, Expr)
94+
assert final.value.args[0].value == 0
95+
96+
97+
@pytest.mark.skipif(not PY311_PLUS, reason="Requires Python 3.11 or higher")
98+
def test_star_exceptions_infer_name() -> None:
99+
trystar = extract_node(
100+
"""
101+
try:
102+
1/0
103+
except* ValueError:
104+
pass"""
105+
)
106+
name = "arbitraryName"
107+
context = InferenceContext()
108+
context.lookupname = name
109+
stmts = bases._infer_stmts([trystar], context)
110+
assert list(stmts) == [Uninferable]
111+
assert context.lookupname == name

0 commit comments

Comments
 (0)