Skip to content

Commit ac07166

Browse files
authored
[mypyc] Fix cross-module class attribute defaults causing KeyError (#21012)
When mypyc compiles a subclass that inherits class attribute defaults from a parent in a different module, the generated `__mypyc_defaults_setup` method would look that up in the subclass's module globals instead of the parent's, causing `KeyError` at runtime. This PR fixes this by tracking the origin module for each default assignment and using a targeted `globals_lookup_module` override on the builder when evaluating inherited rvalues. This only affects `load_globals_dict`, not lambda/closure declarations. Also export each module's globals dict in the export table so cross-group access works in separate compilation mode.
1 parent bf1b7c8 commit ac07166

File tree

4 files changed

+57
-8
lines changed

4 files changed

+57
-8
lines changed

mypyc/codegen/emitmodule.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1284,7 +1284,10 @@ def declare_global(
12841284

12851285
def declare_internal_globals(self, module_name: str, emitter: Emitter) -> None:
12861286
static_name = emitter.static_name("globals", module_name)
1287-
self.declare_global("PyObject *", static_name)
1287+
if static_name not in self.context.declarations:
1288+
self.context.declarations[static_name] = HeaderDeclaration(
1289+
f"PyObject *{static_name};", needs_export=True
1290+
)
12881291

12891292
def module_internal_static_name(self, module_name: str, emitter: Emitter) -> str:
12901293
return emitter.static_name(module_name + "__internal", None, prefix=MODULE_PREFIX)

mypyc/irbuild/builder.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,10 @@ def __init__(
253253

254254
self.can_borrow = False
255255

256+
# When set, load_globals_dict uses this module instead of self.module_name.
257+
# Used by generate_attr_defaults_init for cross-module inherited defaults.
258+
self.globals_lookup_module: str | None = None
259+
256260
# High-level control
257261

258262
def set_module(self, module_name: str, module_path: str) -> None:
@@ -1454,7 +1458,8 @@ def load_global_str(self, name: str, line: int) -> Value:
14541458
return self.primitive_op(dict_get_item_op, [_globals, reg], line)
14551459

14561460
def load_globals_dict(self) -> Value:
1457-
return self.add(LoadStatic(dict_rprimitive, "globals", self.module_name))
1461+
module = self.globals_lookup_module or self.module_name
1462+
return self.add(LoadStatic(dict_rprimitive, "globals", module))
14581463

14591464
def load_module_attr_by_fullname(self, fullname: str, line: int) -> Value:
14601465
module, _, name = fullname.rpartition(".")

mypyc/irbuild/classdef.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,7 @@ def add_non_ext_class_attr(
713713

714714
def find_attr_initializers(
715715
builder: IRBuilder, cdef: ClassDef, skip: Callable[[str, AssignmentStmt], bool] | None = None
716-
) -> tuple[set[str], list[AssignmentStmt]]:
716+
) -> tuple[set[str], list[tuple[AssignmentStmt, str]]]:
717717
"""Find initializers of attributes in a class body.
718718
719719
If provided, the skip arg should be a callable which will return whether
@@ -728,7 +728,7 @@ def find_attr_initializers(
728728

729729
# Pull out all assignments in classes in the mro so we can initialize them
730730
# TODO: Support nested statements
731-
default_assignments = []
731+
default_assignments: list[tuple[AssignmentStmt, str]] = []
732732
for info in reversed(cdef.info.mro):
733733
if info not in builder.mapper.type_to_ir:
734734
continue
@@ -763,13 +763,13 @@ def find_attr_initializers(
763763
continue
764764

765765
attrs_with_defaults.add(name)
766-
default_assignments.append(stmt)
766+
default_assignments.append((stmt, info.module_name))
767767

768768
return attrs_with_defaults, default_assignments
769769

770770

771771
def generate_attr_defaults_init(
772-
builder: IRBuilder, cdef: ClassDef, default_assignments: list[AssignmentStmt]
772+
builder: IRBuilder, cdef: ClassDef, default_assignments: list[tuple[AssignmentStmt, str]]
773773
) -> None:
774774
"""Generate an initialization method for default attr values (from class vars)."""
775775
if not default_assignments:
@@ -780,14 +780,23 @@ def generate_attr_defaults_init(
780780

781781
with builder.enter_method(cls, "__mypyc_defaults_setup", bool_rprimitive):
782782
self_var = builder.self()
783-
for stmt in default_assignments:
783+
for stmt, origin_module in default_assignments:
784784
lvalue = stmt.lvalues[0]
785785
assert isinstance(lvalue, NameExpr), lvalue
786786
if not stmt.is_final_def and not is_constant(stmt.rvalue):
787787
builder.warning("Unsupported default attribute value", stmt.rvalue.line)
788788

789789
attr_type = cls.attr_type(lvalue.name)
790-
val = builder.coerce(builder.accept(stmt.rvalue), attr_type, stmt.line)
790+
# When the default comes from a parent in a different module,
791+
# set the globals lookup module so NameExpr references resolve
792+
# against the correct module's globals dict.
793+
builder.globals_lookup_module = (
794+
origin_module if origin_module != builder.module_name else None
795+
)
796+
try:
797+
val = builder.coerce(builder.accept(stmt.rvalue), attr_type, stmt.line)
798+
finally:
799+
builder.globals_lookup_module = None
791800
init = SetAttr(self_var, lvalue.name, val, stmt.rvalue.line)
792801
init.mark_as_initializer()
793802
builder.add(init)

mypyc/test-data/run-multimodule.test

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,3 +962,35 @@ def translate(b: bytes) -> bytes:
962962
[file driver.py]
963963
import native
964964
assert native.translate(b'ABCD') == b'BBCD'
965+
966+
[case testCrossModuleAttrDefaults]
967+
from other import Parent
968+
969+
class Child(Parent):
970+
extra: int = 99
971+
972+
def test() -> None:
973+
c = Child()
974+
assert c.config == {"key": "value"}
975+
assert c.extra == 99
976+
assert c.total == 60
977+
p = Parent()
978+
assert p.config == {"key": "value"}
979+
assert p.total == 60
980+
assert Parent.TAG == "parent"
981+
982+
[file other.py]
983+
from typing import ClassVar, Dict
984+
MY_DEFAULT: Dict[str, str] = {"key": "value"}
985+
VAL_1: int = 10
986+
VAL_2: int = 20
987+
VAL_3: int = 30
988+
989+
class Parent:
990+
config: Dict[str, str] = MY_DEFAULT
991+
total: int = VAL_1 + VAL_2 + VAL_3
992+
TAG: ClassVar[str] = "parent"
993+
994+
[file driver.py]
995+
from native import test
996+
test()

0 commit comments

Comments
 (0)