Skip to content

Commit

Permalink
Support lookup in nested non-FunctionDef scopes (#1102)
Browse files Browse the repository at this point in the history
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
  • Loading branch information
david-yz-liu and Pierre-Sassoulas authored Jul 19, 2021
1 parent 9947a59 commit e66a5e2
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 8 deletions.
2 changes: 2 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 11 additions & 8 deletions astroid/scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
114 changes: 114 additions & 0 deletions tests/unittest_lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
"""
Expand Down

0 comments on commit e66a5e2

Please sign in to comment.