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+
83298466def 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 (
0 commit comments