Skip to content

Commit 3174e0b

Browse files
Fix ClassDef.fromlineno for Python < 3.8 (#1395)
Co-authored-by: Pierre Sassoulas <[email protected]>
1 parent a7d0a18 commit 3174e0b

File tree

3 files changed

+66
-8
lines changed

3 files changed

+66
-8
lines changed

ChangeLog

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ Release date: TBA
4444

4545
Closes #1085
4646

47+
* Fix ``ClassDef.fromlineno``. For Python < 3.8 the ``lineno`` attribute includes decorators.
48+
``fromlineno`` should return the line of the ``class`` statement itself.
49+
4750
What's New in astroid 2.9.4?
4851
============================
4952
Release date: TBA

astroid/nodes/scoped_nodes/scoped_nodes.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
from astroid import bases
5656
from astroid import decorators as decorators_mod
5757
from astroid import mixins, util
58-
from astroid.const import PY39_PLUS
58+
from astroid.const import PY38_PLUS, PY39_PLUS
5959
from astroid.context import (
6060
CallContext,
6161
InferenceContext,
@@ -1706,13 +1706,10 @@ def type(
17061706
return type_name
17071707

17081708
@decorators_mod.cachedproperty
1709-
def fromlineno(self):
1710-
"""The first line that this node appears on in the source code.
1711-
1712-
:type: int or None
1713-
"""
1709+
def fromlineno(self) -> Optional[int]:
1710+
"""The first line that this node appears on in the source code."""
17141711
# lineno is the line number of the first decorator, we want the def
1715-
# statement lineno
1712+
# statement lineno. Similar to 'ClassDef.fromlineno'
17161713
lineno = self.lineno
17171714
if self.decorators is not None:
17181715
lineno += sum(
@@ -2300,6 +2297,21 @@ def _newstyle_impl(self, context=None):
23002297
doc=("Whether this is a new style class or not\n\n" ":type: bool or None"),
23012298
)
23022299

2300+
@decorators_mod.cachedproperty
2301+
def fromlineno(self) -> Optional[int]:
2302+
"""The first line that this node appears on in the source code."""
2303+
if not PY38_PLUS:
2304+
# For Python < 3.8 the lineno is the line number of the first decorator.
2305+
# We want the class statement lineno. Similar to 'FunctionDef.fromlineno'
2306+
lineno = self.lineno
2307+
if self.decorators is not None:
2308+
lineno += sum(
2309+
node.tolineno - node.lineno + 1 for node in self.decorators.nodes
2310+
)
2311+
2312+
return lineno
2313+
return super().fromlineno
2314+
23032315
@decorators_mod.cachedproperty
23042316
def blockstart_tolineno(self):
23052317
"""The line on which the beginning of this block ends.

tests/unittest_builder.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import socket
3434
import sys
3535
import tempfile
36+
import textwrap
3637
import unittest
3738

3839
import pytest
@@ -136,12 +137,54 @@ def function(
136137
__name__,
137138
)
138139
function = astroid["function"]
139-
# XXX discussable, but that's what is expected by pylint right now
140+
# XXX discussable, but that's what is expected by pylint right now, similar to ClassDef
140141
self.assertEqual(function.fromlineno, 3)
141142
self.assertEqual(function.tolineno, 5)
142143
self.assertEqual(function.decorators.fromlineno, 2)
143144
self.assertEqual(function.decorators.tolineno, 2)
144145

146+
@staticmethod
147+
def test_decorated_class_lineno() -> None:
148+
code = textwrap.dedent(
149+
"""
150+
class A:
151+
...
152+
153+
@decorator
154+
class B:
155+
...
156+
157+
@deco1
158+
@deco2(
159+
var=42
160+
)
161+
class C:
162+
...
163+
"""
164+
)
165+
166+
ast_module: nodes.Module = builder.parse(code) # type: ignore[assignment]
167+
168+
a = ast_module.body[0]
169+
assert isinstance(a, nodes.ClassDef)
170+
assert a.fromlineno == 2
171+
assert a.tolineno == 3
172+
173+
b = ast_module.body[1]
174+
assert isinstance(b, nodes.ClassDef)
175+
assert b.fromlineno == 6
176+
assert b.tolineno == 7
177+
178+
c = ast_module.body[2]
179+
assert isinstance(c, nodes.ClassDef)
180+
if not PY38_PLUS:
181+
# Not perfect, but best we can do for Python 3.7
182+
# Can't detect closing bracket on new line.
183+
assert c.fromlineno == 12
184+
else:
185+
assert c.fromlineno == 13
186+
assert c.tolineno == 14
187+
145188
def test_class_lineno(self) -> None:
146189
stmts = self.astroid.body
147190
# on line 20:

0 commit comments

Comments
 (0)