Skip to content

Commit 4b63a80

Browse files
committed
[mypyc] Fix allow_interpreted_subclasses not seeing subclass attribute overrides
When a compiled class with allow_interpreted_subclasses=True has methods that access self.ATTR via direct C struct slots, interpreted subclasses that override ATTR in their class __dict__ are ignored — the compiled method always reads the base class default from the slot. Fix: in visit_get_attr for non-property attribute access, check if the instance is a mypyc-compiled type (via a new CPy_TPFLAGS_MYPYC_COMPILED tp_flags bit). If not, fall back to PyObject_GenericGetAttr which respects the MRO and finds the subclass override. Using tp_flags rather than an exact type check ensures compiled subclasses retain fast direct struct access, while only interpreted subclasses hit the GenericGetAttr slow path. For unboxed types (bool, int), the PyObject* result is unboxed to the expected C type.
1 parent acfef9c commit 4b63a80

4 files changed

Lines changed: 142 additions & 1 deletion

File tree

mypyc/codegen/emitclass.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,8 @@ def emit_line() -> None:
379379
generate_methods_table(cl, methods_name, setup_name if generate_full else None, emitter)
380380
emit_line()
381381

382-
flags = ["Py_TPFLAGS_DEFAULT", "Py_TPFLAGS_HEAPTYPE", "Py_TPFLAGS_BASETYPE"]
382+
flags = ["Py_TPFLAGS_DEFAULT", "Py_TPFLAGS_HEAPTYPE", "Py_TPFLAGS_BASETYPE",
383+
"CPy_TPFLAGS_MYPYC_COMPILED"]
383384
if generate_full and not cl.is_acyclic:
384385
flags.append("Py_TPFLAGS_HAVE_GC")
385386
if cl.has_method("__call__"):

mypyc/codegen/emitfunc.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,39 @@ def visit_get_attr(self, op: GetAttr) -> None:
404404
)
405405
else:
406406
# Otherwise, use direct or offset struct access.
407+
# For classes with allow_interpreted_subclasses, an interpreted
408+
# subclass may override class attributes in its __dict__. The
409+
# compiled code reads from instance struct slots, so we check if
410+
# the instance is a compiled type (via tp_flags). If not, fall
411+
# back to Python's generic attribute lookup which respects the MRO.
412+
# We use the CPy_TPFLAGS_MYPYC_COMPILED flag (set on all mypyc-compiled
413+
# types) so that compiled subclasses get direct struct access while only
414+
# interpreted subclasses hit the slow path.
415+
use_fallback = cl.allow_interpreted_subclasses and not cl.is_trait
416+
if use_fallback:
417+
fallback_attr = self.emitter.temp_name()
418+
fallback_result = self.emitter.temp_name()
419+
self.declarations.emit_line(f"PyObject *{fallback_attr};")
420+
self.declarations.emit_line(f"PyObject *{fallback_result};")
421+
self.emit_line(
422+
f"if (!(Py_TYPE({obj})->tp_flags & CPy_TPFLAGS_MYPYC_COMPILED)) {{"
423+
)
424+
self.emit_line(
425+
f'{fallback_attr} = PyUnicode_FromString("{op.attr}");'
426+
)
427+
self.emit_line(
428+
f"{fallback_result} = PyObject_GenericGetAttr((PyObject *){obj}, {fallback_attr});"
429+
)
430+
self.emit_line(f"Py_DECREF({fallback_attr});")
431+
if attr_rtype.is_unboxed:
432+
self.emitter.emit_unbox(
433+
fallback_result, dest, attr_rtype, raise_exception=False
434+
)
435+
self.emit_line(f"Py_XDECREF({fallback_result});")
436+
else:
437+
self.emit_line(f"{dest} = {fallback_result};")
438+
self.emit_line("} else {")
439+
407440
attr_expr = self.get_attr_expr(obj, op, decl_cl)
408441
self.emitter.emit_line(f"{dest} = {attr_expr};")
409442
always_defined = cl.is_always_defined(op.attr)
@@ -447,6 +480,9 @@ def visit_get_attr(self, op: GetAttr) -> None:
447480
elif not always_defined:
448481
self.emitter.emit_line("}")
449482

