[mypyc] Preserve inherited attribute defaults under separate=True (#21547)
Fixes #21542
Under `separate=True`, when a subclass is recompiled while its parent is
loaded from mypy's incremental cache, parent default-attribute
assignments are silently dropped from the subclass's
`__mypyc_defaults_setup`. The first read of an inherited default-attr
then raises:
```
AttributeError: attribute '<name>' of '<Parent>' undefined
```
`find_attr_initializers` walks `cdef.info.mro` and reads
`info.defn.defs.body` for `AssignmentStmt`s. `ClassDef.serialize`
(mypy/nodes.py) does not serialize `defs`, so a cache-loaded parent has
`defs = Block([])`; the MRO walk collects no parent assignments and the
subclass's emitted setup leaves inherited slots in the
undefined-sentinel state.
This PR implements the fix discussed in the linked issue.diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py
index ea3e2dd..db94f1d 100644
--- a/mypyc/codegen/emitclass.py
+++ b/mypyc/codegen/emitclass.py
@@ -29,6 +29,7 @@
BITMAP_BITS,
BITMAP_TYPE,
CPYFUNCTION_NAME,
+ MYPYC_DEFAULTS_SETUP,
NATIVE_PREFIX,
PREFIX,
REG_PREFIX,
@@ -285,7 +286,7 @@
# If the class has a method to initialize default attribute
# values, we need to call it during initialization.
- defaults_fn = cl.get_method("__mypyc_defaults_setup")
+ defaults_fn = cl.get_method(MYPYC_DEFAULTS_SETUP)
# If there is a __init__ method, we'll use it in the native constructor.
init_fn = cl.get_method("__init__")
diff --git a/mypyc/common.py b/mypyc/common.py
index 64fe812..382d640 100644
--- a/mypyc/common.py
+++ b/mypyc/common.py
@@ -24,6 +24,7 @@
LAMBDA_NAME: Final = "__mypyc_lambda__"
PROPSET_PREFIX: Final = "__mypyc_setter__"
SELF_NAME: Final = "__mypyc_self__"
+MYPYC_DEFAULTS_SETUP: Final = "__mypyc_defaults_setup"
GENERATOR_ATTRIBUTE_PREFIX: Final = "__mypyc_generator_attribute__"
CPYFUNCTION_NAME = "__cpyfunction__"
diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py
index 3f95cf8..5bc19c9 100644
--- a/mypyc/irbuild/classdef.py
+++ b/mypyc/irbuild/classdef.py
@@ -7,6 +7,7 @@
from typing import Final
from mypy.nodes import (
+ ARG_POS,
EXCLUDED_ENUM_ATTRIBUTES,
TYPE_VAR_TUPLE_KIND,
AssignmentStmt,
@@ -21,7 +22,6 @@
NameExpr,
OverloadedFuncDef,
PassStmt,
- RefExpr,
StrExpr,
TempNode,
TypeInfo,
@@ -29,7 +29,7 @@
is_class_var,
)
from mypy.types import Instance, UnboundType, get_proper_type
-from mypyc.common import PROPSET_PREFIX
+from mypyc.common import MYPYC_DEFAULTS_SETUP, PROPSET_PREFIX
from mypyc.ir.class_ir import ClassIR, NonExtClassInfo
from mypyc.ir.func_ir import FuncDecl, FuncSignature
from mypyc.ir.ops import (
@@ -48,15 +48,7 @@
TupleSet,
Value,
)
-from mypyc.ir.rtypes import (
- RType,
- bool_rprimitive,
- dict_rprimitive,
- is_none_rprimitive,
- is_object_rprimitive,
- is_optional_type,
- object_rprimitive,
-)
+from mypyc.ir.rtypes import RType, bool_rprimitive, dict_rprimitive, object_rprimitive
from mypyc.irbuild.builder import IRBuilder, create_type_params
from mypyc.irbuild.function import (
gen_property_getter_ir,
@@ -66,7 +58,13 @@
load_type,
)
from mypyc.irbuild.prepare import GENERATOR_HELPER_NAME
-from mypyc.irbuild.util import dataclass_type, get_func_def, is_constant, is_dataclass_decorator
+from mypyc.irbuild.util import (
+ dataclass_type,
+ default_attr_name,
+ get_func_def,
+ is_constant,
+ is_dataclass_decorator,
+)
from mypyc.primitives.dict_ops import dict_new_op, exact_dict_set_item_op
from mypyc.primitives.generic_ops import (
iter_op,
@@ -322,10 +320,6 @@
def class_body_obj(self) -> Value | None:
return self.type_obj
- def skip_attr_default(self, name: str, stmt: AssignmentStmt) -> bool:
- """Controls whether to skip generating a default for an attribute."""
- return False
-
def add_method(self, fdef: FuncDef) -> None:
handle_ext_method(self.builder, self.cdef, fdef)
@@ -348,11 +342,18 @@
# Call __init_subclass__ after class attributes have been set
self.builder.call_c(py_init_subclass_op, [self.type_obj], self.cdef.line)
- attrs_with_defaults, default_assignments = find_attr_initializers(
- self.builder, self.cdef, self.skip_attr_default
- )
- ir.attrs_with_defaults.update(attrs_with_defaults)
- generate_attr_defaults_init(self.builder, self.cdef, default_assignments)
+ # Under separate compilation, prepare.py pre-registers the decl iff
+ # the class has its own default attribute assignments to emit, so we
+ # can skip the body walk entirely when it isn't present. Without
+ # separate compilation, find_attr_initializers walks the MRO so that
+ # inherited defaults are reflected in ir.attrs_with_defaults (relied
+ # on by the attribute-definedness analysis), so we always run it.
+ if not self.builder.options.separate or MYPYC_DEFAULTS_SETUP in ir.method_decls:
+ attrs_with_defaults, default_assignments = find_attr_initializers(
+ self.builder, self.cdef
+ )
+ ir.attrs_with_defaults.update(attrs_with_defaults)
+ generate_attr_defaults_init(self.builder, self.cdef, default_assignments)
create_ne_from_eq(self.builder, self.cdef)
@@ -380,9 +381,6 @@
self.builder.add(LoadAddress(type_object_op.type, type_object_op.src, self.cdef.line)),
)
- def skip_attr_default(self, name: str, stmt: AssignmentStmt) -> bool:
- return stmt.type is not None
-
def get_type_annotation(self, stmt: AssignmentStmt) -> TypeInfo | None:
# We populate __annotations__ because dataclasses uses it to determine
# which attributes to compute on.
@@ -445,9 +443,6 @@
add_annotations_to_dict = False
- def skip_attr_default(self, name: str, stmt: AssignmentStmt) -> bool:
- return True
-
def get_type_annotation(self, stmt: AssignmentStmt) -> TypeInfo | None:
if isinstance(stmt.rvalue, CallExpr):
# find the type arg in `attr.ib(type=str)`
@@ -741,58 +736,50 @@
def find_attr_initializers(
- builder: IRBuilder, cdef: ClassDef, skip: Callable[[str, AssignmentStmt], bool] | None = None
+ builder: IRBuilder, cdef: ClassDef
) -> tuple[set[str], list[tuple[AssignmentStmt, str]]]:
"""Find initializers of attributes in a class body.
- If provided, the skip arg should be a callable which will return whether
- to skip generating a default for an attribute. It will be passed the name of
- the attribute and the corresponding AssignmentStmt.
+ Under separate compilation, only this class's own body is walked, and
+ generate_attr_defaults_init emits a runtime call to the parent's
+ __mypyc_defaults_setup so inherited defaults are produced by chaining,
+ not by inlining. Walking the MRO here would break under separate=True
+ with mypy's incremental cache: a base class loaded from the cache has
+ an empty ClassDef.defs.body (mypy/nodes.py::ClassDef.serialize doesn't
+ serialize the class body), so inherited assignments would be silently
+ dropped and the subclass's __mypyc_defaults_setup would leave inherited
+ slots in the "undefined" state at runtime.
+
+ Without separate compilation, all modules are parsed in the same pass
+ and the MRO walk is safe; we keep the original inline-all behavior
+ there as an optimization (no chain call needed for instance creation).
"""
cls = builder.mapper.type_to_ir[cdef.info]
if cls.builtin_base:
return set(), []
- attrs_with_defaults = set()
-
- # Pull out all assignments in classes in the mro so we can initialize them
- # TODO: Support nested statements
+ cls_type = dataclass_type(cdef)
+ attrs_with_defaults: set[str] = set()
default_assignments: list[tuple[AssignmentStmt, str]] = []
- for info in reversed(cdef.info.mro):
- if info not in builder.mapper.type_to_ir:
+
+ # TODO: Support nested statements
+ if builder.options.separate:
+ infos: list[TypeInfo] = [cdef.info]
+ else:
+ infos = list(reversed(cdef.info.mro))
+
+ for info in infos:
+ info_ir = builder.mapper.type_to_ir.get(info)
+ if info_ir is None:
continue
for stmt in info.defn.defs.body:
- if (
- isinstance(stmt, AssignmentStmt)
- and isinstance(stmt.lvalues[0], NameExpr)
- and not is_class_var(stmt.lvalues[0])
- and not isinstance(stmt.rvalue, TempNode)
- ):
- name = stmt.lvalues[0].name
- if name == "__slots__":
- continue
-
- if name == "__deletable__":
- check_deletable_declaration(builder, cls, stmt.line)
- continue
-
- if skip is not None and skip(name, stmt):
- continue
-
- attr_type = cls.attr_type(name)
-
- # If the attribute is initialized to None and type isn't optional,
- # doesn't initialize it to anything (special case for "# type:" comments).
- if isinstance(stmt.rvalue, RefExpr) and stmt.rvalue.fullname == "builtins.None":
- if (
- not is_optional_type(attr_type)
- and not is_object_rprimitive(attr_type)
- and not is_none_rprimitive(attr_type)
- ):
- continue
-
- attrs_with_defaults.add(name)
- default_assignments.append((stmt, info.module_name))
+ if not isinstance(stmt, AssignmentStmt):
+ continue
+ name = default_attr_name(stmt, info_ir, cls_type)
+ if name is None:
+ continue
+ attrs_with_defaults.add(name)
+ default_assignments.append((stmt, info.module_name))
return attrs_with_defaults, default_assignments
@@ -800,15 +787,49 @@
def generate_attr_defaults_init(
builder: IRBuilder, cdef: ClassDef, default_assignments: list[tuple[AssignmentStmt, str]]
) -> None:
- """Generate an initialization method for default attr values (from class vars)."""
- if not default_assignments:
- return
+ """Generate an initialization method for default attr values (from class vars).
+
+ Under separate compilation, the emitted __mypyc_defaults_setup chains to
+ the nearest ancestor that has the method (Python __init__ style), then
+ sets only this class's own defaults; inherited defaults are produced by
+ the chain at runtime. The ancestor lookup uses cls.mro[1:] and relies on
+ prepare.py having registered the FuncDecl on every class that needs one
+ before any IR build runs. IR build within a compilation group proceeds
+ in filename order, so this class may be IR-built before its base, and a
+ method_decls lookup that depended on the base having been IR-built first
+ would miss. Without separate compilation, find_attr_initializers has
+ already collected the full MRO's defaults into default_assignments, so
+ we inline them all as before.
+ """
cls = builder.mapper.type_to_ir[cdef.info]
if cls.builtin_base:
return
- with builder.enter_method(cls, "__mypyc_defaults_setup", bool_rprimitive):
+ parent_with_defaults: ClassIR | None = None
+ if builder.options.separate:
+ for ancestor in cls.mro[1:]:
+ if MYPYC_DEFAULTS_SETUP in ancestor.method_decls:
+ parent_with_defaults = ancestor
+ break
+
+ if not default_assignments and parent_with_defaults is None:
+ return
+
+ with builder.enter_method(cls, MYPYC_DEFAULTS_SETUP, bool_rprimitive):
self_var = builder.self()
+
+ # Chain to parent's setup so inherited defaults run first; propagate
+ # its False return so a parent default that raised still aborts
+ # instance creation rather than being silently swallowed here.
+ if parent_with_defaults is not None:
+ decl = parent_with_defaults.method_decl(MYPYC_DEFAULTS_SETUP)
+ parent_ok = builder.builder.call(decl, [self_var], [ARG_POS], [None], cdef.line)
+ fail_block, continue_block = BasicBlock(), BasicBlock()
+ builder.add(Branch(parent_ok, continue_block, fail_block, Branch.BOOL))
+ builder.activate_block(fail_block)
+ builder.add(Return(builder.false()))
+ builder.activate_block(continue_block)
+
for stmt, origin_module in default_assignments:
lvalue = stmt.lvalues[0]
assert isinstance(lvalue, NameExpr), lvalue
@@ -833,26 +854,6 @@
builder.add(Return(builder.true()))
-def check_deletable_declaration(builder: IRBuilder, cl: ClassIR, line: int) -> None:
- for attr in cl.deletable:
- if attr not in cl.attributes:
- if not cl.has_attr(attr):
- builder.error(f'Attribute "{attr}" not defined', line)
- continue
- for base in cl.mro:
- if attr in base.property_types:
- builder.error(f'Cannot make property "{attr}" deletable', line)
- break
- else:
- _, base = cl.attr_details(attr)
- builder.error(
- ('Attribute "{}" not defined in "{}" ' + '(defined in "{}")').format(
- attr, cl.name, base.name
- ),
- line,
- )
-
-
def create_ne_from_eq(builder: IRBuilder, cdef: ClassDef) -> None:
"""Create a "__ne__" method from a "__eq__" method (if only latter exists)."""
cls = builder.mapper.type_to_ir[cdef.info]
diff --git a/mypyc/irbuild/prepare.py b/mypyc/irbuild/prepare.py
index f143ce1..8b73b10 100644
--- a/mypyc/irbuild/prepare.py
+++ b/mypyc/irbuild/prepare.py
@@ -21,6 +21,7 @@
from mypy.nodes import (
ARG_STAR,
ARG_STAR2,
+ AssignmentStmt,
CallExpr,
ClassDef,
Decorator,
@@ -39,7 +40,13 @@
from mypy.semanal import refers_to_fullname
from mypy.traverser import TraverserVisitor
from mypy.types import Instance, Type, get_proper_type
-from mypyc.common import FAST_PREFIX, PROPSET_PREFIX, SELF_NAME, get_id_from_name
+from mypyc.common import (
+ FAST_PREFIX,
+ MYPYC_DEFAULTS_SETUP,
+ PROPSET_PREFIX,
+ SELF_NAME,
+ get_id_from_name,
+)
from mypyc.crash import catch_errors
from mypyc.errors import Errors
from mypyc.ir.class_ir import ClassIR
@@ -55,6 +62,7 @@
from mypyc.ir.rtypes import (
RInstance,
RType,
+ bool_rprimitive,
dict_rprimitive,
none_rprimitive,
object_pointer_rprimitive,
@@ -63,6 +71,8 @@
)
from mypyc.irbuild.mapper import Mapper
from mypyc.irbuild.util import (
+ dataclass_type,
+ default_attr_name,
get_func_def,
get_mypyc_attrs,
is_dataclass,
@@ -131,6 +141,24 @@
if class_ir.is_ext_class:
prepare_implicit_property_accessors(cdef.info, class_ir, module.fullname, mapper)
+ # Register __mypyc_defaults_setup FuncDecls on classes that have their own
+ # class-level default attribute assignments. Done here, before any IR build
+ # runs, so that the cross-class lookup in generate_attr_defaults_init is
+ # order-independent: IR build within a compilation group proceeds in
+ # filename order, so a subclass may be IR-built before its base.
+ for module, cdef in classes:
+ class_ir = mapper.type_to_ir[cdef.info]
+ if class_ir.is_ext_class and _has_own_default_attrs(cdef, class_ir):
+ _register_defaults_setup_decl(class_ir, module.fullname)
+
+ # Validate __deletable__ declarations. Done here so the compiler exits
+ # early on invalid input before any IR is built.
+ for module, cdef in classes:
+ class_ir = mapper.type_to_ir[cdef.info]
+ if class_ir.is_ext_class:
+ with catch_errors(module.path, cdef.line):
+ _check_deletable_declarations(module.path, cdef, class_ir, errors)
+
# Collect all the functions also. We collect from the symbol table
# so that we can easily pick out the right copy of a function that
# is conditionally defined. This doesn't include nested functions!
@@ -408,6 +436,68 @@
)
+def _has_own_default_attrs(cdef: ClassDef, ir: ClassIR) -> bool:
+ """Whether this class's own body has any default attribute assignment
+ that would be emitted into __mypyc_defaults_setup.
+
+ Used during prepare to decide whether to register a
+ __mypyc_defaults_setup FuncDecl ahead of IR build.
+ """
+ if ir.builtin_base or ir.is_trait:
+ return False
+ cls_type = dataclass_type(cdef)
+ return any(
+ default_attr_name(stmt, ir, cls_type) is not None
+ for stmt in cdef.info.defn.defs.body
+ if isinstance(stmt, AssignmentStmt)
+ )
+
+
+def _register_defaults_setup_decl(ir: ClassIR, module_name: str) -> None:
+ sig = FuncSignature([RuntimeArg(SELF_NAME, RInstance(ir))], bool_rprimitive)
+ ir.method_decls[MYPYC_DEFAULTS_SETUP] = FuncDecl(
+ MYPYC_DEFAULTS_SETUP, ir.name, module_name, sig
+ )
+
+
+def _check_deletable_declarations(path: str, cdef: ClassDef, ir: ClassIR, errors: Errors) -> None:
+ """Validate that attributes listed in __deletable__ refer to definable
+ attributes on the class.
+
+ Runs in the prepare phase so we exit early on invalid programs before
+ any IR is built.
+ """
+ if not ir.deletable:
+ return
+ line = next(
+ (
+ stmt.line
+ for stmt in cdef.info.defn.defs.body
+ if isinstance(stmt, AssignmentStmt)
+ and isinstance(stmt.lvalues[0], NameExpr)
+ and stmt.lvalues[0].name == "__deletable__"
+ ),
+ cdef.line,
+ )
+ for attr in ir.deletable:
+ if attr not in ir.attributes:
+ if not ir.has_attr(attr):
+ errors.error(f'Attribute "{attr}" not defined', path, line)
+ continue
+ for base in ir.mro:
+ if attr in base.property_types:
+ errors.error(f'Cannot make property "{attr}" deletable', path, line)
+ break
+ else:
+ _, base = ir.attr_details(attr)
+ errors.error(
+ f'Attribute "{attr}" not defined in "{ir.name}" '
+ f'(defined in "{base.name}")',
+ path,
+ line,
+ )
+
+
def prepare_class_def(
path: str,
module_name: str,
diff --git a/mypyc/irbuild/util.py b/mypyc/irbuild/util.py
index 5eda51a..a6f793c 100644
--- a/mypyc/irbuild/util.py
+++ b/mypyc/irbuild/util.py
@@ -12,6 +12,7 @@
ARG_POS,
GDEF,
ArgKind,
+ AssignmentStmt,
BytesExpr,
CallExpr,
ClassDef,
@@ -24,13 +25,17 @@
OverloadedFuncDef,
RefExpr,
StrExpr,
+ TempNode,
TupleExpr,
UnaryExpr,
Var,
+ is_class_var,
)
from mypy.semanal import refers_to_fullname
from mypy.types import FINAL_DECORATOR_NAMES
from mypyc.errors import Errors
+from mypyc.ir.class_ir import ClassIR
+from mypyc.ir.rtypes import is_none_rprimitive, is_object_rprimitive, is_optional_type
MYPYC_ATTRS: Final[frozenset[MypycAttr]] = frozenset(
["native_class", "allow_interpreted_subclasses", "serializable", "free_list_len", "acyclic"]
@@ -102,6 +107,50 @@
return None
+def _defaults_skip(stmt: AssignmentStmt, cls_type: str | None) -> bool:
+ """Whether a class-level default assignment is skipped when emitting
+ __mypyc_defaults_setup, based on class type.
+
+ - attr (auto_attribs=False): skip all (handled by attr.ib machinery).
+ - dataclasses / attr-auto: skip annotated assignments.
+ - regular extension class: skip nothing.
+ """
+ if cls_type == "attr":
+ return True
+ if cls_type in ("dataclasses", "attr-auto"):
+ return stmt.type is not None
+ return False
+
+
+def default_attr_name(stmt: AssignmentStmt, ir: ClassIR, cls_type: str | None) -> str | None:
+ """Return the attribute name if `stmt` is a class-level default assignment
+ that __mypyc_defaults_setup should emit; otherwise None.
+
+ Single source of truth for the predicate used by both
+ mypyc.irbuild.classdef.find_attr_initializers (IR build) and
+ mypyc.irbuild.prepare._has_own_default_attrs (prepare-phase decl registration).
+ """
+ lvalue = stmt.lvalues[0]
+ if not isinstance(lvalue, NameExpr) or is_class_var(lvalue):
+ return None
+ if isinstance(stmt.rvalue, TempNode):
+ return None
+ name = lvalue.name
+ if name in ("__slots__", "__deletable__") or name not in ir.attributes:
+ return None
+ if _defaults_skip(stmt, cls_type):
+ return None
+ if isinstance(stmt.rvalue, RefExpr) and stmt.rvalue.fullname == "builtins.None":
+ attr_type = ir.attributes[name]
+ if (
+ not is_optional_type(attr_type)
+ and not is_object_rprimitive(attr_type)
+ and not is_none_rprimitive(attr_type)
+ ):
+ return None
+ return name
+
+
def get_mypyc_attr_literal(e: Expression) -> Any:
"""Convert an expression from a mypyc_attr decorator to a value.
diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test
index d13bd95..a7fdc09 100644
--- a/mypyc/test-data/irbuild-classes.test
+++ b/mypyc/test-data/irbuild-classes.test
@@ -1135,7 +1135,7 @@
__deletable__ = ['x']
x: int
-[case testInvalidDeletableAttribute]
+[case testDeleteNonDeletableAttribute]
class NotDeletable:
__deletable__ = ['x']
x: int
@@ -1146,6 +1146,7 @@
del o.y # E: "y" cannot be deleted \
# N: Using "__deletable__ = ['<attr>']" in the class body enables "del obj.<attr>"
+[case testInvalidDeletableAttribute]
class Base:
x: int
diff --git a/mypyc/test-data/run-multimodule.test b/mypyc/test-data/run-multimodule.test
index ace1ab9..e586a3c 100644
--- a/mypyc/test-data/run-multimodule.test
+++ b/mypyc/test-data/run-multimodule.test
@@ -1813,3 +1813,86 @@
[out2]
empty
hello
+
+[case testIncrementalCrossModuleInheritedAttrDefaultsWithOverride]
+# Regression: same shape as testIncrementalCrossModuleInheritedAttrDefaults,
+# but the subclass adds an attribute of its own, so generate_attr_defaults_init
+# emits a __mypyc_defaults_setup for it. Before the fix, the recompiled
+# subclass walked the parent's ClassDef.defs.body to collect inherited
+# defaults; when the parent was loaded from mypy's incremental cache that
+# body was empty, so the inherited initialization was dropped and any
+# access to an inherited attribute through compiled code raised
+# "AttributeError: attribute '<name>' of '<base>' undefined".
+import other_a
+
+def test() -> None:
+ c = other_a.Child()
+ # Inherited attributes must still be initialized after the subclass
+ # has been recompiled against a cache-loaded parent.
+ assert c.x == 1
+ assert c.y == "hello"
+ # Own override is set by the subclass's own __mypyc_defaults_setup.
+ assert c.z is True
+ # Method defined on the parent reads an inherited attribute through
+ # the compiled path; this is what crashes pre-fix.
+ assert c.use() == 1
+
+[file other_b.py]
+class Parent:
+ x: int = 1
+ y: str = "hello"
+ z: bool = False
+
+ def use(self) -> int:
+ if self.x:
+ return 1
+ return 0
+
+[file other_a.py]
+from other_b import Parent
+
+class Child(Parent):
+ z: bool = True
+
+[file other_a.py.2]
+from other_b import Parent
+
+class Child(Parent):
+ z: bool = True
+
+def _force_recompile() -> int:
+ return 1
+
+[file driver.py]
+from native import test
+test()
+
+[case testCrossModuleInheritedAttrDefaultsSameGroup]
+# separate: [(["native.py"], "grp1"), (["other_a.py", "other_b.py"], "grp2")]
+# Regression: with the subclass (other_a) and base (other_b) in the same
+# compilation group, IR build runs alphabetically within the group, so
+# the subclass is IR-built before the base. The decision to emit
+# __mypyc_defaults_setup (and a chained call to the ancestor's) must be
+# set up in the prepare phase, before any IR build runs; otherwise the
+# subclass's lookup of the parent's setup decl misses and inherited
+# defaults are lost on a fresh build.
+import other_a
+
+def test() -> None:
+ c = other_a.Child()
+ assert c.x == 1
+ assert c.z is True
+
+[file other_b.py]
+class Parent:
+ x: int = 1
+
+[file other_a.py]
+from other_b import Parent
+
+class Child(Parent):
+ z: bool = True
+
+[file driver.py]
+from native import test
+test()