Skip to content
Open
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
13 changes: 12 additions & 1 deletion astroid/nodes/node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
from astroid import nodes
from astroid.nodes import LocalsDictNodeNG

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


def _is_const(value) -> bool:
return isinstance(value, tuple(CONST_CLS))
Expand Down Expand Up @@ -2239,13 +2241,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
30 changes: 30 additions & 0 deletions tests/test_scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2909,6 +2909,36 @@ def method():
assert module.frame() == module
assert module.frame() == module

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

@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 +2937 to +2938
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