483+
if use_fallback:
484+
self.emitter.emit_line("}")
485+
450486
def get_attr_with_allow_error_value(self, op: GetAttr) -> None:
451487
"""Handle GetAttr with allow_error_value=True.
452488

mypyc/lib-rt/mypyc_util.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ typedef PyObject CPyModule;
146146
#define CPY_NONE_ERROR 2
147147
#define CPY_NONE 1
148148

149+
// Flag bit set on all mypyc-compiled types. Used to distinguish compiled
150+
// subclasses (safe for direct struct access) from interpreted subclasses
151+
// (need PyObject_GenericGetAttr fallback) in allow_interpreted_subclasses mode.
152+
#define CPy_TPFLAGS_MYPYC_COMPILED (1UL << 20)
153+
149154
typedef void (*CPyVTableItem)(void);
150155

151156
static inline CPyTagged CPyTagged_ShortFromInt(int x) {

mypyc/test-data/run-classes.test

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5890,3 +5890,102 @@ assert NonExtDict.BASE == {"x": 1}
58905890
assert NonExtDict.EXTENDED == {"x": 1, "y": 2}
58915891

58925892
assert NonExtChained.Z == {10, 20, 30}
5893+
5894+
[case testInterpretedSubclassAttrOverrideWithAllowInterpretedSubclasses]
5895+
# Test that interpreted subclasses can override class attributes and the
5896+
# compiled base class methods see the overridden values via GenericGetAttr.
5897+
from mypy_extensions import mypyc_attr
5898+
5899+
@mypyc_attr(allow_interpreted_subclasses=True)
5900+
class Base:
5901+
VALUE: int = 10
5902+
FLAG: bool = False
5903+
5904+
def get_value(self) -> int:
5905+
return self.VALUE
5906+
5907+
def check_flag(self) -> bool:
5908+
return self.FLAG
5909+
5910+
[file driver.py]
5911+
from native import Base
5912+
5913+
# Interpreted subclass that overrides class attributes
5914+
class Sub(Base):
5915+
VALUE = 42
5916+
FLAG = True
5917+
5918+
b = Base()
5919+
assert b.get_value() == 10
5920+
assert not b.check_flag()
5921+
5922+
s = Sub()
5923+
assert s.get_value() == 42, "compiled method doesn't see subclass override"
5924+
assert s.check_flag(), "compiled method doesn't see subclass override"
5925+
5926+
[case testCompiledSubclassAttrAccessWithAllowInterpretedSubclasses]
5927+
# Test that compiled subclasses of a class with allow_interpreted_subclasses=True
5928+
# can correctly access parent instance attributes via direct struct access
5929+
# (not falling back to PyObject_GenericGetAttr).
5930+
from mypy_extensions import mypyc_attr
5931+
5932+
@mypyc_attr(allow_interpreted_subclasses=True)
5933+
class Base:
5934+
def __init__(self, x: int, name: str) -> None:
5935+
self.x = x
5936+
self.name = name
5937+
5938+
def get_x(self) -> int:
5939+
return self.x
5940+
5941+
def get_name(self) -> str:
5942+
return self.name
5943+
5944+
def compute(self) -> int:
5945+
return self.x * 2
5946+
5947+
@mypyc_attr(allow_interpreted_subclasses=True)
5948+
class Child(Base):
5949+
def __init__(self, x: int, name: str, y: int) -> None:
5950+
super().__init__(x, name)
5951+
self.y = y
5952+
5953+
def compute(self) -> int:
5954+
return self.x + self.y
5955+
5956+
def get_both(self) -> int:
5957+
return self.x + self.y
5958+
5959+
@mypyc_attr(allow_interpreted_subclasses=True)
5960+
class GrandChild(Child):
5961+
def __init__(self, x: int, name: str, y: int, z: int) -> None:
5962+
super().__init__(x, name, y)
5963+
self.z = z
5964+
5965+
def compute(self) -> int:
5966+
return self.x + self.y + self.z
5967+
5968+
def test_compiled_subclass_attr_access() -> None:
5969+
b = Base(10, "base")
5970+
assert b.get_x() == 10
5971+
assert b.get_name() == "base"
5972+
assert b.compute() == 20
5973+
5974+
c = Child(10, "child", 5)
5975+
assert c.get_x() == 10
5976+
assert c.get_name() == "child"
5977+
assert c.compute() == 15
5978+
assert c.get_both() == 15
5979+
5980+
g = GrandChild(10, "grand", 5, 3)
5981+
assert g.get_x() == 10
5982+
assert g.get_name() == "grand"
5983+
assert g.compute() == 18
5984+
5985+
ref: Base = Child(7, "ref", 3)
5986+
assert ref.get_x() == 7
5987+
assert ref.compute() == 10
5988+
5989+
[file driver.py]
5990+
from native import test_compiled_subclass_attr_access
5991+
test_compiled_subclass_attr_access()

0 commit comments

Comments
 (0)