Make `else` its own scope and standardize behaviour of `block_range`
diff --git a/ChangeLog b/ChangeLog
index ec13d64..c9af822 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -7,6 +7,11 @@
============================
Release date: TBA
+* Changed `block_range` to consider `else` its own block, allowing `pylint` to apply
+ disables to just the block.
+
+ References pylint-dev/pylint#872
+
What's New in astroid 4.1.2?
============================
diff --git a/astroid/nodes/_base_nodes.py b/astroid/nodes/_base_nodes.py
index df452cb..a3cf912 100644
--- a/astroid/nodes/_base_nodes.py
+++ b/astroid/nodes/_base_nodes.py
@@ -237,22 +237,51 @@
class MultiLineWithElseBlockNode(MultiLineBlockNode):
"""Base node for multi-line blocks that can have else statements."""
+ body: list[NodeNG]
+ """The contents of the block."""
+
+ orelse: list[NodeNG]
+ """The contents of the ``else`` block."""
+
@cached_property
def blockstart_tolineno(self):
return self.lineno
+ 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.
+ """
+ if lineno < self.fromlineno:
+ return lineno, self.tolineno
+ if lineno == self.body[0].fromlineno:
+ return lineno, lineno
+ if lineno <= self.body[-1].tolineno:
+ return lineno, self.body[-1].tolineno
+ return self._elsed_block_range(lineno, self.orelse, self.body[0].fromlineno - 1)
+
def _elsed_block_range(
self, lineno: int, orelse: list[nodes.NodeNG], last: int | None = None
) -> tuple[int, int]:
"""Handle block line numbers range for try/finally, for, if and while
statements.
"""
- if lineno == self.fromlineno:
+ # If at the end of the node, return same line
+ if lineno == self.tolineno:
return lineno, lineno
if orelse:
- if lineno >= orelse[0].fromlineno:
+ # If the lineno is beyond the body of the node we check the orelse
+ if lineno >= self.body[-1].tolineno + 1:
+ # If the orelse has a scope of its own we determine the block range there
+ if isinstance(orelse[0], MultiLineWithElseBlockNode):
+ return orelse[0]._elsed_block_range(lineno, orelse[0].orelse)
+ # Return last line of orelse
return lineno, orelse[-1].tolineno
- return lineno, orelse[0].fromlineno - 1
+ # If the lineno is within the body we take the last line of the body
+ return lineno, self.body[-1].tolineno
return lineno, last or self.tolineno
diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py
index af2131c..144f2f8 100644
--- a/astroid/nodes/node_classes.py
+++ b/astroid/nodes/node_classes.py
@@ -3061,20 +3061,6 @@
"""
return self.test.tolineno
- 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.
- """
- if lineno == self.body[0].fromlineno:
- return lineno, lineno
- if lineno <= self.body[-1].tolineno:
- return lineno, self.body[-1].tolineno
- return self._elsed_block_range(lineno, self.orelse, self.body[0].fromlineno - 1)
-
def get_children(self):
yield self.test
@@ -3915,27 +3901,20 @@
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
+ return lineno, exhandler.tolineno
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
+ return lineno, self.finalbody[0].tolineno
if self.finalbody[0].fromlineno <= lineno <= self.finalbody[-1].tolineno:
return lineno, self.finalbody[-1].tolineno
- return lineno, self.tolineno
+
+ # If not within any of the ExceptHandlers or `finally` body, fall back to regular
+ # handling of block_range for nodes with a potential `else` statement.
+ return super().block_range(lineno)
def get_children(self):
yield from self.body
@@ -4014,36 +3993,29 @@
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
+ def block_range(self, lineno: int) -> tuple[int, int]:
+ """Get a range from a given line number to where this node ends."""
+ for exhandler in self.handlers:
+ if exhandler.type and lineno == exhandler.type.fromlineno:
+ return lineno, exhandler.tolineno
+ if exhandler.body[0].fromlineno <= lineno <= exhandler.body[-1].tolineno:
+ return lineno, exhandler.body[-1].tolineno
+ if self.finalbody:
+ if self.finalbody[0].fromlineno - 1 == lineno:
+ return lineno, self.finalbody[0].tolineno
+ if self.finalbody[0].fromlineno <= lineno <= self.finalbody[-1].tolineno:
+ return lineno, self.finalbody[-1].tolineno
+
+ # If not within any of the ExceptHandlers or `finally` body, fall back to regular
+ # handling of block_range for nodes with a potential `else` statement.
+ return super().block_range(lineno)
+
class Tuple(BaseContainer):
"""Class representing an :class:`ast.Tuple` node.
@@ -4472,16 +4444,6 @@
"""
return self.test.tolineno
- 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.
- """
- return self._elsed_block_range(lineno, self.orelse)
-
def get_children(self):
yield self.test
diff --git a/tests/test_group_exceptions.py b/tests/test_group_exceptions.py
index 9680664..2ced9ae 100644
--- a/tests/test_group_exceptions.py
+++ b/tests/test_group_exceptions.py
@@ -41,7 +41,7 @@
assert isinstance(node, nodes.Try)
handler = node.handlers[0]
assert node.block_range(lineno=1) == (1, 9)
- assert node.block_range(lineno=2) == (2, 2)
+ assert node.block_range(lineno=2) == (2, 3)
assert node.block_range(lineno=5) == (5, 9)
assert isinstance(handler, nodes.ExceptHandler)
assert handler.type.name == "ExceptionGroup"
@@ -72,15 +72,15 @@
assert node.as_string() == code.replace('"', "'").strip()
assert isinstance(node.body[0], nodes.Raise)
assert node.block_range(1) == (1, 11)
- assert node.block_range(2) == (2, 2)
+ assert node.block_range(2) == (2, 3)
assert node.block_range(3) == (3, 3)
- assert node.block_range(4) == (4, 4)
+ assert node.block_range(4) == (4, 5)
assert node.block_range(5) == (5, 5)
- assert node.block_range(6) == (6, 6)
+ assert node.block_range(6) == (6, 7)
assert node.block_range(7) == (7, 7)
- assert node.block_range(8) == (8, 8)
+ assert node.block_range(8) == (8, 9)
assert node.block_range(9) == (9, 9)
- assert node.block_range(10) == (10, 10)
+ assert node.block_range(10) == (10, 11)
assert node.block_range(11) == (11, 11)
assert node.handlers
handler = node.handlers[0]
diff --git a/tests/test_nodes.py b/tests/test_nodes.py
index 4bba86b..ff2ecea 100644
--- a/tests/test_nodes.py
+++ b/tests/test_nodes.py
@@ -427,11 +427,22 @@
pass
else:
raise
+
+ if 1:
+ print()
+ elif (
+ 2
+ and 3
+ ):
+ print()
+ else:
+ # This is using else in a comment
+ raise
"""
def test_if_elif_else_node(self) -> None:
"""Test transformation for If node."""
- self.assertEqual(len(self.astroid.body), 4)
+ self.assertEqual(len(self.astroid.body), 5)
for stmt in self.astroid.body:
self.assertIsInstance(stmt, nodes.If)
self.assertFalse(self.astroid.body[0].orelse) # simple If
@@ -440,13 +451,50 @@
self.assertIsInstance(self.astroid.body[3].orelse[0].orelse[0], nodes.If)
def test_block_range(self) -> None:
- # XXX ensure expected values
- self.assertEqual(self.astroid.block_range(1), (0, 22))
- self.assertEqual(self.astroid.block_range(10), (0, 22)) # XXX (10, 22) ?
+ """Test block_range of various scope constructs"""
+ # Module
+ self.assertEqual(self.astroid.block_range(1), (0, 33))
+ # NOTE: Module does not consider the lineno argument. It would be more consistent to make
+ # this return (10, 33) but without a use case it seems better to not change behaviour.
+ self.assertEqual(self.astroid.block_range(10), (0, 33))
+
+ # if
+ self.assertEqual(self.astroid.body[0].block_range(2), (2, 3))
+ self.assertEqual(self.astroid.body[0].block_range(3), (3, 3))
+
+ # if ... else
self.assertEqual(self.astroid.body[1].block_range(5), (5, 6))
self.assertEqual(self.astroid.body[1].block_range(6), (6, 6))
- self.assertEqual(self.astroid.body[1].orelse[0].block_range(7), (7, 8))
- self.assertEqual(self.astroid.body[1].orelse[0].block_range(8), (8, 8))
+ self.assertEqual(self.astroid.body[1].block_range(7), (7, 8))
+ self.assertEqual(self.astroid.body[1].block_range(8), (8, 8))
+
+ # if ... elif
+ self.assertEqual(self.astroid.body[2].block_range(10), (10, 11))
+ self.assertEqual(self.astroid.body[2].block_range(11), (11, 11))
+ self.assertEqual(self.astroid.body[2].block_range(12), (12, 13))
+ self.assertEqual(self.astroid.body[2].block_range(13), (13, 13))
+
+ # if ... elif ... elif ... else
+ self.assertEqual(self.astroid.body[3].block_range(15), (15, 16))
+ self.assertEqual(self.astroid.body[3].block_range(16), (16, 16))
+ self.assertEqual(self.astroid.body[3].block_range(17), (17, 18))
+ self.assertEqual(self.astroid.body[3].block_range(18), (18, 18))
+ self.assertEqual(self.astroid.body[3].block_range(19), (19, 20))
+ self.assertEqual(self.astroid.body[3].block_range(20), (20, 20))
+ self.assertEqual(self.astroid.body[3].block_range(21), (21, 22))
+ self.assertEqual(self.astroid.body[3].block_range(22), (22, 22))
+
+ # if ... elif ... else
+ self.assertEqual(self.astroid.body[4].block_range(24), (24, 25))
+ self.assertEqual(self.astroid.body[4].block_range(25), (25, 25))
+ self.assertEqual(self.astroid.body[4].block_range(26), (26, 30))
+ self.assertEqual(self.astroid.body[4].block_range(27), (27, 30))
+ self.assertEqual(self.astroid.body[4].block_range(28), (28, 30))
+ self.assertEqual(self.astroid.body[4].block_range(29), (29, 30))
+ self.assertEqual(self.astroid.body[4].block_range(30), (30, 30))
+ self.assertEqual(self.astroid.body[4].block_range(31), (31, 33))
+ self.assertEqual(self.astroid.body[4].block_range(32), (32, 33))
+ self.assertEqual(self.astroid.body[4].block_range(33), (33, 33))
class TryNodeTest(_NodeTest):
@@ -466,81 +514,18 @@
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(2) == (2, 3)
assert try_node.block_range(3) == (3, 3)
- assert try_node.block_range(4) == (4, 4)
+ assert try_node.block_range(4) == (4, 5)
assert try_node.block_range(5) == (5, 5)
- assert try_node.block_range(6) == (6, 6)
+ assert try_node.block_range(6) == (6, 7)
assert try_node.block_range(7) == (7, 7)
- assert try_node.block_range(8) == (8, 8)
+ assert try_node.block_range(8) == (8, 9)
assert try_node.block_range(9) == (9, 9)
- assert try_node.block_range(10) == (10, 10)
+ assert try_node.block_range(10) == (10, 11)
assert try_node.block_range(11) == (11, 11)
-class TryExceptNodeTest(_NodeTest):
- CODE = """
- try:
- print ('pouet')
- except IOError:
- pass
- except UnicodeError:
- print()
- else:
- print()
- """
-
- def test_block_range(self) -> None:
- # XXX ensure expected values
- 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, 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):
- CODE = """
- try:
- print ('pouet')
- finally:
- print ('pouet')
- """
-
- def test_block_range(self) -> None:
- # XXX ensure expected values
- 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, 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):
- CODE = """
- try:
- print('pouet')
- except Exception:
- print ('oops')
- finally:
- print ('pouet')
- """
-
- def test_block_range(self) -> None:
- # XXX ensure expected values
- 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, 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):
def setUp(self) -> None:
super().setUp()
diff --git a/tests/test_nodes_lineno.py b/tests/test_nodes_lineno.py
index bbc1e8c..37db476 100644
--- a/tests/test_nodes_lineno.py
+++ b/tests/test_nodes_lineno.py
@@ -878,6 +878,10 @@
assert (w1.body[0].end_lineno, w1.body[0].end_col_offset) == (2, 8)
assert (w1.orelse[0].lineno, w1.orelse[0].col_offset) == (4, 4)
assert (w1.orelse[0].end_lineno, w1.orelse[0].end_col_offset) == (4, 8)
+ assert w1.block_range(1) == (1, 2)
+ assert w1.block_range(2) == (2, 2)
+ assert w1.block_range(3) == (3, 4)
+ assert w1.block_range(4) == (4, 4)
@staticmethod
def test_end_lineno_string() -> None: