diff --git a/ChangeLog b/ChangeLog index 955fcb541..8750167f8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -183,6 +183,9 @@ Release date: TBA Refs #2154 +* Add new ``nodes.Try`` to better match Python AST. Replaces the ``TryExcept`` + and ``TryFinally`` nodes which have been removed. + * Publicize ``NodeNG.repr_name()`` to facilitate finding a node's nice name. Refs pylint-dev/pylint#8598 diff --git a/astroid/__init__.py b/astroid/__init__.py index 07e743a46..cd549188c 100644 --- a/astroid/__init__.py +++ b/astroid/__init__.py @@ -155,8 +155,7 @@ Slice, Starred, Subscript, - TryExcept, - TryFinally, + Try, TryStar, Tuple, UnaryOp, diff --git a/astroid/node_classes.py b/astroid/node_classes.py index 7f3614e46..f04b09881 100644 --- a/astroid/node_classes.py +++ b/astroid/node_classes.py @@ -70,8 +70,7 @@ Slice, Starred, Subscript, - TryExcept, - TryFinally, + Try, TryStar, Tuple, UnaryOp, diff --git a/astroid/nodes/__init__.py b/astroid/nodes/__init__.py index 44712f107..7c50217ee 100644 --- a/astroid/nodes/__init__.py +++ b/astroid/nodes/__init__.py @@ -76,8 +76,7 @@ Slice, Starred, Subscript, - TryExcept, - TryFinally, + Try, TryStar, Tuple, TypeAlias, @@ -188,8 +187,7 @@ Slice, Starred, Subscript, - TryExcept, - TryFinally, + Try, TryStar, Tuple, TypeAlias, @@ -283,8 +281,7 @@ "Slice", "Starred", "Subscript", - "TryExcept", - "TryFinally", + "Try", "TryStar", "Tuple", "TypeAlias", diff --git a/astroid/nodes/as_string.py b/astroid/nodes/as_string.py index 826c1c997..0b8ca0e4b 100644 --- a/astroid/nodes/as_string.py +++ b/astroid/nodes/as_string.py @@ -491,21 +491,17 @@ def visit_subscript(self, node) -> str: idxstr = idxstr[1:-1] return f"{self._precedence_parens(node, node.value)}[{idxstr}]" - def visit_tryexcept(self, node) -> str: - """return an astroid.TryExcept node as string""" + def visit_try(self, node) -> str: + """return an astroid.Try node as string""" trys = [f"try:\n{self._stmt_list(node.body)}"] for handler in node.handlers: trys.append(handler.accept(self)) if node.orelse: trys.append(f"else:\n{self._stmt_list(node.orelse)}") + if node.finalbody: + trys.append(f"finally:\n{self._stmt_list(node.finalbody)}") return "\n".join(trys) - def visit_tryfinally(self, node) -> str: - """return an astroid.TryFinally node as string""" - return "try:\n{}\nfinally:\n{}".format( - self._stmt_list(node.body), self._stmt_list(node.finalbody) - ) - def visit_trystar(self, node) -> str: """return an astroid.TryStar node as string""" trys = [f"try:\n{self._stmt_list(node.body)}"] diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py index d29d0cb50..80b30b79d 100644 --- a/astroid/nodes/node_classes.py +++ b/astroid/nodes/node_classes.py @@ -143,7 +143,7 @@ def are_exclusive(stmt1, stmt2, exceptions: list[str] | None = None) -> bool: previous = stmt2 for node in stmt2.node_ancestors(): if node in stmt1_parents: - # if the common parent is a If or TryExcept statement, look if + # if the common parent is a If or Try statement, look if # nodes are in exclusive branches if isinstance(node, If) and exceptions is None: c2attr, c2node = node.locate_child(previous) @@ -155,7 +155,7 @@ def are_exclusive(stmt1, stmt2, exceptions: list[str] | None = None) -> bool: if c1attr != c2attr: # different `If` branches (`If.body` and `If.orelse`) return True - elif isinstance(node, TryExcept): + elif isinstance(node, Try): c2attr, c2node = node.locate_child(previous) c1attr, c1node = node.locate_child(children[node]) if c1node is not c2node: @@ -3720,8 +3720,8 @@ def infer_lhs(self, context: InferenceContext | None = None, **kwargs: Any): return self._infer_subscript(context, **kwargs) -class TryExcept(_base_nodes.MultiLineWithElseBlockNode, _base_nodes.Statement): - """Class representing an :class:`ast.TryExcept` node. +class Try(_base_nodes.MultiLineWithElseBlockNode, _base_nodes.Statement): + """Class representing a :class:`ast.Try` node. >>> import astroid >>> node = astroid.extract_node(''' @@ -3729,88 +3729,24 @@ class TryExcept(_base_nodes.MultiLineWithElseBlockNode, _base_nodes.Statement): do_something() except Exception as error: print("Error!") + finally: + print("Cleanup!") ''') >>> node - + """ - _astroid_fields = ("body", "handlers", "orelse") - _multi_line_block_fields = ("body", "handlers", "orelse") - - body: list[NodeNG] - """The contents of the block to catch exceptions from.""" - - handlers: list[ExceptHandler] - """The exception handlers.""" - - orelse: list[NodeNG] - """The contents of the ``else`` block.""" - - def postinit( - self, - body: list[NodeNG], - handlers: list[ExceptHandler], - orelse: list[NodeNG], - ) -> None: - self.body = body - self.handlers = handlers - self.orelse = orelse - - def _infer_name(self, frame, name): - return name - - def block_range(self, lineno: int) -> tuple[int, int]: - """Get a range from the given line number to where this node ends. - - :param lineno: The line number to start the range at. - - :returns: The range of line numbers that this node belongs to, - starting at the given line number. - """ - last = None - 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 last is None: - last = exhandler.body[0].fromlineno - 1 - return self._elsed_block_range(lineno, self.orelse, last) - - def get_children(self): - yield from self.body - - yield from self.handlers or () - yield from self.orelse or () - - -class TryFinally(_base_nodes.MultiLineWithElseBlockNode, _base_nodes.Statement): - """Class representing an :class:`ast.TryFinally` node. - - >>> import astroid - >>> node = astroid.extract_node(''' - try: - do_something() - except Exception as error: - print("Error!") - finally: - print("Cleanup!") - ''') - >>> node - - """ - - _astroid_fields = ("body", "finalbody") - _multi_line_block_fields = ("body", "finalbody") + _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, - parent: NodeNG | None = None, *, - end_lineno: int | None = None, - end_col_offset: int | None = None, + lineno: int, + col_offset: int, + end_lineno: int, + end_col_offset: int, + parent: NodeNG, ) -> None: """ :param lineno: The line that this node appears on in the source code. @@ -3825,8 +3761,14 @@ 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] = [] - """The try-except that the finally is attached to.""" + 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.""" @@ -3841,40 +3783,58 @@ def __init__( def postinit( self, - body: list[NodeNG | TryExcept] | None = None, - finalbody: list[NodeNG] | None = None, + *, + body: list[NodeNG], + handlers: list[ExceptHandler], + orelse: list[NodeNG], + finalbody: list[NodeNG], ) -> None: """Do some setup after initialisation. - :param body: The try-except that the finally is attached to. + :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 is not None: - self.body = body - if finalbody is not None: - self.finalbody = finalbody - - def block_range(self, lineno: int) -> tuple[int, int]: - """Get a range from the given line number to where this node ends. + self.body = body + self.handlers = handlers + self.orelse = orelse + self.finalbody = finalbody - :param lineno: The line number to start the range at. + def _infer_name(self, frame, name): + return name - :returns: The range of line numbers that this node belongs to, - starting at the given line number. - """ - child = self.body[0] - # py2.5 try: except: finally: - if ( - isinstance(child, TryExcept) - and child.fromlineno == self.fromlineno - and child.tolineno >= lineno > self.fromlineno - ): - return child.block_range(lineno) - return self._elsed_block_range(lineno, self.finalbody) + 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 diff --git a/astroid/nodes/node_ng.py b/astroid/nodes/node_ng.py index 81eb8b571..7e92a7917 100644 --- a/astroid/nodes/node_ng.py +++ b/astroid/nodes/node_ng.py @@ -573,7 +573,7 @@ def _get_yield_nodes_skip_lambdas(self): yield from () def _infer_name(self, frame, name): - # overridden for ImportFrom, Import, Global, TryExcept, TryStar and Arguments + # overridden for ImportFrom, Import, Global, Try, TryStar and Arguments pass def _infer( diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index a8d547293..52bb39ffa 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -1531,7 +1531,7 @@ def _infer( # We also don't want to pass parent if the definition is within a Try node if isinstance( self.parent, - (node_classes.TryExcept, node_classes.TryFinally, node_classes.If), + (node_classes.Try, node_classes.If), ): property_already_in_parent_locals = True diff --git a/astroid/rebuilder.py b/astroid/rebuilder.py index 2b111a74c..e7781741c 100644 --- a/astroid/rebuilder.py +++ b/astroid/rebuilder.py @@ -167,9 +167,8 @@ def _reset_end_lineno(self, newnode: nodes.NodeNG) -> None: - ClassDef - For - FunctionDef - While - Call - If - - Decorators - TryExcept - - With - TryFinally - - Assign + - Decorators - Try + - With - Assign """ newnode.end_lineno = None newnode.end_col_offset = None @@ -423,9 +422,7 @@ def visit(self, node: ast.Starred, parent: NodeNG) -> nodes.Starred: ... @overload - def visit( - self, node: ast.Try, parent: NodeNG - ) -> nodes.TryExcept | nodes.TryFinally: + def visit(self, node: ast.Try, parent: NodeNG) -> nodes.Try: ... if sys.version_info >= (3, 11): @@ -1631,56 +1628,23 @@ def visit_starred(self, node: ast.Starred, parent: NodeNG) -> nodes.Starred: newnode.postinit(self.visit(node.value, newnode)) return newnode - def visit_tryexcept(self, node: ast.Try, parent: NodeNG) -> nodes.TryExcept: - """Visit a TryExcept node by returning a fresh instance of it.""" - # TryExcept excludes the 'finally' but that will be included in the - # end_lineno from 'node'. Therefore, we check all non 'finally' - # children to find the correct end_lineno and column. - end_lineno = node.end_lineno - end_col_offset = node.end_col_offset - all_children: list[ast.AST] = [*node.body, *node.handlers, *node.orelse] - for child in reversed(all_children): - end_lineno = child.end_lineno - end_col_offset = child.end_col_offset - break - newnode = nodes.TryExcept( + def visit_try(self, node: ast.Try, parent: NodeNG) -> nodes.Try: + """Visit a Try node by returning a fresh instance of it""" + newnode = nodes.Try( lineno=node.lineno, col_offset=node.col_offset, - end_lineno=end_lineno, - end_col_offset=end_col_offset, + end_lineno=node.end_lineno, + end_col_offset=node.end_col_offset, parent=parent, ) newnode.postinit( - [self.visit(child, newnode) for child in node.body], - [self.visit(child, newnode) for child in node.handlers], - [self.visit(child, newnode) for child in node.orelse], + body=[self.visit(child, newnode) for child in node.body], + handlers=[self.visit(child, newnode) for child in node.handlers], + orelse=[self.visit(child, newnode) for child in node.orelse], + finalbody=[self.visit(child, newnode) for child in node.finalbody], ) return newnode - def visit_try( - self, node: ast.Try, parent: NodeNG - ) -> nodes.TryExcept | nodes.TryFinally | None: - # python 3.3 introduce a new Try node replacing - # TryFinally/TryExcept nodes - if node.finalbody: - newnode = nodes.TryFinally( - lineno=node.lineno, - col_offset=node.col_offset, - end_lineno=node.end_lineno, - end_col_offset=node.end_col_offset, - parent=parent, - ) - body: list[NodeNG | nodes.TryExcept] - if node.handlers: - body = [self.visit_tryexcept(node, newnode)] - else: - body = [self.visit(child, newnode) for child in node.body] - newnode.postinit(body, [self.visit(n, newnode) for n in node.finalbody]) - return newnode - if node.handlers: - return self.visit_tryexcept(node, parent) - return None - def visit_trystar(self, node: ast.TryStar, parent: NodeNG) -> nodes.TryStar: newnode = nodes.TryStar( lineno=node.lineno, diff --git a/tests/test_group_exceptions.py b/tests/test_group_exceptions.py index ce1f142a5..2ee4143fc 100644 --- a/tests/test_group_exceptions.py +++ b/tests/test_group_exceptions.py @@ -10,7 +10,7 @@ ExceptHandler, For, Name, - TryExcept, + Try, Uninferable, bases, extract_node, @@ -35,10 +35,9 @@ def test_group_exceptions() -> None: print("Handling TypeError")""" ) ) - assert isinstance(node, TryExcept) + assert isinstance(node, Try) handler = node.handlers[0] - exception_group_block_range = (1, 4) - assert node.block_range(lineno=1) == exception_group_block_range + assert node.block_range(lineno=1) == (1, 9) assert node.block_range(lineno=2) == (2, 2) assert node.block_range(lineno=5) == (5, 9) assert isinstance(handler, ExceptHandler) @@ -47,7 +46,7 @@ def test_group_exceptions() -> None: assert len(children) == 3 exception_group, short_name, for_loop = children assert isinstance(exception_group, Name) - assert exception_group.block_range(1) == exception_group_block_range + assert exception_group.block_range(1) == (1, 4) assert isinstance(short_name, AssignName) assert isinstance(for_loop, For) diff --git a/tests/test_nodes.py b/tests/test_nodes.py index 41429fc5a..392544d71 100644 --- a/tests/test_nodes.py +++ b/tests/test_nodes.py @@ -368,6 +368,35 @@ def test_block_range(self) -> None: self.assertEqual(self.astroid.body[1].orelse[0].block_range(8), (8, 8)) +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] + 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: @@ -382,14 +411,15 @@ class TryExceptNodeTest(_NodeTest): def test_block_range(self) -> None: # XXX ensure expected values - self.assertEqual(self.astroid.body[0].block_range(1), (1, 8)) + self.assertEqual(self.astroid.body[0].block_range(1), (1, 9)) self.assertEqual(self.astroid.body[0].block_range(2), (2, 2)) - self.assertEqual(self.astroid.body[0].block_range(3), (3, 8)) + self.assertEqual(self.astroid.body[0].block_range(3), (3, 3)) self.assertEqual(self.astroid.body[0].block_range(4), (4, 4)) self.assertEqual(self.astroid.body[0].block_range(5), (5, 5)) self.assertEqual(self.astroid.body[0].block_range(6), (6, 6)) self.assertEqual(self.astroid.body[0].block_range(7), (7, 7)) self.assertEqual(self.astroid.body[0].block_range(8), (8, 8)) + self.assertEqual(self.astroid.body[0].block_range(9), (9, 9)) class TryFinallyNodeTest(_NodeTest): @@ -402,10 +432,11 @@ class TryFinallyNodeTest(_NodeTest): def test_block_range(self) -> None: # XXX ensure expected values - self.assertEqual(self.astroid.body[0].block_range(1), (1, 4)) + self.assertEqual(self.astroid.body[0].block_range(1), (1, 5)) self.assertEqual(self.astroid.body[0].block_range(2), (2, 2)) - self.assertEqual(self.astroid.body[0].block_range(3), (3, 4)) + self.assertEqual(self.astroid.body[0].block_range(3), (3, 3)) self.assertEqual(self.astroid.body[0].block_range(4), (4, 4)) + self.assertEqual(self.astroid.body[0].block_range(5), (5, 5)) class TryExceptFinallyNodeTest(_NodeTest): @@ -420,12 +451,13 @@ class TryExceptFinallyNodeTest(_NodeTest): def test_block_range(self) -> None: # XXX ensure expected values - self.assertEqual(self.astroid.body[0].block_range(1), (1, 6)) + self.assertEqual(self.astroid.body[0].block_range(1), (1, 7)) self.assertEqual(self.astroid.body[0].block_range(2), (2, 2)) - self.assertEqual(self.astroid.body[0].block_range(3), (3, 4)) + self.assertEqual(self.astroid.body[0].block_range(3), (3, 3)) self.assertEqual(self.astroid.body[0].block_range(4), (4, 4)) self.assertEqual(self.astroid.body[0].block_range(5), (5, 5)) self.assertEqual(self.astroid.body[0].block_range(6), (6, 6)) + self.assertEqual(self.astroid.body[0].block_range(7), (7, 7)) class ImportNodeTest(resources.SysPathSetup, unittest.TestCase): diff --git a/tests/test_nodes_lineno.py b/tests/test_nodes_lineno.py index c0af6628b..09623c3a7 100644 --- a/tests/test_nodes_lineno.py +++ b/tests/test_nodes_lineno.py @@ -763,7 +763,7 @@ def test_end_lineno_try() -> None: assert isinstance(ast_nodes, list) and len(ast_nodes) == 2 t1 = ast_nodes[0] - assert isinstance(t1, nodes.TryExcept) + assert isinstance(t1, nodes.Try) assert isinstance(t1.body[0], nodes.Pass) assert isinstance(t1.orelse[0], nodes.Pass) assert (t1.lineno, t1.col_offset) == (1, 0) @@ -789,13 +789,12 @@ def test_end_lineno_try() -> None: assert (t2.body[0].end_lineno, t2.body[0].end_col_offset) == (4, 8) t3 = ast_nodes[1] - assert isinstance(t3, nodes.TryFinally) - assert isinstance(t3.body[0], nodes.TryExcept) + assert isinstance(t3, nodes.Try) assert isinstance(t3.finalbody[0], nodes.Pass) assert (t3.lineno, t3.col_offset) == (10, 0) assert (t3.end_lineno, t3.end_col_offset) == (17, 8) - assert (t3.body[0].lineno, t3.body[0].col_offset) == (10, 0) - assert (t3.body[0].end_lineno, t3.body[0].end_col_offset) == (15, 8) + assert (t3.body[0].lineno, t3.body[0].col_offset) == (11, 4) + assert (t3.body[0].end_lineno, t3.body[0].end_col_offset) == (11, 8) assert (t3.finalbody[0].lineno, t3.finalbody[0].col_offset) == (17, 4) assert (t3.finalbody[0].end_lineno, t3.finalbody[0].end_col_offset) == (17, 8)