Skip to content

Commit c73021f

Browse files
[mypyc] Fix ClassVar self-references in class bodies (#21011)
Fixes mypyc/mypyc#972. In CPython, the class body executes as a function where earlier assignments are available to later ones e.g this is possible: ```Python3 class Foo: A: t.ClassVar = {1, 2, 3} B: t.ClassVar = {4, 5, 6} C: t.ClassVar = A | B ``` mypyc previously resolved such names via `load_global()`, looking them up in the module globals dict where they don't exist causing a KeyError at runtime. This PR fixes this by tracking `ClassVar` names as they're defined during class body processing, and redirecting lookups to the class being built: the type object (`py_get_attr`) for extension classes, or the class dict (`dict_get_item_op`) for non-extension classes. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent ac07166 commit c73021f

File tree

5 files changed

+421
-0
lines changed

5 files changed

+421
-0
lines changed

mypyc/irbuild/builder.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,14 @@ def __init__(
233233

234234
self.visitor = visitor
235235

236+
# Class body context: tracks ClassVar names defined so far when processing
237+
# a class body, so that intra-class references (e.g. C = A | B where A is
238+
# a ClassVar defined earlier in the same class) can be resolved correctly.
239+
# Without this, mypyc looks up such names in module globals, which fails.
240+
self.class_body_classvars: dict[str, None] = {}
241+
self.class_body_obj: Value | None = None
242+
self.class_body_ir: ClassIR | None = None
243+
236244
# This list operates similarly to a function call stack for nested functions. Whenever a
237245
# function definition begins to be generated, a FuncInfo instance is added to the stack,
238246
# and information about that function (e.g. whether it is nested, its environment class to

mypyc/irbuild/classdef.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,13 @@ def transform_class_def(builder: IRBuilder, cdef: ClassDef) -> None:
137137
else:
138138
cls_builder = NonExtClassBuilder(builder, cdef)
139139

140+
# Set up class body context so that intra-class ClassVar references
141+
# (e.g. C = A | B where A is defined earlier in the same class) can be
142+
# resolved from the class being built instead of module globals.
143+
builder.class_body_classvars = {}
144+
builder.class_body_obj = cls_builder.class_body_obj()
145+
builder.class_body_ir = ir
146+
140147
for stmt in cdef.defs.body:
141148
if (
142149
isinstance(stmt, (FuncDef, Decorator, OverloadedFuncDef))
@@ -179,13 +186,21 @@ def transform_class_def(builder: IRBuilder, cdef: ClassDef) -> None:
179186
# We want to collect class variables in a dictionary for both real
180187
# non-extension classes and fake dataclass ones.
181188
cls_builder.add_attr(lvalue, stmt)
189+
# Track this ClassVar so subsequent class body statements can reference it.
190+
if is_class_var(lvalue) or stmt.is_final_def:
191+
builder.class_body_classvars[lvalue.name] = None
182192

183193
elif isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, StrExpr):
184194
# Docstring. Ignore
185195
pass
186196
else:
187197
builder.error("Unsupported statement in class body", stmt.line)
188198

199+
# Clear class body context (nested classes are rejected above, so no need to save/restore).
200+
builder.class_body_classvars = {}
201+
builder.class_body_obj = None
202+
builder.class_body_ir = None
203+
189204
# Generate implicit property setters/getters
190205
for name, decl in ir.method_decls.items():
191206
if decl.implicit and decl.is_prop_getter:
@@ -232,12 +247,23 @@ def add_attr(self, lvalue: NameExpr, stmt: AssignmentStmt) -> None:
232247
def finalize(self, ir: ClassIR) -> None:
233248
"""Perform any final operations to complete the class IR"""
234249

250+
def class_body_obj(self) -> Value | None:
251+
"""Return the object to use for loading class attributes during class body init.
252+
253+
For extension classes, this is the type object. For non-extension classes,
254+
this is the class dict. Returns None if not applicable.
255+
"""
256+
return None
257+
235258

236259
class NonExtClassBuilder(ClassBuilder):
237260
def __init__(self, builder: IRBuilder, cdef: ClassDef) -> None:
238261
super().__init__(builder, cdef)
239262
self.non_ext = self.create_non_ext_info()
240263

264+
def class_body_obj(self) -> Value | None:
265+
return self.non_ext.dict
266+
241267
def create_non_ext_info(self) -> NonExtClassInfo:
242268
non_ext_bases = populate_non_ext_bases(self.builder, self.cdef)
243269
non_ext_metaclass = find_non_ext_metaclass(self.builder, self.cdef, non_ext_bases)
@@ -293,6 +319,9 @@ def __init__(self, builder: IRBuilder, cdef: ClassDef) -> None:
293319
# If the class is not decorated, generate an extension class for it.
294320
self.type_obj: Value = allocate_class(builder, cdef)
295321

322+
def class_body_obj(self) -> Value | None:
323+
return self.type_obj
324+
296325
def skip_attr_default(self, name: str, stmt: AssignmentStmt) -> bool:
297326
"""Controls whether to skip generating a default for an attribute."""
298327
return False

mypyc/irbuild/expression.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,17 @@ def transform_name_expr(builder: IRBuilder, expr: NameExpr) -> Value:
213213
else:
214214
return builder.read(builder.get_assignment_target(expr, for_read=True), expr.line)
215215

216+
# If we're evaluating a class body and this name is a ClassVar defined earlier
217+
# in the same class, load it from the class being built (type object for ext classes,
218+
# class dict for non-ext classes) instead of module globals.
219+
if builder.class_body_obj is not None and expr.name in builder.class_body_classvars:
220+
if builder.class_body_ir is not None and builder.class_body_ir.is_ext_class:
221+
return builder.py_get_attr(builder.class_body_obj, expr.name, expr.line)
222+
else:
223+
return builder.primitive_op(
224+
dict_get_item_op, [builder.class_body_obj, builder.load_str(expr.name)], expr.line
225+
)
226+
216227
return builder.load_global(expr)
217228

218229

0 commit comments

Comments
 (0)