Skip to content

Commit 14be7ab

Browse files
authored
[mypyc] Propagate property setter return value (#21814)
The property setter wrapper put in `tp_getset` returns 0 regardless of the value returned by the function generated for the property setter. This can cause strange errors reported by cpython when the property setter raises an exception since the wrapper still returns a non-error value. Fix by checking the return value of the property setter in the wrapper and returning -1 on error.
1 parent 7ecf052 commit 14be7ab

5 files changed

Lines changed: 128 additions & 14 deletions

File tree

mypyc/codegen/emitclass.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1358,15 +1358,18 @@ def generate_property_setter(
13581358
)
13591359
)
13601360
emitter.emit_line("{")
1361+
ret_type = func_ir.ret_type
1362+
emitter.emit_line(f"{emitter.ctype(ret_type)} retval = {emitter.c_undefined_value(ret_type)};")
13611363
if arg_type.is_unboxed:
13621364
emitter.emit_unbox("value", "tmp", arg_type, error=ReturnHandler("-1"), declare_dest=True)
13631365
emitter.emit_line(
1364-
f"{NATIVE_PREFIX}{func_ir.cname(emitter.names)}((PyObject *) self, tmp);"
1366+
f"retval = {NATIVE_PREFIX}{func_ir.cname(emitter.names)}((PyObject *) self, tmp);"
13651367
)
13661368
else:
13671369
emitter.emit_line(
1368-
f"{NATIVE_PREFIX}{func_ir.cname(emitter.names)}((PyObject *) self, value);"
1370+
f"retval = {NATIVE_PREFIX}{func_ir.cname(emitter.names)}((PyObject *) self, value);"
13691371
)
1372+
emitter.emit_error_check("retval", ret_type, "return -1;")
13701373
emitter.emit_line("return 0;")
13711374
emitter.emit_line("}")
13721375

mypyc/codegen/emitfunc.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,7 @@ def visit_set_attr(self, op: SetAttr) -> None:
548548
rtype = op.class_type
549549
cl = rtype.class_ir
550550
attr_rtype, decl_cl = cl.attr_details(op.attr)
551-
if op.is_propset:
551+
if op.propset is not None:
552552
# Again, use vtable access for properties...
553553
assert not op.is_init and op.error_kind == ERR_FALSE, "%s %d %d %s" % (
554554
op.attr,
@@ -557,20 +557,27 @@ def visit_set_attr(self, op: SetAttr) -> None:
557557
rtype,
558558
)
559559
version = "_TRAIT" if cl.is_trait else ""
560+
ret_type = op.propset.sig.ret_type
561+
c_ret_type = self.emitter.ctype(ret_type)
562+
tmp = self.temp_name()
560563
self.emit_line(
561-
"%s = CPY_SET_ATTR%s(%s, %s, %d, %s, %s, %s); /* %s */"
564+
"%s %s = CPY_SET_ATTR%s(%s, %s, %d, %s, %s, %s, %s); /* %s */"
562565
% (
563-
dest,
566+
c_ret_type,
567+
tmp,
564568
version,
565569
obj,
566570
self.emitter.type_struct_name(rtype.class_ir),
567571
rtype.setter_index(op.attr),
568572
src,
569573
rtype.struct_name(self.names),
570574
self.ctype(rtype.attr_type(op.attr)),
575+
c_ret_type,
571576
op.attr,
572577
)
573578
)
579+
self.emit_line(f"{dest} = 1;")
580+
self.emitter.emit_error_check(tmp, ret_type, f"{dest} = 0;")
574581
elif IS_FREE_THREADED and is_simple_refcounted_pointer(attr_rtype):
575582
# In free-threaded builds, publishing a single reference-counted
576583
# 'PyObject *' field must be atomic so a concurrent reader (see

mypyc/ir/ops.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -945,14 +945,18 @@ def __init__(self, obj: Value, attr: str, src: Value, line: int) -> None:
945945
self.is_init = False
946946

947947
cl = self.class_type.class_ir
948-
is_propset = False
948+
self.propset: FuncDecl | None = None
949949
for ir in cl.mro:
950950
propset = ir.method_decls.get(PROPSET_PREFIX + attr)
951951
if propset is not None:
952-
is_propset = not propset.implicit
952+
if not propset.implicit:
953+
self.propset = propset
953954
break
954-
# If True, this op represents calling a property setter.
955-
self.is_propset = is_propset
955+
956+
@property
957+
def is_propset(self) -> bool:
958+
"""If True, this op represents calling a property setter."""
959+
return self.propset is not None
956960

957961
def mark_as_initializer(self) -> None:
958962
self.is_init = True

