Skip to content

Commit 6b387c7

Browse files
authored
[mypyc] Fix property assignment when the setter accepts a wider type than the getter (#22118)
When a property setter accepts a wider type than the getter returns, mypyc coerced the assigned value to the getter's type, so valid assignments failed at runtime: ```python from typing import Union class C: def __init__(self) -> None: self._x = 0 @Property def x(self) -> int: return self._x @x.setter def x(self, value: Union[int, str]) -> None: self._x = int(value) def set_x(c: C) -> int: c.x = "1" # TypeError: int object expected; got str return c.x ``` This uses the setter's argument type instead, both when coercing the assigned value and in the generated `CPY_SET_ATTR` call.
1 parent 4c91067 commit 6b387c7

3 files changed

Lines changed: 80 additions & 1 deletion

File tree

‎mypyc/codegen/emitfunc.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,7 @@ def visit_set_attr(self, op: SetAttr) -> None:
621621
rtype.setter_index(op.attr),
622622
src,
623623
rtype.struct_name(self.names),
624-
self.ctype(rtype.attr_type(op.attr)),
624+
self.ctype(op.propset.sig.args[1].type),
625625
c_ret_type,
626626
op.attr,
627627
)

‎mypyc/irbuild/targets.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from mypyc.common import PROPSET_PREFIX
34
from mypyc.ir.ops import Register, Value
45
from mypyc.ir.rtypes import RInstance, RType, object_rprimitive
56

@@ -49,6 +50,14 @@ def __init__(self, obj: Value, attr: str, can_borrow: bool = False) -> None:
4950
# Native attribute reference
5051
self.obj_type: RType = obj.type
5152
self.type = obj.type.attr_type(attr)
53+
# A property setter may accept a wider type than its getter returns,
54+
# so values assigned through it are coerced to the setter's argument type
55+
for ir in obj.type.class_ir.mro:
56+
setter = ir.method_decls.get(PROPSET_PREFIX + attr)
57+
if setter is not None:
58+
if not setter.implicit:
59+
self.type = setter.sig.args[1].type
60+
break
5261
else:
5362
# Python attribute reference
5463
self.obj_type = object_rprimitive

‎mypyc/test-data/run-classes.test‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2317,6 +2317,76 @@ import other_interpreted
23172317

23182318
[out]
23192319

2320+
[case testPropertySetterWiderThanGetter]
2321+
from typing import Union
2322+
from mypy_extensions import trait
2323+
2324+
class Wrapped:
2325+
def __init__(self, s: str) -> None:
2326+
self.s = s
2327+
2328+
@trait
2329+
class T:
2330+
@property
2331+
def value(self) -> Wrapped:
2332+
raise NotImplementedError
2333+
2334+
@value.setter
2335+
def value(self, v: Union[Wrapped, str]) -> None:
2336+
raise NotImplementedError
2337+
2338+
class C(T):
2339+
def __init__(self) -> None:
2340+
self._value = Wrapped("init")
2341+
self._n = 0
2342+
2343+
@property
2344+
def value(self) -> Wrapped:
2345+
return self._value
2346+
2347+
@value.setter
2348+
def value(self, v: Union[Wrapped, str]) -> None:
2349+
self._value = Wrapped(v) if isinstance(v, str) else v
2350+
2351+
@property
2352+
def n(self) -> int:
2353+
return self._n
2354+
2355+
@n.setter
2356+
def n(self, v: Union[int, str]) -> None:
2357+
self._n = int(v)
2358+
2359+
def set_via_class(c: C, v: Union[Wrapped, str]) -> str:
2360+
c.value = v
2361+
return c.value.s
2362+
2363+
def set_via_trait(t: T, v: Union[Wrapped, str]) -> str:
2364+
t.value = v
2365+
return t.value.s
2366+
2367+
def set_unboxed(c: C, v: Union[int, str]) -> int:
2368+
c.n = v
2369+
return c.n
2370+
2371+
def test_setter_wider_than_getter() -> None:
2372+
c = C()
2373+
assert set_via_class(c, "a") == "a"
2374+
assert set_via_class(c, Wrapped("b")) == "b"
2375+
assert set_via_trait(c, "c") == "c"
2376+
assert set_via_trait(c, Wrapped("d")) == "d"
2377+
assert set_unboxed(c, "5") == 5
2378+
assert set_unboxed(c, 6) == 6
2379+
2380+
[file driver.py]
2381+
from native import C, test_setter_wider_than_getter
2382+
test_setter_wider_than_getter()
2383+
2384+
c = C()
2385+
c.value = "py"
2386+
assert c.value.s == "py"
2387+
c.n = "7"
2388+
assert c.n == 7
2389+
23202390
[case testAttributeOverridesProperty]
23212391
from typing import Any
23222392
from mypy_extensions import trait

0 commit comments

Comments
 (0)