From bd915c5074450cc217861e2158b45f11b10ab9a7 Mon Sep 17 00:00:00 2001 From: vaggelisd Date: Tue, 6 Oct 2026 11:12:41 +0300 Subject: [PATCH] [mypyc] Fix property assignment when the setter accepts a wider type than the getter --- mypyc/codegen/emitfunc.py | 2 +- mypyc/irbuild/targets.py | 9 ++++ mypyc/test-data/run-classes.test | 70 ++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index 8637a2565006d..a1864b3acd31e 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -621,7 +621,7 @@ def visit_set_attr(self, op: SetAttr) -> None: rtype.setter_index(op.attr), src, rtype.struct_name(self.names), - self.ctype(rtype.attr_type(op.attr)), + self.ctype(op.propset.sig.args[1].type), c_ret_type, op.attr, ) diff --git a/mypyc/irbuild/targets.py b/mypyc/irbuild/targets.py index 8bc9da074f07d..67a5ed6f2ac4f 100644 --- a/mypyc/irbuild/targets.py +++ b/mypyc/irbuild/targets.py @@ -1,5 +1,6 @@ from __future__ import annotations +from mypyc.common import PROPSET_PREFIX from mypyc.ir.ops import Register, Value from mypyc.ir.rtypes import RInstance, RType, object_rprimitive @@ -49,6 +50,14 @@ def __init__(self, obj: Value, attr: str, can_borrow: bool = False) -> None: # Native attribute reference self.obj_type: RType = obj.type self.type = obj.type.attr_type(attr) + # A property setter may accept a wider type than its getter returns, + # so values assigned through it are coerced to the setter's argument type + for ir in obj.type.class_ir.mro: + setter = ir.method_decls.get(PROPSET_PREFIX + attr) + if setter is not None: + if not setter.implicit: + self.type = setter.sig.args[1].type + break else: # Python attribute reference self.obj_type = object_rprimitive diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 77f029b6b80b5..588c6d5c01643 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -2317,6 +2317,76 @@ import other_interpreted [out] +[case testPropertySetterWiderThanGetter] +from typing import Union +from mypy_extensions import trait + +class Wrapped: + def __init__(self, s: str) -> None: + self.s = s + +@trait +class T: + @property + def value(self) -> Wrapped: + raise NotImplementedError + + @value.setter + def value(self, v: Union[Wrapped, str]) -> None: + raise NotImplementedError + +class C(T): + def __init__(self) -> None: + self._value = Wrapped("init") + self._n = 0 + + @property + def value(self) -> Wrapped: + return self._value + + @value.setter + def value(self, v: Union[Wrapped, str]) -> None: + self._value = Wrapped(v) if isinstance(v, str) else v + + @property + def n(self) -> int: + return self._n + + @n.setter + def n(self, v: Union[int, str]) -> None: + self._n = int(v) + +def set_via_class(c: C, v: Union[Wrapped, str]) -> str: + c.value = v + return c.value.s + +def set_via_trait(t: T, v: Union[Wrapped, str]) -> str: + t.value = v + return t.value.s + +def set_unboxed(c: C, v: Union[int, str]) -> int: + c.n = v + return c.n + +def test_setter_wider_than_getter() -> None: + c = C() + assert set_via_class(c, "a") == "a" + assert set_via_class(c, Wrapped("b")) == "b" + assert set_via_trait(c, "c") == "c" + assert set_via_trait(c, Wrapped("d")) == "d" + assert set_unboxed(c, "5") == 5 + assert set_unboxed(c, 6) == 6 + +[file driver.py] +from native import C, test_setter_wider_than_getter +test_setter_wider_than_getter() + +c = C() +c.value = "py" +assert c.value.s == "py" +c.n = "7" +assert c.n == 7 + [case testAttributeOverridesProperty] from typing import Any from mypy_extensions import trait