Skip to content

Commit

Permalink
Fix incorrect undefined_name in doubly nested classes (#752)
Browse files Browse the repository at this point in the history
  • Loading branch information
JelleZijlstra authored Mar 11, 2024
1 parent 4dd5205 commit f107b01
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 18 deletions.
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Fix incorrect `undefined_name` errors when a class is nested in a nested
function and uses a name from the outer function (#750)
- Fix incorrect `possibly_undefined_name` error on certain uses of the
walrus operator (#749)
- Fix narrowing on `isinstance` calls with arguments that are not
Expand Down
10 changes: 9 additions & 1 deletion pyanalyze/name_check_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1822,7 +1822,15 @@ def _get_current_class_object(self, node: ast.ClassDef) -> Optional[type]:
def _visit_class_and_get_value(
self, node: ast.ClassDef, current_class: Optional[type]
) -> Value:
if self._is_checking():
if self._is_collecting():
# If this is a nested class, we need to run the collecting phase to get data
# about names accessed from the class.
if len(self.scopes.scopes) > 2:
with self.scopes.add_scope(
ScopeType.class_scope, scope_node=node, scope_object=current_class
), self._set_current_class(current_class):
self._generic_visit_list(node.body)
else:
with self.scopes.add_scope(
ScopeType.class_scope, scope_node=None, scope_object=current_class
), self._set_current_class(current_class):
Expand Down
17 changes: 0 additions & 17 deletions pyanalyze/test_name_check_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,23 +396,6 @@ def test_inferred_unhashable_dict_key(self):
def run():
print({key: 1}) # E: unhashable_key

@assert_passes()
def test_nested_classes(self):
class Caviids(object):
class Capybaras(object):
if False:
print(neochoerus) # E: undefined_name

def method(self, cap: Capybaras):
assert_is_value(cap, TypedValue(Caviids.Capybaras))

@assert_passes()
def test_class_in_function(self):
def get_capybaras(object):
class Capybaras(object):
if False:
print(neochoerus) # E: undefined_name

@assert_passes()
def test_cant_del_tuple(self):
tpl = (1, 2, 3)
Expand Down
62 changes: 62 additions & 0 deletions pyanalyze/test_stacked_scopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1893,3 +1893,65 @@ def capybara(key: Optional[int]):
assert_is_value(key, TypedValue(int))
else:
assert_is_value(key, KnownValue(None))


class TestClassNesting(TestNameCheckVisitorBase):

@assert_passes()
def test_class_in_class(self):
class Caviids(object):
class Capybaras(object):
if False:
print(neochoerus) # E: undefined_name

def method(self, cap: Capybaras):
assert_is_value(cap, TypedValue(Caviids.Capybaras))

@assert_passes()
def test_class_in_function(self):
from typing_extensions import Literal, assert_type

def get_capybaras(object):
x = 3

class Capybaras(object):
if False:
print(neochoerus) # E: undefined_name

assert_type(x, Literal[3])

@assert_passes()
def test_double_nesting(self):
from typing_extensions import Literal, assert_type

def outer():
outer_var = "outer"

def inner():
inner_var = "inner"

class Nested:
assert_type(outer_var, Literal["outer"])
assert_type(inner_var, Literal["inner"])

return Nested

return inner

@assert_passes()
def test_triple_function_nesting(self):
from typing_extensions import Literal, assert_type

def outer():
outer_var = "outer"

def inner():
inner_var = "inner"

def innermost():
assert_type(outer_var, Literal["outer"])
assert_type(inner_var, Literal["inner"])

return innermost

return inner

0 comments on commit f107b01

Please sign in to comment.