From e66a5e2716e6c5970b0427f9fc76c8602bc2da60 Mon Sep 17 00:00:00 2001 From: David Liu Date: Mon, 19 Jul 2021 19:14:00 +0000 Subject: [PATCH] Support lookup in nested non-FunctionDef scopes (#1102) Co-authored-by: Pierre Sassoulas --- ChangeLog | 2 + astroid/scoped_nodes.py | 19 ++++--- tests/unittest_lookup.py | 114 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 8 deletions(-) diff --git a/ChangeLog b/ChangeLog index 530c8f3060..e887f209b8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -25,6 +25,8 @@ Release date: TBA Closes PyCQA/pylint#4685 +* Fix lookup for nested non-function scopes + * Fix issue that ``TypedDict`` instance wasn't callable. Closes PyCQA/pylint#4715 diff --git a/astroid/scoped_nodes.py b/astroid/scoped_nodes.py index 5fa890d94e..d6cda7fa73 100644 --- a/astroid/scoped_nodes.py +++ b/astroid/scoped_nodes.py @@ -244,14 +244,17 @@ def _scope_lookup(self, node, name, offset=0): stmts = () if stmts: return self, stmts - if self.parent: # i.e. not Module - # nested scope: if parent scope is a function, that's fine - # else jump to the module - pscope = self.parent.scope() - if not pscope.is_function: - pscope = pscope.root() - return pscope.scope_lookup(node, name) - return builtin_lookup(name) # Module + + # Handle nested scopes: since class names do not extend to nested + # scopes (e.g., methods), we find the next enclosing non-class scope + pscope = self.parent and self.parent.scope() + while pscope is not None: + if not isinstance(pscope, ClassDef): + return pscope.scope_lookup(node, name) + pscope = pscope.parent and pscope.parent.scope() + + # self is at the top level of a module, or is enclosed only by ClassDefs + return builtin_lookup(name) def set_local(self, name, stmt): """Define that the given name is declared in the given statement node. diff --git a/tests/unittest_lookup.py b/tests/unittest_lookup.py index dd22468bbe..bcd6ae8249 100644 --- a/tests/unittest_lookup.py +++ b/tests/unittest_lookup.py @@ -218,6 +218,120 @@ def test_set_comp_closure(self): var = astroid.body[1].value self.assertRaises(NameInferenceError, var.inferred) + def test_list_comp_nested(self): + astroid = builder.parse( + """ + x = [[i + j for j in range(20)] + for i in range(10)] + """, + __name__, + ) + xnames = [n for n in astroid.nodes_of_class(nodes.Name) if n.name == "i"] + self.assertEqual(len(xnames[0].lookup("i")[1]), 1) + self.assertEqual(xnames[0].lookup("i")[1][0].lineno, 3) + + def test_dict_comp_nested(self): + astroid = builder.parse( + """ + x = {i: {i: j for j in range(20)} + for i in range(10)} + x3 = [{i + j for j in range(20)} # Can't do nested sets + for i in range(10)] + """, + __name__, + ) + xnames = [n for n in astroid.nodes_of_class(nodes.Name) if n.name == "i"] + self.assertEqual(len(xnames[0].lookup("i")[1]), 1) + self.assertEqual(xnames[0].lookup("i")[1][0].lineno, 3) + self.assertEqual(len(xnames[1].lookup("i")[1]), 1) + self.assertEqual(xnames[1].lookup("i")[1][0].lineno, 3) + + def test_set_comp_nested(self): + astroid = builder.parse( + """ + x = [{i + j for j in range(20)} # Can't do nested sets + for i in range(10)] + """, + __name__, + ) + xnames = [n for n in astroid.nodes_of_class(nodes.Name) if n.name == "i"] + self.assertEqual(len(xnames[0].lookup("i")[1]), 1) + self.assertEqual(xnames[0].lookup("i")[1][0].lineno, 3) + + def test_lambda_nested(self): + astroid = builder.parse( + """ + f = lambda x: ( + lambda y: x + y) + """ + ) + xnames = [n for n in astroid.nodes_of_class(nodes.Name) if n.name == "x"] + self.assertEqual(len(xnames[0].lookup("x")[1]), 1) + self.assertEqual(xnames[0].lookup("x")[1][0].lineno, 2) + + def test_function_nested(self): + astroid = builder.parse( + """ + def f1(x): + def f2(y): + return x + y + + return f2 + """ + ) + xnames = [n for n in astroid.nodes_of_class(nodes.Name) if n.name == "x"] + self.assertEqual(len(xnames[0].lookup("x")[1]), 1) + self.assertEqual(xnames[0].lookup("x")[1][0].lineno, 2) + + def test_class_variables(self): + # Class variables are NOT available within nested scopes. + astroid = builder.parse( + """ + class A: + a = 10 + + def f1(self): + return a # a is not defined + + f2 = lambda: a # a is not defined + + b = [a for _ in range(10)] # a is not defined + + class _Inner: + inner_a = a + 1 + """ + ) + names = [n for n in astroid.nodes_of_class(nodes.Name) if n.name == "a"] + self.assertEqual(len(names), 4) + for name in names: + self.assertRaises(NameInferenceError, name.inferred) + + def test_class_in_function(self): + # Function variables are available within classes, including methods + astroid = builder.parse( + """ + def f(): + x = 10 + class A: + a = x + + def f1(self): + return x + + f2 = lambda: x + + b = [x for _ in range(10)] + + class _Inner: + inner_a = x + 1 + """ + ) + names = [n for n in astroid.nodes_of_class(nodes.Name) if n.name == "x"] + self.assertEqual(len(names), 5) + for name in names: + self.assertEqual(len(name.lookup("x")[1]), 1, repr(name)) + self.assertEqual(name.lookup("x")[1][0].lineno, 3, repr(name)) + def test_generator_attributes(self): tree = builder.parse( """