Skip to content
Merged
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
4 changes: 4 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ Release date: TBA

Refs #2860

* Skip direct parent when determining the ``Decorator`` frame.

Refs pylint-dev/pylint#8425



What's New in astroid 4.0.3?
Expand Down
14 changes: 12 additions & 2 deletions astroid/nodes/node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
if TYPE_CHECKING:
from astroid import nodes
from astroid.nodes import LocalsDictNodeNG
from astroid.nodes.node_ng import FrameType


def _is_const(value) -> bool:
Expand Down Expand Up @@ -2239,13 +2240,22 @@ def scope(self) -> LocalsDictNodeNG:

:returns: The first parent scope node.
"""
# skip the function node to go directly to the upper level scope
# skip the function or class node to go directly to the upper level scope
if not self.parent:
raise ParentMissingError(target=self)
if not self.parent.parent:
raise ParentMissingError(target=self.parent)
return self.parent.parent.scope()

def frame(self) -> FrameType:
"""The first parent node defining a new frame."""
# skip the function or class node to go directly to the upper level frame
if not self.parent:
raise ParentMissingError(target=self)
if not self.parent.parent:
raise ParentMissingError(target=self.parent)
return self.parent.parent.frame()

def get_children(self):
yield from self.nodes

Expand Down Expand Up @@ -4927,7 +4937,7 @@ def postinit(self, target: NodeNG, value: NodeNG) -> None:
See astroid/protocols.py for actual implementation.
"""

def frame(self) -> nodes.FunctionDef | nodes.Module | nodes.ClassDef | nodes.Lambda:
def frame(self) -> FrameType:
"""The first parent frame node.

A frame node is a :class:`Module`, :class:`FunctionDef`,
Expand Down
4 changes: 3 additions & 1 deletion astroid/nodes/node_ng.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
if TYPE_CHECKING:
from astroid.nodes import _base_nodes

FrameType = nodes.FunctionDef | nodes.Module | nodes.ClassDef | nodes.Lambda


# Types for 'NodeNG.nodes_of_class()'
_NodesT = TypeVar("_NodesT", bound="NodeNG")
Expand Down Expand Up @@ -284,7 +286,7 @@ def statement(self) -> _base_nodes.Statement:
raise StatementMissing(target=self)
return self.parent.statement()

def frame(self) -> nodes.FunctionDef | nodes.Module | nodes.ClassDef | nodes.Lambda:
def frame(self) -> FrameType:
"""The first parent frame node.

A frame node is a :class:`Module`, :class:`FunctionDef`,
Expand Down
33 changes: 33 additions & 0 deletions tests/test_scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2909,6 +2909,39 @@ def method():
assert module.frame() == module
assert module.frame() == module

@staticmethod
def test_frame_node_for_decorators():
code = builder.extract_node(
"""
def deco(var):
def inner(arg):
...
return inner

@deco(
x := 1 #@
)
def func(): #@
...

@deco(
y := 2 #@
)
class A: #@
...
"""
)
name_expr_node1, func_node, name_expr_node2, class_node = code
module = func_node.root()
assert name_expr_node1.scope() == module
assert name_expr_node1.frame() == module
assert name_expr_node2.scope() == module
assert name_expr_node2.frame() == module
assert module.locals.get("x") == [name_expr_node1.target]
assert module.locals.get("y") == [name_expr_node2.target]
Comment on lines +2940 to +2941
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The frame is used for the scope lookup to determine where to assign the variables to. Without the changes in this PR, any variables get assigned to the function / class scope instead even though decorators are evaluated in the module scope.

--
A similar workaround might be required for function argument defaults. Though I haven't searched the issues tracker to see if anyone has complained yet.

assert "x" not in func_node.locals
assert "y" not in class_node.locals

@staticmethod
def test_non_frame_node():
"""Test if the frame of non frame nodes is set correctly."""
Expand Down