From 2d05b5497b4e5a692e1b33c5824c8984c32b76e2 Mon Sep 17 00:00:00 2001 From: Piotr Sawicki Date: Wed, 29 Jul 2026 15:02:00 +0200 Subject: [PATCH 1/6] [mypyc] Propagate property setter return value --- mypyc/codegen/emitclass.py | 6 ++++-- mypyc/lib-rt/CPy.h | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index ba48a24643b7d..836f5999caeeb 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -1358,15 +1358,17 @@ def generate_property_setter( ) ) emitter.emit_line("{") + emitter.emit_line("char retval = 0;") if arg_type.is_unboxed: emitter.emit_unbox("value", "tmp", arg_type, error=ReturnHandler("-1"), declare_dest=True) emitter.emit_line( - f"{NATIVE_PREFIX}{func_ir.cname(emitter.names)}((PyObject *) self, tmp);" + f"retval = {NATIVE_PREFIX}{func_ir.cname(emitter.names)}((PyObject *) self, tmp);" ) else: emitter.emit_line( - f"{NATIVE_PREFIX}{func_ir.cname(emitter.names)}((PyObject *) self, value);" + f"retval = {NATIVE_PREFIX}{func_ir.cname(emitter.names)}((PyObject *) self, value);" ) + emitter.emit_line("if (retval != 1) return -1;") emitter.emit_line("return 0;") emitter.emit_line("}") diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 458db90efd530..8b153b2a28c2c 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -107,14 +107,14 @@ static inline size_t CPy_FindAttrOffset(PyTypeObject *trait, CPyVTableItem *vtab #define CPY_GET_ATTR_TRAIT(obj, trait, vtable_index, object_type, attr_type) \ ((attr_type (*)(object_type *))(CPy_FindTraitVtable(trait, ((object_type *)obj)->vtable))[vtable_index])((object_type *)obj) -// Set attribute value using vtable +// Set attribute value using vtable. Native property setters return 1 for success and 2 for errors. #define CPY_SET_ATTR(obj, type, vtable_index, value, object_type, attr_type) \ - ((bool (*)(object_type *, attr_type))((object_type *)obj)->vtable[vtable_index])( \ - (object_type *)obj, value) + (((char (*)(object_type *, attr_type))((object_type *)obj)->vtable[vtable_index])( \ + (object_type *)obj, value) == 1) #define CPY_SET_ATTR_TRAIT(obj, trait, vtable_index, value, object_type, attr_type) \ - ((bool (*)(object_type *, attr_type))(CPy_FindTraitVtable(trait, ((object_type *)obj)->vtable))[vtable_index])( \ - (object_type *)obj, value) + (((char (*)(object_type *, attr_type))(CPy_FindTraitVtable(trait, ((object_type *)obj)->vtable))[vtable_index])( \ + (object_type *)obj, value) == 1) #define CPY_GET_METHOD(obj, type, vtable_index, object_type, method_type) \ ((method_type)(((object_type *)obj)->vtable[vtable_index])) From 1f52d4bbab0aa32ea471f8d9bf0bde9528d0a88e Mon Sep 17 00:00:00 2001 From: Piotr Sawicki Date: Wed, 5 Aug 2026 18:36:58 +0200 Subject: [PATCH 2/6] Add test --- mypyc/test-data/run-classes.test | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 822ce63861167..61afa133ac798 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -6280,3 +6280,35 @@ def comp(ns: list[int]) -> list[int]: def test_borrowed_final_attribute_in_comprehension() -> None: for _ in range(1000): assert comp([1, 2, 3, 4, 5]) == [112, 113, 114, 115, 116] + +[case testPropertySetterException] +from testutil import assertRaises + +class T: + def __init__(self, val: int) -> None: + self._val = val + + @property + def val(self) -> int: + return self._val + + @val.setter + def val(self, x: int) -> None: + if x < 0: + raise ValueError("No") + self._val = x + +def test_property_setter_exception() -> None: + t = T(1) + assert t.val == 1 + + t.val = 2 + assert t.val == 2 + + with assertRaises(ValueError): + t.val = -1 + + # Generic setattr goes through the Python descriptor setter instead of + # calling the native property setter directly through the vtable. + with assertRaises(ValueError): + setattr(t, "val", -1) From 1566bc100c7fa5b1c4a79b3d0a3e40b9191d6c5b Mon Sep 17 00:00:00 2001 From: Piotr Sawicki Date: Thu, 6 Aug 2026 12:16:37 +0200 Subject: [PATCH 3/6] Use C type based on function return type --- mypyc/codegen/emitclass.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index 836f5999caeeb..f45becbbc54ce 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -1358,7 +1358,8 @@ def generate_property_setter( ) ) emitter.emit_line("{") - emitter.emit_line("char retval = 0;") + ret_type = func_ir.ret_type + emitter.emit_line(f"{emitter.ctype(ret_type)} retval = {emitter.c_undefined_value(ret_type)};") if arg_type.is_unboxed: emitter.emit_unbox("value", "tmp", arg_type, error=ReturnHandler("-1"), declare_dest=True) emitter.emit_line( @@ -1368,7 +1369,7 @@ def generate_property_setter( emitter.emit_line( f"retval = {NATIVE_PREFIX}{func_ir.cname(emitter.names)}((PyObject *) self, value);" ) - emitter.emit_line("if (retval != 1) return -1;") + emitter.emit_line(f"if (retval == {emitter.c_error_value(ret_type)}) return -1;") emitter.emit_line("return 0;") emitter.emit_line("}") From 5ce07f11cb5400f903bea0803e0270593ca7d37d Mon Sep 17 00:00:00 2001 From: Piotr Sawicki Date: Thu, 6 Aug 2026 12:26:07 +0200 Subject: [PATCH 4/6] Add test for exception in getter --- mypyc/test-data/run-classes.test | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 61afa133ac798..81496327afe0d 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -6281,12 +6281,13 @@ def test_borrowed_final_attribute_in_comprehension() -> None: for _ in range(1000): assert comp([1, 2, 3, 4, 5]) == [112, 113, 114, 115, 116] -[case testPropertySetterException] +[case testPropertyException] from testutil import assertRaises class T: def __init__(self, val: int) -> None: self._val = val + self._locked = 0 @property def val(self) -> int: @@ -6298,6 +6299,10 @@ class T: raise ValueError("No") self._val = x + @property + def locked(self) -> int: + raise ValueError("Locked") + def test_property_setter_exception() -> None: t = T(1) assert t.val == 1 @@ -6312,3 +6317,12 @@ def test_property_setter_exception() -> None: # calling the native property setter directly through the vtable. with assertRaises(ValueError): setattr(t, "val", -1) + +def test_property_getter_exception() -> None: + t = T(1) + + with assertRaises(ValueError): + print(t.locked) + + with assertRaises(ValueError): + getattr(t, "locked") From d651bdc609b434584154354671af292443ec16ad Mon Sep 17 00:00:00 2001 From: Piotr Sawicki Date: Thu, 6 Aug 2026 16:28:57 +0200 Subject: [PATCH 5/6] Handle property setters returning types with error overlap --- mypyc/codegen/emitfunc.py | 15 ++++++++++++--- mypyc/ir/ops.py | 12 ++++++++---- mypyc/lib-rt/CPy.h | 16 ++++++++-------- mypyc/test-data/run-classes.test | 24 ++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index 4c3c17d047c8a..5162b88cfd0a6 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -548,7 +548,7 @@ def visit_set_attr(self, op: SetAttr) -> None: rtype = op.class_type cl = rtype.class_ir attr_rtype, decl_cl = cl.attr_details(op.attr) - if op.is_propset: + if op.propset is not None: # Again, use vtable access for properties... assert not op.is_init and op.error_kind == ERR_FALSE, "%s %d %d %s" % ( op.attr, @@ -557,10 +557,14 @@ def visit_set_attr(self, op: SetAttr) -> None: rtype, ) version = "_TRAIT" if cl.is_trait else "" + ret_type = op.propset.sig.ret_type + c_ret_type = self.emitter.ctype(ret_type) + tmp = self.temp_name() self.emit_line( - "%s = CPY_SET_ATTR%s(%s, %s, %d, %s, %s, %s); /* %s */" + "%s %s = CPY_SET_ATTR%s(%s, %s, %d, %s, %s, %s, %s); /* %s */" % ( - dest, + c_ret_type, + tmp, version, obj, self.emitter.type_struct_name(rtype.class_ir), @@ -568,9 +572,14 @@ def visit_set_attr(self, op: SetAttr) -> None: src, rtype.struct_name(self.names), self.ctype(rtype.attr_type(op.attr)), + c_ret_type, op.attr, ) ) + error_cond = f"{tmp} == {self.emitter.c_error_value(ret_type)}" + if ret_type.error_overlap: + error_cond += " && PyErr_Occurred()" + self.emit_line(f"{dest} = !({error_cond});") elif IS_FREE_THREADED and is_simple_refcounted_pointer(attr_rtype): # In free-threaded builds, publishing a single reference-counted # 'PyObject *' field must be atomic so a concurrent reader (see diff --git a/mypyc/ir/ops.py b/mypyc/ir/ops.py index 14e12559be1eb..febd0764c0837 100644 --- a/mypyc/ir/ops.py +++ b/mypyc/ir/ops.py @@ -945,14 +945,18 @@ def __init__(self, obj: Value, attr: str, src: Value, line: int) -> None: self.is_init = False cl = self.class_type.class_ir - is_propset = False + self.propset: FuncDecl | None = None for ir in cl.mro: propset = ir.method_decls.get(PROPSET_PREFIX + attr) if propset is not None: - is_propset = not propset.implicit + if not propset.implicit: + self.propset = propset break - # If True, this op represents calling a property setter. - self.is_propset = is_propset + + @property + def is_propset(self) -> bool: + """If True, this op represents calling a property setter.""" + return self.propset is not None def mark_as_initializer(self) -> None: self.is_init = True diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 8b153b2a28c2c..7e8ea1d71716b 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -107,14 +107,14 @@ static inline size_t CPy_FindAttrOffset(PyTypeObject *trait, CPyVTableItem *vtab #define CPY_GET_ATTR_TRAIT(obj, trait, vtable_index, object_type, attr_type) \ ((attr_type (*)(object_type *))(CPy_FindTraitVtable(trait, ((object_type *)obj)->vtable))[vtable_index])((object_type *)obj) -// Set attribute value using vtable. Native property setters return 1 for success and 2 for errors. -#define CPY_SET_ATTR(obj, type, vtable_index, value, object_type, attr_type) \ - (((char (*)(object_type *, attr_type))((object_type *)obj)->vtable[vtable_index])( \ - (object_type *)obj, value) == 1) - -#define CPY_SET_ATTR_TRAIT(obj, trait, vtable_index, value, object_type, attr_type) \ - (((char (*)(object_type *, attr_type))(CPy_FindTraitVtable(trait, ((object_type *)obj)->vtable))[vtable_index])( \ - (object_type *)obj, value) == 1) +// Set attribute value using vtable. +#define CPY_SET_ATTR(obj, type, vtable_index, value, object_type, attr_type, ret_type) \ + ((ret_type (*)(object_type *, attr_type))((object_type *)obj)->vtable[vtable_index])( \ + (object_type *)obj, value) + +#define CPY_SET_ATTR_TRAIT(obj, trait, vtable_index, value, object_type, attr_type, ret_type) \ + ((ret_type (*)(object_type *, attr_type))(CPy_FindTraitVtable(trait, ((object_type *)obj)->vtable))[vtable_index])( \ + (object_type *)obj, value) #define CPY_GET_METHOD(obj, type, vtable_index, object_type, method_type) \ ((method_type)(((object_type *)obj)->vtable[vtable_index])) diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 81496327afe0d..9651c12318b3b 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -6282,11 +6282,14 @@ def test_borrowed_final_attribute_in_comprehension() -> None: assert comp([1, 2, 3, 4, 5]) == [112, 113, 114, 115, 116] [case testPropertyException] +from mypy_extensions import i16 + from testutil import assertRaises class T: def __init__(self, val: int) -> None: self._val = val + self._val_overlaps: i16 = -1 self._locked = 0 @property @@ -6299,6 +6302,17 @@ class T: raise ValueError("No") self._val = x + @property + def val_overlaps(self) -> i16: + return self._val_overlaps + + @val_overlaps.setter + def val_overlaps(self, x: i16) -> i16: + if x > 0: + raise ValueError("Invalid") + self._val_overlaps = x + return self._val_overlaps + @property def locked(self) -> int: raise ValueError("Locked") @@ -6306,18 +6320,28 @@ class T: def test_property_setter_exception() -> None: t = T(1) assert t.val == 1 + assert t.val_overlaps == -1 t.val = 2 assert t.val == 2 + t.val_overlaps = -113 + assert t.val_overlaps == -113 + with assertRaises(ValueError): t.val = -1 + with assertRaises(ValueError): + t.val_overlaps = 1 + # Generic setattr goes through the Python descriptor setter instead of # calling the native property setter directly through the vtable. with assertRaises(ValueError): setattr(t, "val", -1) + with assertRaises(ValueError): + setattr(t, "val_overlaps", 1) + def test_property_getter_exception() -> None: t = T(1) From 16dfa37215702cfcfed430b6ef81c982f465cd5e Mon Sep 17 00:00:00 2001 From: Piotr Sawicki Date: Thu, 6 Aug 2026 16:38:38 +0200 Subject: [PATCH 6/6] Use generic emit_error_check --- mypyc/codegen/emitclass.py | 2 +- mypyc/codegen/emitfunc.py | 6 ++---- mypyc/test-data/run-classes.test | 30 ++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index f45becbbc54ce..b15c48092ce57 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -1369,7 +1369,7 @@ def generate_property_setter( emitter.emit_line( f"retval = {NATIVE_PREFIX}{func_ir.cname(emitter.names)}((PyObject *) self, value);" ) - emitter.emit_line(f"if (retval == {emitter.c_error_value(ret_type)}) return -1;") + emitter.emit_error_check("retval", ret_type, "return -1;") emitter.emit_line("return 0;") emitter.emit_line("}") diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index 5162b88cfd0a6..78746e1434331 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -576,10 +576,8 @@ def visit_set_attr(self, op: SetAttr) -> None: op.attr, ) ) - error_cond = f"{tmp} == {self.emitter.c_error_value(ret_type)}" - if ret_type.error_overlap: - error_cond += " && PyErr_Occurred()" - self.emit_line(f"{dest} = !({error_cond});") + self.emit_line(f"{dest} = 1;") + self.emitter.emit_error_check(tmp, ret_type, f"{dest} = 0;") elif IS_FREE_THREADED and is_simple_refcounted_pointer(attr_rtype): # In free-threaded builds, publishing a single reference-counted # 'PyObject *' field must be atomic so a concurrent reader (see diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 9651c12318b3b..637ff4df0099e 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -6290,6 +6290,7 @@ class T: def __init__(self, val: int) -> None: self._val = val self._val_overlaps: i16 = -1 + self._tuple_val = (0, 0) self._locked = 0 @property @@ -6313,6 +6314,17 @@ class T: self._val_overlaps = x return self._val_overlaps + @property + def tuple_val(self) -> tuple[int, int]: + return self._tuple_val + + @tuple_val.setter + def tuple_val(self, x: tuple[int, int]) -> tuple[int, int]: + if x[0] < 0: + raise ValueError("Invalid tuple") + self._tuple_val = x + return self._tuple_val + @property def locked(self) -> int: raise ValueError("Locked") @@ -6328,12 +6340,27 @@ def test_property_setter_exception() -> None: t.val_overlaps = -113 assert t.val_overlaps == -113 + t.val_overlaps = -1 + assert t.val_overlaps == -1 + + t.tuple_val = (1, 2) + assert t.tuple_val == (1, 2) + + setattr(t, "val_overlaps", -113) + assert t.val_overlaps == -113 + + setattr(t, "tuple_val", (3, 4)) + assert t.tuple_val == (3, 4) + with assertRaises(ValueError): t.val = -1 with assertRaises(ValueError): t.val_overlaps = 1 + with assertRaises(ValueError): + t.tuple_val = (-1, 0) + # Generic setattr goes through the Python descriptor setter instead of # calling the native property setter directly through the vtable. with assertRaises(ValueError): @@ -6342,6 +6369,9 @@ def test_property_setter_exception() -> None: with assertRaises(ValueError): setattr(t, "val_overlaps", 1) + with assertRaises(ValueError): + setattr(t, "tuple_val", (-1, 0)) + def test_property_getter_exception() -> None: t = T(1)