Skip to content

Add Try node #1389

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions astroid/node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
Slice,
Starred,
Subscript,
Try,
TryExcept,
TryFinally,
Tuple,
Expand Down
2 changes: 2 additions & 0 deletions astroid/nodes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
Slice,
Starred,
Subscript,
Try,
TryExcept,
TryFinally,
Tuple,
Expand Down Expand Up @@ -194,6 +195,7 @@
Slice,
Starred,
Subscript,
Try,
TryExcept,
TryFinally,
Tuple,
Expand Down
132 changes: 131 additions & 1 deletion astroid/nodes/node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3991,6 +3991,128 @@ def get_children(self):
yield self.slice


class Try(_base_nodes.MultiLineWithElseBlockNode, _base_nodes.Statement):
"""Class representing a :class:`ast.Try` node.

Replaces the `TryExcept` and `TryFinally` nodes.

>>> import astroid
>>> node = astroid.extract_node('''
try:
do_something()
except Exception as error:
print("Error!")
''')
>>> node
<Try l.2 at 0x7f23b2e41d68>
"""

_astroid_fields = ("body", "handlers", "orelse", "finalbody")
_multi_line_block_fields = ("body", "handlers", "orelse", "finalbody")

def __init__(
self,
*,
lineno: int | None = None,
col_offset: int | None = None,
end_lineno: int | None = None,
end_col_offset: int | None = None,
parent: NodeNG | None = None,
) -> None:
"""
:param lineno: The line that this node appears on in the source code.

:param col_offset: The column that this node appears on in the
source code.

:param parent: The parent node in the syntax tree.

:param end_lineno: The last line this node appears on in the source code.

:param end_col_offset: The end column this node appears on in the
source code. Note: This is after the last symbol.
"""
self.body: list[NodeNG] = []
"""The contents of the block to catch exceptions from."""

self.handlers: list[ExceptHandler] = []
"""The exception handlers."""

self.orelse: list[NodeNG] = []
"""The contents of the ``else`` block."""

self.finalbody: list[NodeNG] = []
"""The contents of the ``finally`` block."""

super().__init__(
lineno=lineno,
col_offset=col_offset,
end_lineno=end_lineno,
end_col_offset=end_col_offset,
parent=parent,
)

def postinit(
self,
*,
body: list[NodeNG] | None = None,
handlers: list[ExceptHandler] | None = None,
orelse: list[NodeNG] | None = None,
finalbody: list[NodeNG] | None = None,
) -> None:
"""Do some setup after initialisation.

:param body: The contents of the block to catch exceptions from.

:param handlers: The exception handlers.

:param orelse: The contents of the ``else`` block.

:param finalbody: The contents of the ``finally`` block.
"""
if body:
self.body = body
if handlers:
self.handlers = handlers
if orelse:
self.orelse = orelse
if finalbody:
self.finalbody = finalbody

def _infer_name(self, frame, name):
return name

def block_range(self, lineno: int) -> tuple[int, int]:
"""Get a range from a given line number to where this node ends."""
if lineno == self.fromlineno:
return lineno, lineno
if self.body and self.body[0].fromlineno <= lineno <= self.body[-1].tolineno:
# Inside try body - return from lineno till end of try body
return lineno, self.body[-1].tolineno
for exhandler in self.handlers:
if exhandler.type and lineno == exhandler.type.fromlineno:
return lineno, lineno
if exhandler.body[0].fromlineno <= lineno <= exhandler.body[-1].tolineno:
return lineno, exhandler.body[-1].tolineno
if self.orelse:
if self.orelse[0].fromlineno - 1 == lineno:
return lineno, lineno
if self.orelse[0].fromlineno <= lineno <= self.orelse[-1].tolineno:
return lineno, self.orelse[-1].tolineno
if self.finalbody:
if self.finalbody[0].fromlineno - 1 == lineno:
return lineno, lineno
if self.finalbody[0].fromlineno <= lineno <= self.finalbody[-1].tolineno:
return lineno, self.finalbody[-1].tolineno
return lineno, self.tolineno

def get_children(self):
yield from self.body
yield from self.handlers
yield from self.orelse
yield from self.finalbody


class TryExcept(_base_nodes.MultiLineWithElseBlockNode, _base_nodes.Statement):
"""Class representing an :class:`ast.TryExcept` node.

Expand All @@ -4008,6 +4130,11 @@ class TryExcept(_base_nodes.MultiLineWithElseBlockNode, _base_nodes.Statement):
_astroid_fields = ("body", "handlers", "orelse")
_multi_line_block_fields = ("body", "handlers", "orelse")

try_node: nodes.Try
"""Map to new ``Try`` node. Only set for the parent Try node, i.e. not for
nested ``TryExcept`` inside the body of ``TryFinally``.
"""

def __init__(
self,
lineno: int | None = None,
Expand Down Expand Up @@ -4117,6 +4244,9 @@ class TryFinally(_base_nodes.MultiLineWithElseBlockNode, _base_nodes.Statement):
_astroid_fields = ("body", "finalbody")
_multi_line_block_fields = ("body", "finalbody")

try_node: nodes.Try
"""Map to new ``Try`` node."""

def __init__(
self,
lineno: int | None = None,
Expand All @@ -4139,7 +4269,7 @@ def __init__(
:param end_col_offset: The end column this node appears on in the
source code. Note: This is after the last symbol.
"""
self.body: list[NodeNG | TryExcept] = []
self.body: list[TryExcept] | list[NodeNG] = []
"""The try-except that the finally is attached to."""

