From 099d6339261bc6cc5c2989fbf6a47847196926f3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 13 Feb 2022 00:54:22 +0100 Subject: [PATCH 1/4] Fix ClassDef.fromlineno with Python 3.7 --- ChangeLog | 3 ++ astroid/const.py | 1 + astroid/nodes/scoped_nodes/scoped_nodes.py | 20 ++++++++- tests/unittest_builder.py | 48 +++++++++++++++++++++- 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 6baadce493..b281e39e0b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -42,6 +42,9 @@ Release date: TBA Closes #1085 +* Fix ``ClassDef.fromlineno``. Only in Python 3.7 the ``lineno`` attribute includes decorators. + ``fromlineno`` should return the line of the ``class`` statement itself. + What's New in astroid 2.9.4? ============================ Release date: TBA diff --git a/astroid/const.py b/astroid/const.py index 81384e346d..6a2d46af90 100644 --- a/astroid/const.py +++ b/astroid/const.py @@ -1,6 +1,7 @@ import enum import sys +PY37 = sys.version_info[:2] == (3, 7) PY38 = sys.version_info[:2] == (3, 8) PY37_PLUS = sys.version_info >= (3, 7) PY38_PLUS = sys.version_info >= (3, 8) diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index f8da9d2602..6919c79f5b 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -55,7 +55,7 @@ from astroid import bases from astroid import decorators as decorators_mod from astroid import mixins, util -from astroid.const import PY39_PLUS +from astroid.const import PY37, PY39_PLUS from astroid.context import ( CallContext, InferenceContext, @@ -2297,6 +2297,24 @@ def _newstyle_impl(self, context=None): doc=("Whether this is a new style class or not\n\n" ":type: bool or None"), ) + @decorators_mod.cachedproperty + def fromlineno(self): + """The first line that this node appears on in the source code. + + :type: int or None + """ + if PY37: + # Only in Python 3.7 is the lineno the line number of the first decorator. + # We want the class statement lineno. Similar to 'FunctionDef.fromlineno' + lineno = self.lineno + if self.decorators is not None: + lineno += sum( + node.tolineno - node.lineno + 1 for node in self.decorators.nodes + ) + + return lineno + return super().fromlineno + @decorators_mod.cachedproperty def blockstart_tolineno(self): """The line on which the beginning of this block ends. diff --git a/tests/unittest_builder.py b/tests/unittest_builder.py index eb7fbdc77f..586276c7b1 100644 --- a/tests/unittest_builder.py +++ b/tests/unittest_builder.py @@ -29,12 +29,13 @@ import os import socket import sys +import textwrap import unittest import pytest from astroid import Instance, builder, nodes, test_utils, util -from astroid.const import PY38_PLUS +from astroid.const import PY37, PY38_PLUS from astroid.exceptions import ( AstroidBuildingError, AstroidSyntaxError, @@ -132,12 +133,55 @@ def function( __name__, ) function = astroid["function"] - # XXX discussable, but that's what is expected by pylint right now + # XXX discussable, but that's what is expected by pylint right now, similar to ClassDef self.assertEqual(function.fromlineno, 3) self.assertEqual(function.tolineno, 5) self.assertEqual(function.decorators.fromlineno, 2) self.assertEqual(function.decorators.tolineno, 2) + @staticmethod + def test_decorated_class_lineno() -> None: + code = textwrap.dedent( + """ + class A: + ... + + @decorator + class B: + ... + + @deco1 + @deco2( + var=42 + ) + class C: + ... + """ + ) + + ast_module: nodes.Module = builder.parse(code) # type: ignore[assignment] + + # XXX discussable, but that's what is expected by pylint right now, similar to FunctionDef + a = ast_module.body[0] + assert isinstance(a, nodes.ClassDef) + assert a.fromlineno == 2 + assert a.tolineno == 3 + + b = ast_module.body[1] + assert isinstance(b, nodes.ClassDef) + assert b.fromlineno == 6 + assert b.tolineno == 7 + + c = ast_module.body[2] + assert isinstance(c, nodes.ClassDef) + if PY37: + # Not perfect, but best we can do for Python 3.7 + # Can't detect closing bracket on new line. + assert c.fromlineno == 12 + else: + assert c.fromlineno == 13 + assert c.tolineno == 14 + def test_class_lineno(self) -> None: stmts = self.astroid.body # on line 20: From 949fda154db277022d6114824effe00410a9adaa Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 13 Feb 2022 01:43:44 +0100 Subject: [PATCH 2/4] Fix Python 3.6 --- ChangeLog | 2 +- astroid/const.py | 1 - astroid/nodes/scoped_nodes/scoped_nodes.py | 6 +++--- tests/unittest_builder.py | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/ChangeLog b/ChangeLog index b281e39e0b..9aeebd438c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -42,7 +42,7 @@ Release date: TBA Closes #1085 -* Fix ``ClassDef.fromlineno``. Only in Python 3.7 the ``lineno`` attribute includes decorators. +* Fix ``ClassDef.fromlineno``. For Python < 3.8 the ``lineno`` attribute includes decorators. ``fromlineno`` should return the line of the ``class`` statement itself. What's New in astroid 2.9.4? diff --git a/astroid/const.py b/astroid/const.py index 6a2d46af90..81384e346d 100644 --- a/astroid/const.py +++ b/astroid/const.py @@ -1,7 +1,6 @@ import enum import sys -PY37 = sys.version_info[:2] == (3, 7) PY38 = sys.version_info[:2] == (3, 8) PY37_PLUS = sys.version_info >= (3, 7) PY38_PLUS = sys.version_info >= (3, 8) diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index 6919c79f5b..67356a25de 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -55,7 +55,7 @@ from astroid import bases from astroid import decorators as decorators_mod from astroid import mixins, util -from astroid.const import PY37, PY39_PLUS +from astroid.const import PY38_PLUS, PY39_PLUS from astroid.context import ( CallContext, InferenceContext, @@ -2303,8 +2303,8 @@ def fromlineno(self): :type: int or None """ - if PY37: - # Only in Python 3.7 is the lineno the line number of the first decorator. + if not PY38_PLUS: + # For Python < 3.8 the lineno is the line number of the first decorator. # We want the class statement lineno. Similar to 'FunctionDef.fromlineno' lineno = self.lineno if self.decorators is not None: diff --git a/tests/unittest_builder.py b/tests/unittest_builder.py index 586276c7b1..3c1b7138ac 100644 --- a/tests/unittest_builder.py +++ b/tests/unittest_builder.py @@ -35,7 +35,7 @@ import pytest from astroid import Instance, builder, nodes, test_utils, util -from astroid.const import PY37, PY38_PLUS +from astroid.const import PY38_PLUS from astroid.exceptions import ( AstroidBuildingError, AstroidSyntaxError, @@ -174,7 +174,7 @@ class C: c = ast_module.body[2] assert isinstance(c, nodes.ClassDef) - if PY37: + if not PY38_PLUS: # Not perfect, but best we can do for Python 3.7 # Can't detect closing bracket on new line. assert c.fromlineno == 12 From ef60922b0bf8502c937de0fdec417036515b80ea Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 13 Feb 2022 13:04:00 +0100 Subject: [PATCH 3/4] Code review --- astroid/nodes/scoped_nodes/scoped_nodes.py | 16 +++++----------- tests/unittest_builder.py | 1 - 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index 67356a25de..35d74c6acd 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -1706,13 +1706,10 @@ def type( return type_name @decorators_mod.cachedproperty - def fromlineno(self): - """The first line that this node appears on in the source code. - - :type: int or None - """ + def fromlineno(self) -> Optional[int]: + """The first line that this node appears on in the source code.""" # lineno is the line number of the first decorator, we want the def - # statement lineno + # statement lineno. Similar to 'ClassDef.fromlineno' lineno = self.lineno if self.decorators is not None: lineno += sum( @@ -2298,11 +2295,8 @@ def _newstyle_impl(self, context=None): ) @decorators_mod.cachedproperty - def fromlineno(self): - """The first line that this node appears on in the source code. - - :type: int or None - """ + def fromlineno(self) -> Optional[int]: + """The first line that this node appears on in the source code.""" if not PY38_PLUS: # For Python < 3.8 the lineno is the line number of the first decorator. # We want the class statement lineno. Similar to 'FunctionDef.fromlineno' diff --git a/tests/unittest_builder.py b/tests/unittest_builder.py index 3c1b7138ac..17d139b374 100644 --- a/tests/unittest_builder.py +++ b/tests/unittest_builder.py @@ -161,7 +161,6 @@ class C: ast_module: nodes.Module = builder.parse(code) # type: ignore[assignment] - # XXX discussable, but that's what is expected by pylint right now, similar to FunctionDef a = ast_module.body[0] assert isinstance(a, nodes.ClassDef) assert a.fromlineno == 2 From 822ccb706c25900663e349885010c7b3a0f9b143 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 19 Feb 2022 09:17:21 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/unittest_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unittest_builder.py b/tests/unittest_builder.py index 0ec26c927d..151abcbe02 100644 --- a/tests/unittest_builder.py +++ b/tests/unittest_builder.py @@ -32,8 +32,8 @@ import py_compile import socket import sys -import textwrap import tempfile +import textwrap import unittest import pytest