Skip to content

Commit 1498e83

Browse files
author
Chetan Khanna
committed
Remodel class scoped name resolution
Refactored to mimic CPython's symantics for name resolution in class scope when using LOAD_NAME instead of LOAD_CLASSDEREF Works on #9991
1 parent 8f2e6ad commit 1498e83

File tree

2 files changed

+290
-3
lines changed

2 files changed

+290
-3
lines changed

mypy/semanal.py

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@
201201
ClassPattern,
202202
MappingPattern,
203203
OrPattern,
204+
Pattern,
204205
SequencePattern,
205206
SingletonPattern,
206207
StarredPattern,
@@ -546,6 +547,12 @@ def __init__(
546547
# import foo.bar
547548
self.transitive_submodule_imports: dict[str, set[str]] = {}
548549

550+
# Stack of sets of names assigned in each enclosing class body.
551+
# Used to determine whether a name in a class body should be looked up
552+
# in enclosing function-local scopes or skipped (matching CPython's
553+
# LOAD_NAME vs LOAD_CLASSDEREF or LOAD_FROM_DICT_OR_DEREF behavior.
554+
self.class_body_assigned_names: list[set[str]] = []
555+
549556
# mypyc doesn't properly handle implementing an abstractproperty
550557
# with a regular attribute so we make them properties
551558
@property
@@ -2100,6 +2107,12 @@ def is_core_builtin_class(self, defn: ClassDef) -> bool:
21002107
def analyze_class_body_common(self, defn: ClassDef) -> None:
21012108
"""Parts of class body analysis that are common to all kinds of class defs."""
21022109
self.enter_class(defn.info)
2110+
# Pre-scan class body to find names assigned at class scope level.
2111+
# This must happen after enter_class (which pushes an empty set) so we
2112+
# can replace it with the real set.
2113+
self.class_body_assigned_names[-1] = collect_class_body_assigned_names(
2114+
defn.defs.body
2115+
)
21032116
if any(b.self_type is not None for b in defn.info.mro):
21042117
self.setup_self_type()
21052118
defn.defs.accept(self)
@@ -2225,6 +2238,7 @@ def enter_class(self, info: TypeInfo) -> None:
22252238
self.loop_depth.append(0)
22262239
self._type = info
22272240
self.missing_names.append(set())
2241+
self.class_body_assigned_names.append(set())
22282242

22292243
def leave_class(self) -> None:
22302244
"""Restore analyzer state."""
@@ -2234,6 +2248,7 @@ def leave_class(self) -> None:
22342248
self.scope_stack.pop()
22352249
self._type = self.type_stack.pop()
22362250
self.missing_names.pop()
2251+
self.class_body_assigned_names.pop()
22372252

22382253
def analyze_class_decorator(self, defn: ClassDef, decorator: Expression) -> None:
22392254
decorator.accept(self)
@@ -6600,9 +6615,19 @@ def _lookup(
66006615
v._fullname = self.qualified_name(name)
66016616
return SymbolTableNode(MDEF, v)
66026617
# 3. Local (function) scopes
6603-
for table in reversed(self.locals):
6604-
if table is not None and name in table:
6605-
return table[name]
6618+
# If we're in a class body and this name is assigned in the class body,
6619+
# skip enclosing function locals. This matches CPython's use of LOAD_NAME
6620+
# (class dict -> globals -> builtins) instead of
6621+
# LOAD_CLASSDEREF or LOAD_FROM_DICT_OR_DEREF for such names.
6622+
skip_func_locals = (
6623+
self.is_class_scope()
6624+
and self.class_body_assigned_names
6625+
and name in self.class_body_assigned_names[-1]
6626+
)
6627+
if not skip_func_locals:
6628+
for table in reversed(self.locals):
6629+
if table is not None and name in table:
6630+
return table[name]
66066631

66076632
# 4. Current file global scope
66086633
if name in self.globals:
@@ -8326,6 +8351,118 @@ def names_modified_in_lvalue(lvalue: Lvalue) -> list[NameExpr]:
83268351
return []
83278352

83288353

8354+
def _collect_lvalue_names(lvalue: Lvalue, names: set[str]) -> None:
8355+
"""Collect simple names from an lvalue into a set."""
8356+
if isinstance(lvalue, NameExpr):
8357+
names.add(lvalue.name)
8358+
elif isinstance(lvalue, StarExpr):
8359+
_collect_lvalue_names(lvalue.expr, names)
8360+
elif isinstance(lvalue, (ListExpr, TupleExpr)):
8361+
for item in lvalue.items:
8362+
_collect_lvalue_names(item, names)
8363+
8364+
8365+
def _collect_pattern_names(pattern: Pattern, names: set[str]) -> None:
8366+
"""Collect names bound by a match pattern."""
8367+
if isinstance(pattern, AsPattern):
8368+
if pattern.name is not None:
8369+
names.add(pattern.name.name)
8370+
if pattern.pattern is not None:
8371+
_collect_pattern_names(pattern.pattern, names)
8372+
elif isinstance(pattern, OrPattern):
8373+
for p in pattern.patterns:
8374+
_collect_pattern_names(p, names)
8375+
elif isinstance(pattern, SequencePattern):
8376+
for p in pattern.patterns:
8377+
_collect_pattern_names(p, names)
8378+
elif isinstance(pattern, StarredPattern):
8379+
if pattern.capture is not None:
8380+
names.add(pattern.capture.name)
8381+
elif isinstance(pattern, MappingPattern):
8382+
for p in pattern.values:
8383+
_collect_pattern_names(p, names)
8384+
if pattern.rest is not None:
8385+
names.add(pattern.rest.name)
8386+
elif isinstance(pattern, ClassPattern):
8387+
for p in pattern.positionals:
8388+
_collect_pattern_names(p, names)
8389+
for p in pattern.keyword_values:
8390+
_collect_pattern_names(p, names)
8391+
8392+
8393+
def collect_class_body_assigned_names(stmts: list[Statement]) -> set[str]:
8394+
"""Pre-scan a class body to find all names that are assigned at class scope.
8395+
8396+
This mirrors CPython's compile-time analysis that determines whether a name
8397+
in a class body is accessed via LOAD_NAME (class dict -> globals -> builtins)
8398+
or LOAD_CLASSDEREF or LOAD_FROM_DICT_OR_DEREF (class dict -> enclosing function cell).
8399+
8400+
Names that are assigned anywhere in the class body (even inside if/for/while/try/with blocks)
8401+
use LOAD_NAME, so they should NOT be resolved from enclosing function locals.
8402+
8403+
The scan is shallow: it recurses into control-flow blocks (if, for, while,
8404+
try, with, match) which don't create new scopes, but does NOT recurse into
8405+
function or class definitions which create their own scopes.
8406+
"""
8407+
names: set[str] = set()
8408+
for s in stmts:
8409+
if isinstance(s, AssignmentStmt):
8410+
for lvalue in s.lvalues:
8411+
_collect_lvalue_names(lvalue, names)
8412+
elif isinstance(s, OperatorAssignmentStmt):
8413+
_collect_lvalue_names(s.lvalue, names)
8414+
elif isinstance(s, (FuncDef, OverloadedFuncDef, Decorator)):
8415+
names.add(s.name)
8416+
elif isinstance(s, ClassDef):
8417+
names.add(s.name)
8418+
elif isinstance(s, Import):
8419+
for module_id, as_id in s.ids:
8420+
names.add(as_id if as_id else module_id.split(".")[0])
8421+
elif isinstance(s, ImportFrom):
8422+
for name, as_name in s.names:
8423+
names.add(as_name if as_name else name)
8424+
elif isinstance(s, TypeAliasStmt):
8425+
names.add(s.name.name)
8426+
elif isinstance(s, DelStmt):
8427+
_collect_lvalue_names(s.expr, names)
8428+
elif isinstance(s, ForStmt):
8429+
_collect_lvalue_names(s.index, names)
8430+
names.update(collect_class_body_assigned_names(s.body.body))
8431+
if s.else_body:
8432+
names.update(collect_class_body_assigned_names(s.else_body.body))
8433+
elif isinstance(s, IfStmt):
8434+
for block in s.body:
8435+
names.update(collect_class_body_assigned_names(block.body))
8436+
if s.else_body:
8437+
names.update(collect_class_body_assigned_names(s.else_body.body))
8438+
elif isinstance(s, WhileStmt):
8439+
names.update(collect_class_body_assigned_names(s.body.body))
8440+
if s.else_body:
8441+
names.update(collect_class_body_assigned_names(s.else_body.body))
8442+
elif isinstance(s, TryStmt):
8443+
names.update(collect_class_body_assigned_names(s.body.body))
8444+
for var in s.vars:
8445+
if var is not None:
8446+
names.add(var.name)
8447+
for handler in s.handlers:
8448+
names.update(collect_class_body_assigned_names(handler.body))
8449+
if s.else_body:
8450+
names.update(collect_class_body_assigned_names(s.else_body.body))
8451+
if s.finally_body:
8452+
names.update(collect_class_body_assigned_names(s.finally_body.body))
8453+
elif isinstance(s, WithStmt):
8454+
for target in s.target:
8455+
if target is not None:
8456+
_collect_lvalue_names(target, names)
8457+
names.update(collect_class_body_assigned_names(s.body.body))
8458+
elif isinstance(s, MatchStmt):
8459+
for pattern in s.patterns:
8460+
_collect_pattern_names(pattern, names)
8461+
for body_block in s.bodies:
8462+
names.update(collect_class_body_assigned_names(body_block.body))
8463+
return names
8464+
8465+
83298466
def is_same_var_from_getattr(n1: SymbolNode | None, n2: SymbolNode | None) -> bool:
83308467
"""Do n1 and n2 refer to the same Var derived from module-level __getattr__?"""
83318468
return (

test-data/unit/check-classes.test

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9412,3 +9412,153 @@ from typ import NT
94129412
def f() -> NT:
94139413
return NT(x='')
94149414
[builtins fixtures/tuple.pyi]
9415+
9416+
-- Class body scope: enclosing function local visibility
9417+
-- -----------------------------------------------------
9418+
-- These tests verify that names assigned in a class body nested inside a
9419+
-- function are NOT resolved from the enclosing function's local scope,
9420+
-- matching CPython's LOAD_NAME semantics for class bodies (class dict ->
9421+
-- globals -> builtins, skipping enclosing function locals).
9422+
9423+
[case testClassBodySkipsEnclosingFunctionLocalForAssignedName]
9424+
# A name assigned in the class body should not resolve from the enclosing
9425+
# function's locals; it should fall through to globals/builtins.
9426+
x = 1
9427+
y = 2
9428+
def func() -> None:
9429+
x = "xlocal"
9430+
y = "ylocal"
9431+
class C:
9432+
reveal_type(x) # N: Revealed type is "builtins.str"
9433+
reveal_type(y) # N: Revealed type is "builtins.int"
9434+
y = 1
9435+
func()
9436+
9437+
[case testClassBodyAssignedNameNoGlobalGivesError]
9438+
# If the name is assigned in the class body and there is no global definition,
9439+
# looking it up before the assignment should be an error.
9440+
def func() -> None:
9441+
y = "ylocal"
9442+
class C:
9443+
z = y # E: Name "y" is not defined
9444+
y = 1
9445+
func()
9446+
9447+
[case testClassBodyUnassignedNameSeesEnclosingFunctionLocal]
9448+
# A name that is NOT assigned anywhere in the class body can still be
9449+
# resolved from the enclosing function's local scope.
9450+
def func() -> None:
9451+
x = "hello"
9452+
class C:
9453+
reveal_type(x) # N: Revealed type is "builtins.str"
9454+
func()
9455+
9456+
[case testClassBodyMethodClosesOverEnclosingFunctionLocal]
9457+
# Methods inside the class should still close over the enclosing function
9458+
# locals, even for names that are assigned in the class body.
9459+
x = 1
9460+
y = 2
9461+
def func() -> None:
9462+
x = "xlocal"
9463+
y = "ylocal"
9464+
class C:
9465+
y = 1
9466+
def method(self) -> None:
9467+
reveal_type(x) # N: Revealed type is "builtins.str"
9468+
reveal_type(y) # N: Revealed type is "builtins.str"
9469+
func()
9470+
9471+
[case testClassBodyForwardRefClassAttrFallsToGlobal]
9472+
# When a class attribute is assigned textually *after* its use,
9473+
# is_active_symbol_in_class_body rejects it, and lookup should fall
9474+
# through to the global — not the enclosing function local.
9475+
x: int = 10
9476+
def func() -> None:
9477+
x = "hello"
9478+
class C:
9479+
y = x # should see global int, not func's str
9480+
x = 42
9481+
reveal_type(C.y) # N: Revealed type is "builtins.int"
9482+
func()
9483+
9484+
[case testClassBodyNestedClassInFunctionSkipsFunctionLocal]
9485+
# A doubly-nested class should also skip the enclosing function's locals.
9486+
y: int = 1
9487+
def func() -> None:
9488+
y = "ylocal"
9489+
class Outer:
9490+
class Inner:
9491+
reveal_type(y) # N: Revealed type is "builtins.int"
9492+
y = 42
9493+
func()
9494+
9495+
[case testClassBodyMethodInNestedClassClosesOverFunctionLocal]
9496+
# A method in a nested class should still form a closure over the
9497+
# enclosing function's locals.
9498+
def func() -> None:
9499+
y = "ylocal"
9500+
class Outer:
9501+
class Inner:
9502+
y = 42
9503+
def method(self) -> None:
9504+
reveal_type(y) # N: Revealed type is "builtins.str"
9505+
func()
9506+
9507+
[case testClassBodyGenericClassInFunctionStillWorks]
9508+
# TypeVar defined in the enclosing function should still be visible
9509+
# in the class body (TypeVarExpr is not a plain Var).
9510+
from typing import TypeVar, Generic
9511+
def func() -> None:
9512+
T = TypeVar('T')
9513+
class MyGeneric(Generic[T]):
9514+
def get(self, x: T) -> T:
9515+
return x
9516+
reveal_type(MyGeneric[int]().get(1)) # N: Revealed type is "builtins.int"
9517+
func()
9518+
9519+
[case testClassBodyAssignmentInControlFlow]
9520+
# Names assigned inside if/for/while/try/with blocks in the class body
9521+
# should still be treated as class-local.
9522+
x: int = 10
9523+
def func() -> None:
9524+
x = "xlocal"
9525+
class C:
9526+
reveal_type(x) # N: Revealed type is "builtins.int"
9527+
if True:
9528+
x = 42
9529+
func()
9530+
[builtins fixtures/bool.pyi]
9531+
9532+
[case testClassBodyForLoopVariable]
9533+
# A for-loop index variable at class scope should be local to the class.
9534+
x: int = 10
9535+
def func() -> None:
9536+
x = "xlocal"
9537+
class C:
9538+
reveal_type(x) # N: Revealed type is "builtins.int"
9539+
for x in [1, 2, 3]:
9540+
pass
9541+
func()
9542+
[builtins fixtures/list.pyi]
9543+
9544+
[case testClassBodyImportedName]
9545+
# An imported name in the class body should be treated as a class-local binding.
9546+
import typing
9547+
x: int = 10
9548+
def func() -> None:
9549+
x = "xlocal"
9550+
class C:
9551+
reveal_type(x) # N: Revealed type is "builtins.int"
9552+
import typing as x # type: ignore
9553+
# x is now assigned in the class body; use before assignment sees global int
9554+
func()
9555+
9556+
[case testClassBodyComprehensionSeesEnclosingVars]
9557+
# Comprehensions inside a class body can see enclosing function locals
9558+
def func() -> None:
9559+
items = [1, 2, 3]
9560+
class C:
9561+
result = [i for i in items]
9562+
reveal_type(result) # N: Revealed type is "builtins.list[builtins.int]"
9563+
func()
9564+
[builtins fixtures/list.pyi]

0 commit comments

Comments
 (0)