Skip to content

Commit ff66bf3

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 537740b commit ff66bf3

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
@@ -5774,3 +5774,102 @@ from native import Concrete
57745774
c = Concrete()
57755775
assert c.value() == 42
57765776
assert c.derived() == 42
5777+
5778+
[case testInterpretedSubclassAttrOverrideWithAllowInterpretedSubclasses]
5779+
# Test that interpreted subclasses can override class attributes and the
5780+
# compiled base class methods see the overridden values via GenericGetAttr.
5781+
from mypy_extensions import mypyc_attr
5782+
5783+
@mypyc_attr(allow_interpreted_subclasses=True)
5784+
class Base:
5785+
VALUE: int = 10
5786+
FLAG: bool = False
5787+
5788+
def get_value(self) -> int:
5789+
return self.VALUE
5790+
5791+
def check_flag(self) -> bool:
5792+
return self.FLAG
5793+
5794+
[file driver.py]
5795+
from native import Base
5796+
5797+
# Interpreted subclass that overrides class attributes
5798+
class Sub(Base):
5799+
VALUE = 42
5800+
FLAG = True
5801+
5802+
b = Base()
5803+
assert b.get_value() == 10
5804+
assert not b.check_flag()
5805+
5806+
s = Sub()
5807+
assert s.get_value() == 42, "compiled method doesn't see subclass override"
5808+
assert s.check_flag(), "compiled method doesn't see subclass override"
5809+
5810+
[case testCompiledSubclassAttrAccessWithAllowInterpretedSubclasses]
5811+
# Test that compiled subclasses of a class with allow_interpreted_subclasses=True
5812+
# can correctly access parent instance attributes via direct struct access
5813+
# (not falling back to PyObject_GenericGetAttr).
5814+
from mypy_extensions import mypyc_attr
5815+
5816+
@mypyc_attr(allow_interpreted_subclasses=True)
5817+
class Base:
5818+
def __init__(self, x: int, name: str) -> None:
5819+
self.x = x
5820+
self.name = name
5821+
5822+
def get_x(self) -> int:
5823+
return self.x
5824+
5825+
def get_name(self) -> str:
5826+
return self.name
5827+
5828+
def compute(self) -> int:
5829+
return self.x * 2
5830+
5831+
@mypyc_attr(allow_interpreted_subclasses=True)
5832+
class Child(Base):
5833+
def __init__(self, x: int, name: str, y: int) -> None:
5834+
super().__init__(x, name)
5835+
self.y = y
5836+
5837+
def compute(self) -> int:
5838+
return self.x + self.y
5839+
5840+
def get_both(self) -> int:
5841+
return self.x + self.y
5842+
5843+
@mypyc_attr(allow_interpreted_subclasses=True)
5844+
class GrandChild(Child):
5845+
def __init__(self, x: int, name: str, y: int, z: int) -> None:
5846+
super().__init__(x, name, y)
5847+
self.z = z
5848+
5849+
def compute(self) -> int:
5850+
return self.x + self.y + self.z
5851+
5852+
def test_compiled_subclass_attr_access() -> None:
5853+
b = Base(10, "base")
5854+
assert b.get_x() == 10
5855+
assert b.get_name() == "base"
5856+
assert b.compute() == 20
5857+
5858+
c = Child(10, "child", 5)
5859+
assert c.get_x() == 10
5860+
assert c.get_name() == "child"
5861+
assert c.compute() == 15
5862+
assert c.get_both() == 15
5863+
5864+
g = GrandChild(10, "grand", 5, 3)
5865+
assert g.get_x() == 10
5866+
assert g.get_name() == "grand"
5867+
assert g.compute() == 18
5868+
5869+
ref: Base = Child(7, "ref", 3)
5870+
assert ref.get_x() == 7
5871+
assert ref.compute() == 10
5872+
5873+
[file driver.py]
5874+
from native import test_compiled_subclass_attr_access
5875+
test_compiled_subclass_attr_access()

0 commit comments

Comments
 (0)