diff --git a/mypyc/irbuild/function.py b/mypyc/irbuild/function.py index 7303f561d60c..0cf0edb32f86 100644 --- a/mypyc/irbuild/function.py +++ b/mypyc/irbuild/function.py @@ -540,7 +540,11 @@ def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None # children. if class_ir.allow_interpreted_subclasses: f = gen_glue(builder, func_ir.sig, func_ir, class_ir, class_ir, fdef, do_py_ops=True) - class_ir.glue_methods[(class_ir, name)] = f + # Use func_ir.decl.name (unique) rather than fdef.name, because for properties + # the getter and setter share the same fdef.name but have distinct decl names + # (e.g. "prop" vs "__mypyc_setter__prop"). Using fdef.name would cause the + # setter's glue to overwrite the getter's glue in the shadow vtable. + class_ir.glue_methods[(class_ir, func_ir.decl.name)] = f builder.functions.append(f) if fdef.name == "__getattr__": @@ -653,8 +657,9 @@ def gen_glue( """ if fdef.is_property: return gen_glue_property(builder, base_sig, target, cls, base, fdef.line, do_py_ops) - else: - return gen_glue_method(builder, base_sig, target, cls, base, fdef.line, do_py_ops) + if do_py_ops and target.name.startswith(PROPSET_PREFIX): + return gen_glue_property_setter(builder, base_sig, target, cls, base, fdef.line) + return gen_glue_method(builder, base_sig, target, cls, base, fdef.line, do_py_ops) class ArgInfo(NamedTuple): @@ -846,6 +851,56 @@ def gen_glue_property( ) +def gen_glue_property_setter( + builder: IRBuilder, sig: FuncSignature, target: FuncIR, cls: ClassIR, base: ClassIR, line: int +) -> FuncIR: + """Generate a shadow glue method for a property setter. + + For interpreted subclasses, property setters can't be called via the + internal __mypyc_setter__ method. Instead, use Python's setattr + to set the property via the standard descriptor protocol. + """ + builder.enter() + builder.ret_types[-1] = sig.ret_type + + rt_args = list(sig.args) + rt_args[0] = RuntimeArg(sig.args[0].name, RInstance(cls)) + + arg_info = get_args(builder, rt_args, line) + args = arg_info.args + + self_arg = args[0] + value_arg = args[1] + + # Extract the property name from "__mypyc_setter__" + assert target.name.startswith(PROPSET_PREFIX) + prop_name = target.name[len(PROPSET_PREFIX) :] + + builder.primitive_op( + py_setattr_op, + [ + self_arg, + builder.load_str(prop_name), + builder.coerce(value_arg, object_rprimitive, line), + ], + line, + ) + retval = builder.coerce(builder.none(), sig.ret_type, line) + builder.add(Return(retval)) + + arg_regs, _, blocks, return_type, _ = builder.leave() + return FuncIR( + FuncDecl( + target.name + "__" + base.name + "_glue", + cls.name, + builder.module_name, + FuncSignature(rt_args, return_type), + ), + arg_regs, + blocks, + ) + + def get_func_target(builder: IRBuilder, fdef: FuncDef) -> AssignmentTarget: """Given a FuncDef, return the target for the instance of its callable class. diff --git a/mypyc/test-data/irbuild-glue-methods.test b/mypyc/test-data/irbuild-glue-methods.test index 35e6be1283eb..87b3bf7fee58 100644 --- a/mypyc/test-data/irbuild-glue-methods.test +++ b/mypyc/test-data/irbuild-glue-methods.test @@ -453,3 +453,59 @@ class EE(E): main:7: error: An argument with type "i64" cannot be given a default value in a method override main:13: error: Incompatible argument type "i64" (base class has type "int") main:18: error: Incompatible argument type "int" (base class has type "i64") + +[case testPropertySetterShadowGlue] +import mypy_extensions + +@mypy_extensions.mypyc_attr(allow_interpreted_subclasses=True) +class Base: + _x: str = "" + + @property + def x(self) -> str: + return self._x + + @x.setter + def x(self, v: str) -> None: + self._x = v +[out] +def Base.x(self): + self :: __main__.Base + r0 :: str +L0: + r0 = self._x + return r0 +def Base.x__Base_glue(__mypyc_self__): + __mypyc_self__ :: __main__.Base + r0 :: str + r1 :: object + r2 :: str +L0: + r0 = 'x' + r1 = CPyObject_GetAttr(__mypyc_self__, r0) + r2 = cast(str, r1) + return r2 +def Base.__mypyc_setter__x(self, v): + self :: __main__.Base + v :: str + r0 :: bool +L0: + self._x = v; r0 = is_error + return 1 +def Base.__mypyc_setter__x__Base_glue(self, v): + self :: __main__.Base + v, r0 :: str + r1 :: i32 + r2 :: bit +L0: + r0 = 'x' + r1 = PyObject_SetAttr(self, r0, v) + r2 = r1 >= 0 :: signed + return 1 +def Base.__mypyc_defaults_setup(__mypyc_self__): + __mypyc_self__ :: __main__.Base + r0 :: str +L0: + r0 = '' + __mypyc_self__._x = r0 + return 1 diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 54e568a47768..0c1557b9eed9 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -5774,3 +5774,43 @@ from native import Concrete c = Concrete() assert c.value() == 42 assert c.derived() == 42 + +[case testPropertyShadowVtableGlue] +from typing import List +import mypy_extensions + +@mypy_extensions.mypyc_attr(allow_interpreted_subclasses=True) +class Base: + _x: str + _y: List[int] + def __init__(self) -> None: + self._x = "" + self._y = [] + + @property + def x(self) -> str: + return self._x + + @x.setter + def x(self, v: str) -> None: + self._x = v + + @property + def y(self) -> List[int]: + return self._y + + @y.setter + def y(self, v: List[int]) -> None: + self._y = v + + def method(self) -> str: + self.x = "a" + self.y = [1] + return self.x + str(len(self.y)) + +[file driver.py] +from native import Base + +Sub = type("Sub", (Base,), {}) +s = Sub() +assert s.method() == "a1"