From ff66bf33e3427747815a11dabda90b72d39164e9 Mon Sep 17 00:00:00 2001 From: vaggelisd Date: Thu, 12 Mar 2026 15:15:28 +0200 Subject: [PATCH 1/2] [mypyc] Fix allow_interpreted_subclasses not seeing subclass attribute overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- mypyc/codegen/emitclass.py | 3 +- mypyc/codegen/emitfunc.py | 36 ++++++++++++ mypyc/lib-rt/mypyc_util.h | 5 ++ mypyc/test-data/run-classes.test | 99 ++++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 1 deletion(-) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index 26bf189694f97..27980d72d373d 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -379,7 +379,8 @@ def emit_line() -> None: generate_methods_table(cl, methods_name, setup_name if generate_full else None, emitter) emit_line() - flags = ["Py_TPFLAGS_DEFAULT", "Py_TPFLAGS_HEAPTYPE", "Py_TPFLAGS_BASETYPE"] + flags = ["Py_TPFLAGS_DEFAULT", "Py_TPFLAGS_HEAPTYPE", "Py_TPFLAGS_BASETYPE", + "CPy_TPFLAGS_MYPYC_COMPILED"] if generate_full and not cl.is_acyclic: flags.append("Py_TPFLAGS_HAVE_GC") if cl.has_method("__call__"): diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index c1202d1c928ca..d10f2fa22c710 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -404,6 +404,39 @@ def visit_get_attr(self, op: GetAttr) -> None: ) else: # Otherwise, use direct or offset struct access. + # For classes with allow_interpreted_subclasses, an interpreted + # subclass may override class attributes in its __dict__. The + # compiled code reads from instance struct slots, so we check if + # the instance is a compiled type (via tp_flags). If not, fall + # back to Python's generic attribute lookup which respects the MRO. + # We use the CPy_TPFLAGS_MYPYC_COMPILED flag (set on all mypyc-compiled + # types) so that compiled subclasses get direct struct access while only + # interpreted subclasses hit the slow path. + use_fallback = cl.allow_interpreted_subclasses and not cl.is_trait + if use_fallback: + fallback_attr = self.emitter.temp_name() + fallback_result = self.emitter.temp_name() + self.declarations.emit_line(f"PyObject *{fallback_attr};") + self.declarations.emit_line(f"PyObject *{fallback_result};") + self.emit_line( + f"if (!(Py_TYPE({obj})->tp_flags & CPy_TPFLAGS_MYPYC_COMPILED)) {{" + ) + self.emit_line( + f'{fallback_attr} = PyUnicode_FromString("{op.attr}");' + ) + self.emit_line( + f"{fallback_result} = PyObject_GenericGetAttr((PyObject *){obj}, {fallback_attr});" + ) + self.emit_line(f"Py_DECREF({fallback_attr});") + if attr_rtype.is_unboxed: + self.emitter.emit_unbox( + fallback_result, dest, attr_rtype, raise_exception=False + ) + self.emit_line(f"Py_XDECREF({fallback_result});") + else: + self.emit_line(f"{dest} = {fallback_result};") + self.emit_line("} else {") + attr_expr = self.get_attr_expr(obj, op, decl_cl) self.emitter.emit_line(f"{dest} = {attr_expr};") always_defined = cl.is_always_defined(op.attr) @@ -447,6 +480,9 @@ def visit_get_attr(self, op: GetAttr) -> None: elif not always_defined: self.emitter.emit_line("}") + if use_fallback: + self.emitter.emit_line("}") + def get_attr_with_allow_error_value(self, op: GetAttr) -> None: """Handle GetAttr with allow_error_value=True. diff --git a/mypyc/lib-rt/mypyc_util.h b/mypyc/lib-rt/mypyc_util.h index 6715d67d96572..cc41402bd76d7 100644 --- a/mypyc/lib-rt/mypyc_util.h +++ b/mypyc/lib-rt/mypyc_util.h @@ -146,6 +146,11 @@ typedef PyObject CPyModule; #define CPY_NONE_ERROR 2 #define CPY_NONE 1 +// Flag bit set on all mypyc-compiled types. Used to distinguish compiled +// subclasses (safe for direct struct access) from interpreted subclasses +// (need PyObject_GenericGetAttr fallback) in allow_interpreted_subclasses mode. +#define CPy_TPFLAGS_MYPYC_COMPILED (1UL << 20) + typedef void (*CPyVTableItem)(void); static inline CPyTagged CPyTagged_ShortFromInt(int x) { diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 54e568a477684..22f6f2c466919 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -5774,3 +5774,102 @@ from native import Concrete c = Concrete() assert c.value() == 42 assert c.derived() == 42 + +[case testInterpretedSubclassAttrOverrideWithAllowInterpretedSubclasses] +# Test that interpreted subclasses can override class attributes and the +# compiled base class methods see the overridden values via GenericGetAttr. +from mypy_extensions import mypyc_attr + +@mypyc_attr(allow_interpreted_subclasses=True) +class Base: + VALUE: int = 10 + FLAG: bool = False + + def get_value(self) -> int: + return self.VALUE + + def check_flag(self) -> bool: + return self.FLAG + +[file driver.py] +from native import Base + +# Interpreted subclass that overrides class attributes +class Sub(Base): + VALUE = 42 + FLAG = True + +b = Base() +assert b.get_value() == 10 +assert not b.check_flag() + +s = Sub() +assert s.get_value() == 42, "compiled method doesn't see subclass override" +assert s.check_flag(), "compiled method doesn't see subclass override" + +[case testCompiledSubclassAttrAccessWithAllowInterpretedSubclasses] +# Test that compiled subclasses of a class with allow_interpreted_subclasses=True +# can correctly access parent instance attributes via direct struct access +# (not falling back to PyObject_GenericGetAttr). +from mypy_extensions import mypyc_attr + +@mypyc_attr(allow_interpreted_subclasses=True) +class Base: + def __init__(self, x: int, name: str) -> None: + self.x = x + self.name = name + + def get_x(self) -> int: + return self.x + + def get_name(self) -> str: + return self.name + + def compute(self) -> int: + return self.x * 2 + +@mypyc_attr(allow_interpreted_subclasses=True) +class Child(Base): + def __init__(self, x: int, name: str, y: int) -> None: + super().__init__(x, name) + self.y = y + + def compute(self) -> int: + return self.x + self.y + + def get_both(self) -> int: + return self.x + self.y + +@mypyc_attr(allow_interpreted_subclasses=True) +class GrandChild(Child): + def __init__(self, x: int, name: str, y: int, z: int) -> None: + super().__init__(x, name, y) + self.z = z + + def compute(self) -> int: + return self.x + self.y + self.z + +def test_compiled_subclass_attr_access() -> None: + b = Base(10, "base") + assert b.get_x() == 10 + assert b.get_name() == "base" + assert b.compute() == 20 + + c = Child(10, "child", 5) + assert c.get_x() == 10 + assert c.get_name() == "child" + assert c.compute() == 15 + assert c.get_both() == 15 + + g = GrandChild(10, "grand", 5, 3) + assert g.get_x() == 10 + assert g.get_name() == "grand" + assert g.compute() == 18 + + ref: Base = Child(7, "ref", 3) + assert ref.get_x() == 7 + assert ref.compute() == 10 + +[file driver.py] +from native import test_compiled_subclass_attr_access +test_compiled_subclass_attr_access() From 6ac30b03ded78ac2655f551f75d348e1fdeb57ac Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:22:29 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypyc/codegen/emitclass.py | 8 ++++++-- mypyc/codegen/emitfunc.py | 8 ++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index 27980d72d373d..d7ff730151fc8 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -379,8 +379,12 @@ def emit_line() -> None: generate_methods_table(cl, methods_name, setup_name if generate_full else None, emitter) emit_line() - flags = ["Py_TPFLAGS_DEFAULT", "Py_TPFLAGS_HEAPTYPE", "Py_TPFLAGS_BASETYPE", - "CPy_TPFLAGS_MYPYC_COMPILED"] + flags = [ + "Py_TPFLAGS_DEFAULT", + "Py_TPFLAGS_HEAPTYPE", + "Py_TPFLAGS_BASETYPE", + "CPy_TPFLAGS_MYPYC_COMPILED", + ] if generate_full and not cl.is_acyclic: flags.append("Py_TPFLAGS_HAVE_GC") if cl.has_method("__call__"): diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index d10f2fa22c710..804924732ca5d 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -418,12 +418,8 @@ def visit_get_attr(self, op: GetAttr) -> None: fallback_result = self.emitter.temp_name() self.declarations.emit_line(f"PyObject *{fallback_attr};") self.declarations.emit_line(f"PyObject *{fallback_result};") - self.emit_line( - f"if (!(Py_TYPE({obj})->tp_flags & CPy_TPFLAGS_MYPYC_COMPILED)) {{" - ) - self.emit_line( - f'{fallback_attr} = PyUnicode_FromString("{op.attr}");' - ) + self.emit_line(f"if (!(Py_TYPE({obj})->tp_flags & CPy_TPFLAGS_MYPYC_COMPILED)) {{") + self.emit_line(f'{fallback_attr} = PyUnicode_FromString("{op.attr}");') self.emit_line( f"{fallback_result} = PyObject_GenericGetAttr((PyObject *){obj}, {fallback_attr});" )