mypyc/lib-rt/CPy.h

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,13 @@ static inline size_t CPy_FindAttrOffset(PyTypeObject *trait, CPyVTableItem *vtab
107107
#define CPY_GET_ATTR_TRAIT(obj, trait, vtable_index, object_type, attr_type) \
108108
((attr_type (*)(object_type *))(CPy_FindTraitVtable(trait, ((object_type *)obj)->vtable))[vtable_index])((object_type *)obj)
109109

110-
// Set attribute value using vtable
111-
#define CPY_SET_ATTR(obj, type, vtable_index, value, object_type, attr_type) \
112-
((bool (*)(object_type *, attr_type))((object_type *)obj)->vtable[vtable_index])( \
110+
// Set attribute value using vtable.
111+
#define CPY_SET_ATTR(obj, type, vtable_index, value, object_type, attr_type, ret_type) \
112+
((ret_type (*)(object_type *, attr_type))((object_type *)obj)->vtable[vtable_index])( \
113113
(object_type *)obj, value)
114114

115-
#define CPY_SET_ATTR_TRAIT(obj, trait, vtable_index, value, object_type, attr_type) \
116-
((bool (*)(object_type *, attr_type))(CPy_FindTraitVtable(trait, ((object_type *)obj)->vtable))[vtable_index])( \
115+
#define CPY_SET_ATTR_TRAIT(obj, trait, vtable_index, value, object_type, attr_type, ret_type) \
116+
((ret_type (*)(object_type *, attr_type))(CPy_FindTraitVtable(trait, ((object_type *)obj)->vtable))[vtable_index])( \
117117
(object_type *)obj, value)
118118

119119
#define CPY_GET_METHOD(obj, type, vtable_index, object_type, method_type) \

mypyc/test-data/run-classes.test

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6280,3 +6280,103 @@ def comp(ns: list[int]) -> list[int]:
62806280
def test_borrowed_final_attribute_in_comprehension() -> None:
62816281
for _ in range(1000):
62826282
assert comp([1, 2, 3, 4, 5]) == [112, 113, 114, 115, 116]
6283+
6284+
[case testPropertyException]
6285+
from mypy_extensions import i16
6286+
6287+
from testutil import assertRaises
6288+
6289+
class T:
6290+
def __init__(self, val: int) -> None:
6291+
self._val = val
6292+
self._val_overlaps: i16 = -1
6293+
self._tuple_val = (0, 0)
6294+
self._locked = 0
6295+
6296+
@property
6297+
def val(self) -> int:
6298+
return self._val
6299+
6300+
@val.setter
6301+
def val(self, x: int) -> None:
6302+
if x < 0:
6303+
raise ValueError("No")
6304+
self._val = x
6305+
6306+
@property
6307+
def val_overlaps(self) -> i16:
6308+
return self._val_overlaps
6309+
6310+
@val_overlaps.setter
6311+
def val_overlaps(self, x: i16) -> i16:
6312+
if x > 0:
6313+
raise ValueError("Invalid")
6314+
self._val_overlaps = x
6315+
return self._val_overlaps
6316+
6317+
@property
6318+
def tuple_val(self) -> tuple[int, int]:
6319+
return self._tuple_val
6320+
6321+
@tuple_val.setter
6322+
def tuple_val(self, x: tuple[int, int]) -> tuple[int, int]:
6323+
if x[0] < 0:
6324+
raise ValueError("Invalid tuple")
6325+
self._tuple_val = x
6326+
return self._tuple_val
6327+
6328+
@property
6329+
def locked(self) -> int:
6330+
raise ValueError("Locked")
6331+
6332+
def test_property_setter_exception() -> None:
6333+
t = T(1)
6334+
assert t.val == 1
6335+
assert t.val_overlaps == -1
6336+
6337+
t.val = 2
6338+
assert t.val == 2
6339+
6340+
t.val_overlaps = -113
6341+
assert t.val_overlaps == -113
6342+
6343+
t.val_overlaps = -1
6344+
assert t.val_overlaps == -1
6345+
6346+
t.tuple_val = (1, 2)
6347+
assert t.tuple_val == (1, 2)
6348+
6349+
setattr(t, "val_overlaps", -113)
6350+
assert t.val_overlaps == -113
6351+
6352+
setattr(t, "tuple_val", (3, 4))
6353+
assert t.tuple_val == (3, 4)
6354+
6355+
with assertRaises(ValueError):
6356+
t.val = -1
6357+
6358+
with assertRaises(ValueError):
6359+
t.val_overlaps = 1
6360+
6361+
with assertRaises(ValueError):
6362+
t.tuple_val = (-1, 0)
6363+
6364+
# Generic setattr goes through the Python descriptor setter instead of
6365+
# calling the native property setter directly through the vtable.
6366+
with assertRaises(ValueError):
6367+
setattr(t, "val", -1)
6368+
6369+
with assertRaises(ValueError):
6370+
setattr(t, "val_overlaps", 1)
6371+
6372+
with assertRaises(ValueError):
6373+
setattr(t, "tuple_val", (-1, 0))
6374+
6375+
def test_property_getter_exception() -> None:
6376+
t = T(1)
6377+
6378+
with assertRaises(ValueError):
6379+
print(t.locked)
6380+
6381+
with assertRaises(ValueError):
6382+
getattr(t, "locked")

0 commit comments

Comments
 (0)