self.finalbody: list[NodeNG] = []
Expand Down
2 changes: 1 addition & 1 deletion astroid/nodes/node_ng.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ def _get_yield_nodes_skip_lambdas(self):
yield from ()

def _infer_name(self, frame, name):
# overridden for ImportFrom, Import, Global, TryExcept and Arguments
# overridden for ImportFrom, Import, Global, Try, TryExcept and Arguments
pass

def _infer(
Expand Down
66 changes: 57 additions & 9 deletions astroid/rebuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,9 @@ def _reset_end_lineno(self, newnode: nodes.NodeNG) -> None:
- ClassDef - For
- FunctionDef - While
- Call - If
- Decorators - TryExcept
- With - TryFinally
- Assign
- Decorators - Try
- With - TryExcept
- Assign - TryFinally
"""
newnode.end_lineno = None
newnode.end_col_offset = None
Expand Down Expand Up @@ -1810,15 +1810,63 @@ def visit_try(
end_col_offset=getattr(node, "end_col_offset", None),
parent=parent,
)
body: list[NodeNG | nodes.TryExcept]
if node.handlers:
body = [self.visit_tryexcept(node, newnode)]
try_except = self.visit_tryexcept(node, newnode) if node.handlers else None
newnode.postinit(
body=[try_except]
if try_except
else [self.visit(child, newnode) for child in node.body],
finalbody=[self.visit(n, newnode) for n in node.finalbody],
)
try_node = nodes.Try(
lineno=node.lineno,
col_offset=node.col_offset,
# end_lineno and end_col_offset added in 3.8
end_lineno=getattr(node, "end_lineno", None),
end_col_offset=getattr(node, "end_col_offset", None),
parent=parent,
)
if IS_PYPY and PY38:
# Reset end_* attributes manually since try_node isn't part
# of the final tree yet.
try_node.end_lineno = None
try_node.end_col_offset = None
if try_except:
try_node.postinit(
body=[*try_except.body],
handlers=[*try_except.handlers],
orelse=[*try_except.orelse],
finalbody=[*newnode.finalbody],
)
else:
body = [self.visit(child, newnode) for child in node.body]
newnode.postinit(body, [self.visit(n, newnode) for n in node.finalbody])
try_node.postinit(
body=[*newnode.body],
finalbody=[*newnode.finalbody],
)
newnode.try_node = try_node
return newnode
if node.handlers:
return self.visit_tryexcept(node, parent)
# pylint: disable-next=redefined-variable-type
newnode = self.visit_tryexcept(node, parent)
try_node = nodes.Try(
lineno=node.lineno,
col_offset=node.col_offset,
# end_lineno and end_col_offset added in 3.8
end_lineno=getattr(node, "end_lineno", None),
end_col_offset=getattr(node, "end_col_offset", None),
parent=parent,
)
if IS_PYPY and PY38:
# Reset end_* attributes manually since try_node isn't part
# of the final tree yet.
try_node.end_lineno = None
try_node.end_col_offset = None
try_node.postinit(
body=[*newnode.body],
handlers=[*newnode.handlers],
orelse=[*newnode.orelse],
)
newnode.try_node = try_node
return newnode
return None

def visit_tuple(self, node: ast.Tuple, parent: NodeNG) -> nodes.Tuple:
Expand Down
29 changes: 29 additions & 0 deletions tests/unittest_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,35 @@ def test_if_typing_guard() -> None:
assert code[3].is_typing_guard() is False


class TryNodeTest(_NodeTest):
CODE = """
try: # L2
print("Hello")
except IOError:
pass
except UnicodeError:
pass
else:
print()
finally:
print()
"""

def test_block_range(self) -> None:
try_node = self.astroid.body[0].try_node
assert try_node.block_range(1) == (1, 11)
assert try_node.block_range(2) == (2, 2)
assert try_node.block_range(3) == (3, 3)
assert try_node.block_range(4) == (4, 4)
assert try_node.block_range(5) == (5, 5)
assert try_node.block_range(6) == (6, 6)
assert try_node.block_range(7) == (7, 7)
assert try_node.block_range(8) == (8, 8)
assert try_node.block_range(9) == (9, 9)
assert try_node.block_range(10) == (10, 10)
assert try_node.block_range(11) == (11, 11)


class TryExceptNodeTest(_NodeTest):
CODE = """
try:
Expand Down