diff options
author | Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com> | 2021-10-24 23:42:44 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-24 23:42:44 +0200 |
commit | b90714ecaa77c4346f877581ca61ba1bc0715798 (patch) | |
tree | 53fc8c850a92b9a8504c8f1d5a46533f28177de8 | |
parent | 1620fb33e852e3d36244244cd99ee2d91aedfc5a (diff) | |
download | astroid-b90714ecaa77c4346f877581ca61ba1bc0715798.tar.gz |
Change ``frame`` and ``scope`` of ``NamedExpr`` for certain parents (#1221)
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
-rw-r--r-- | ChangeLog | 4 | ||||
-rw-r--r-- | astroid/nodes/node_classes.py | 40 | ||||
-rw-r--r-- | tests/unittest_nodes.py | 111 |
3 files changed, 155 insertions, 0 deletions
@@ -12,6 +12,10 @@ What's New in astroid 2.8.4? ============================ Release date: TBA +* Fix the ``scope()`` and ``frame()`` methods of ``NamedExpr`` nodes. + When these nodes occur in ``Arguments``, ``Keyword`` or ``Comprehension`` nodes these + methods now correctly point to the outer-scope of the `` FunctionDef``, + ``ClassDef`` or ``Comprehension``. What's New in astroid 2.8.3? diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py index c27b7689..cb1d09c9 100644 --- a/astroid/nodes/node_classes.py +++ b/astroid/nodes/node_classes.py @@ -4225,6 +4225,46 @@ class NamedExpr(mixins.AssignTypeMixin, NodeNG): self.target = target self.value = value + def frame(self): + """The first parent frame node. + + A frame node is a :class:`Module`, :class:`FunctionDef`, + or :class:`ClassDef`. + + :returns: The first parent frame node. + """ + if not self.parent: + raise ParentMissingError(target=self) + + # For certain parents NamedExpr evaluate to the scope of the parent + if isinstance(self.parent, (Arguments, Keyword, Comprehension)): + if not self.parent.parent: + raise ParentMissingError(target=self.parent) + if not self.parent.parent.parent: + raise ParentMissingError(target=self.parent.parent) + return self.parent.parent.parent.frame() + + return self.parent.frame() + + def scope(self) -> "LocalsDictNodeNG": + """The first parent node defining a new scope. + These can be Module, FunctionDef, ClassDef, Lambda, or GeneratorExp nodes. + + :returns: The first parent scope node. + """ + if not self.parent: + raise ParentMissingError(target=self) + + # For certain parents NamedExpr evaluate to the scope of the parent + if isinstance(self.parent, (Arguments, Keyword, Comprehension)): + if not self.parent.parent: + raise ParentMissingError(target=self.parent) + if not self.parent.parent.parent: + raise ParentMissingError(target=self.parent.parent) + return self.parent.parent.parent.scope() + + return self.parent.scope() + class Unknown(mixins.AssignTypeMixin, NodeNG): """This node represents a node in a constructed AST where diff --git a/tests/unittest_nodes.py b/tests/unittest_nodes.py index 05c8b4ad..9b1fee88 100644 --- a/tests/unittest_nodes.py +++ b/tests/unittest_nodes.py @@ -682,6 +682,117 @@ class NameNodeTest(unittest.TestCase): builder.parse(code) +@pytest.mark.skipif(not PY38_PLUS, reason="needs assignment expressions") +class TestNamedExprNode: + """Tests for the NamedExpr node""" + + @staticmethod + def test_frame() -> None: + """Test if the frame of NamedExpr is correctly set for certain types + of parent nodes. + """ + module = builder.parse( + """ + def func(var_1): + pass + + def func_two(var_2, var_2 = (named_expr_1 := "walrus")): + pass + + class MyBaseClass: + pass + + class MyInheritedClass(MyBaseClass, var_3=(named_expr_2 := "walrus")): + pass + + VAR = lambda y = (named_expr_3 := "walrus"): print(y) + + def func_with_lambda( + var_5 = ( + named_expr_4 := lambda y = (named_expr_5 := "walrus"): y + ) + ): + pass + + COMPREHENSION = [y for i in (1, 2) if (y := i ** 2)] + """ + ) + function = module.body[0] + assert function.args.frame() == function + + function_two = module.body[1] + assert function_two.args.args[0].frame() == function_two + assert function_two.args.args[1].frame() == function_two + assert function_two.args.defaults[0].frame() == module + + inherited_class = module.body[3] + assert inherited_class.keywords[0].frame() == inherited_class + assert inherited_class.keywords[0].value.frame() == module + + lambda_assignment = module.body[4].value + assert lambda_assignment.args.args[0].frame() == lambda_assignment + assert lambda_assignment.args.defaults[0].frame() == module + + lambda_named_expr = module.body[5].args.defaults[0] + assert lambda_named_expr.value.args.defaults[0].frame() == module + + comprehension = module.body[6].value + assert comprehension.generators[0].ifs[0].frame() == module + + @staticmethod + def test_scope() -> None: + """Test if the scope of NamedExpr is correctly set for certain types + of parent nodes. + """ + module = builder.parse( + """ + def func(var_1): + pass + + def func_two(var_2, var_2 = (named_expr_1 := "walrus")): + pass + + class MyBaseClass: + pass + + class MyInheritedClass(MyBaseClass, var_3=(named_expr_2 := "walrus")): + pass + + VAR = lambda y = (named_expr_3 := "walrus"): print(y) + + def func_with_lambda( + var_5 = ( + named_expr_4 := lambda y = (named_expr_5 := "walrus"): y + ) + ): + pass + + COMPREHENSION = [y for i in (1, 2) if (y := i ** 2)] + """ + ) + function = module.body[0] + assert function.args.scope() == function + + function_two = module.body[1] + assert function_two.args.args[0].scope() == function_two + assert function_two.args.args[1].scope() == function_two + assert function_two.args.defaults[0].scope() == module + + inherited_class = module.body[3] + assert inherited_class.keywords[0].scope() == inherited_class + assert inherited_class.keywords[0].value.scope() == module + + lambda_assignment = module.body[4].value + assert lambda_assignment.args.args[0].scope() == lambda_assignment + assert lambda_assignment.args.defaults[0].scope() + + lambda_named_expr = module.body[5].args.defaults[0] + assert lambda_named_expr.value.args.defaults[0].scope() == module + + comprehension = module.body[6].value + assert comprehension.generators[0].ifs[0].scope() == module + + class AnnAssignNodeTest(unittest.TestCase): def test_primitive(self) -> None: code = textwrap.dedent( |