Skip to content

Commit 4ed9c54

Browse files
committed
[mypyc] Generate __setattr__ wrapper
1 parent 19697af commit 4ed9c54

File tree

10 files changed

+892
-11
lines changed

10 files changed

+892
-11
lines changed

mypyc/codegen/emitclass.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def dunder_attr_slot(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
6262
"__hash__": ("tp_hash", generate_hash_wrapper),
6363
"__get__": ("tp_descr_get", generate_get_wrapper),
6464
"__getattr__": ("tp_getattro", dunder_attr_slot),
65+
"__setattr__": ("tp_setattro", dunder_attr_slot),
6566
}
6667

6768
AS_MAPPING_SLOT_DEFS: SlotTable = {

mypyc/irbuild/builder.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
Integer,
7777
IntOp,
7878
LoadStatic,
79+
MethodCall,
7980
Op,
8081
PrimitiveDescription,
8182
RaiseStandardError,
@@ -735,8 +736,15 @@ def assign(self, target: Register | AssignmentTarget, rvalue_reg: Value, line: i
735736
self.add(Assign(target.register, rvalue_reg))
736737
elif isinstance(target, AssignmentTargetAttr):
737738
if isinstance(target.obj_type, RInstance):
738-
rvalue_reg = self.coerce_rvalue(rvalue_reg, target.type, line)
739-
self.add(SetAttr(target.obj, target.attr, rvalue_reg, line))
739+
setattr = target.obj_type.class_ir.get_method("__setattr__")
740+
if setattr:
741+
key = self.load_str(target.attr)
742+
boxed_reg = self.builder.box(rvalue_reg)
743+
call = MethodCall(target.obj, setattr.name, [key, boxed_reg], line)
744+
self.add(call)
745+
else:
746+
rvalue_reg = self.coerce_rvalue(rvalue_reg, target.type, line)
747+
self.add(SetAttr(target.obj, target.attr, rvalue_reg, line))
740748
else:
741749
key = self.load_str(target.attr)
742750
boxed_reg = self.builder.box(rvalue_reg)

mypyc/irbuild/expression.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
apply_function_specialization,
102102
apply_method_specialization,
103103
translate_object_new,
104+
translate_object_setattr,
104105
)
105106
from mypyc.primitives.bytes_ops import bytes_slice_op
106107
from mypyc.primitives.dict_ops import dict_get_item_op, dict_new_op, exact_dict_set_item_op
@@ -485,6 +486,12 @@ def translate_super_method_call(builder: IRBuilder, expr: CallExpr, callee: Supe
485486
# Call translates to object.__init__(self), which is a
486487
# no-op, so omit the call.
487488
return builder.none()
489+
elif callee.name == "__setattr__":
490+
result = translate_object_setattr(
491+
builder, expr, MemberExpr(callee.call, "__setattr__")
492+
)
493+
if result:
494+
return result
488495
return translate_call(builder, expr, callee)
489496

490497
decl = base.method_decl(callee.name)

mypyc/irbuild/function.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
from mypyc.ir.rtypes import (
5858
RInstance,
5959
bool_rprimitive,
60+
c_int_rprimitive,
6061
dict_rprimitive,
6162
int_rprimitive,
6263
object_rprimitive,
@@ -415,6 +416,34 @@ def generate_getattr_wrapper(builder: IRBuilder, cdef: ClassDef, getattr: FuncDe
415416
builder.add(Return(getattr_result, line))
416417

417418

419+
def generate_setattr_wrapper(builder: IRBuilder, cdef: ClassDef, setattr: FuncDef) -> None:
420+
"""
421+
Generate a wrapper function for __setattr__ that can be put into the tp_setattro slot.
422+
The wrapper takes two arguments besides self - attribute name and the new value.
423+
Returns 0 on success and -1 on failure. Restrictions are similar to the __getattr__
424+
wrapper above.
425+
426+
This one is simpler because to match interpreted python semantics it's enough to always
427+
call the user-provided function, including for names matching regular attributes.
428+
"""
429+
name = setattr.name + "__wrapper"
430+
ir = builder.mapper.type_to_ir[cdef.info]
431+
line = setattr.line
432+
433+
error_base = f'"__setattr__" not supported in class "{cdef.name}" because '
434+
if ir.allow_interpreted_subclasses:
435+
builder.error(error_base + "it allows interpreted subclasses", line)
436+
if ir.inherits_python:
437+
builder.error(error_base + "it inherits from a non-native class", line)
438+
439+
with builder.enter_method(ir, name, c_int_rprimitive, internal=True):
440+
attr_arg = builder.add_argument("attr", object_rprimitive)
441+
value_arg = builder.add_argument("value", object_rprimitive)
442+
443+
builder.gen_method_call(builder.self(), setattr.name, [attr_arg, value_arg], None, line)
444+
builder.add(Return(Integer(0, c_int_rprimitive), line))
445+
446+
418447
def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None:
419448
# Perform the function of visit_method for methods inside extension classes.
420449
name = fdef.name
@@ -483,6 +512,8 @@ def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None
483512

484513
if fdef.name == "__getattr__":
485514
generate_getattr_wrapper(builder, cdef, fdef)
515+
elif fdef.name == "__setattr__":
516+
generate_setattr_wrapper(builder, cdef, fdef)
486517

487518

488519
def handle_non_ext_method(

mypyc/irbuild/specialize.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
Integer,
4343
RaiseStandardError,
4444
Register,
45+
SetAttr,
4546
Truncate,
4647
Unreachable,
4748
Value,
@@ -97,6 +98,7 @@
9798
isinstance_dict,
9899
)
99100
from mypyc.primitives.float_ops import isinstance_float
101+
from mypyc.primitives.generic_ops import generic_setattr
100102
from mypyc.primitives.int_ops import isinstance_int
101103
from mypyc.primitives.list_ops import isinstance_list, new_list_set_item_op
102104
from mypyc.primitives.misc_ops import isinstance_bool
@@ -1007,19 +1009,28 @@ def translate_ord(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value
10071009
return None
10081010

10091011

1010-
@specialize_function("__new__", object_rprimitive)
1011-
def translate_object_new(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value | None:
1012-
fn = builder.fn_info
1013-
if fn.name != "__new__":
1014-
return None
1015-
1016-
is_super_new = isinstance(expr.callee, SuperExpr)
1017-
is_object_new = (
1012+
def is_object(callee: RefExpr) -> bool:
1013+
"""
1014+
Returns True for object.<name> calls
1015+
"""
1016+
return (
10181017
isinstance(callee, MemberExpr)
10191018
and isinstance(callee.expr, NameExpr)
10201019
and callee.expr.fullname == "builtins.object"
10211020
)
1022-
if not (is_super_new or is_object_new):
1021+
1022+
1023+
def is_super_or_object(expr: CallExpr, callee: RefExpr) -> bool:
1024+
"""
1025+
Returns True for super().<name> or object.<name> calls.
1026+
"""
1027+
return isinstance(expr.callee, SuperExpr) or is_object(callee)
1028+
1029+
1030+
@specialize_function("__new__", object_rprimitive)
1031+
def translate_object_new(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value | None:
1032+
fn = builder.fn_info
1033+
if fn.name != "__new__" or not is_super_or_object(expr, callee):
10231034
return None
10241035

10251036
ir = builder.get_current_class_ir()
@@ -1046,3 +1057,28 @@ def translate_object_new(builder: IRBuilder, expr: CallExpr, callee: RefExpr) ->
10461057
return builder.add(Call(ir.setup, [subtype], expr.line))
10471058

10481059
return None
1060+
1061+
1062+
@specialize_function("__setattr__", object_rprimitive)
1063+
def translate_object_setattr(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value | None:
1064+
is_super = isinstance(expr.callee, SuperExpr)
1065+
is_object_callee = is_object(callee)
1066+
if not ((is_super and len(expr.args) >= 2) or (is_object_callee and len(expr.args) >= 3)):
1067+
return None
1068+
1069+
self_reg = builder.accept(expr.args[0]) if is_object_callee else builder.self()
1070+
ir = builder.get_current_class_ir()
1071+
# Need to offset by 1 for super().__setattr__ calls because there is no self arg in this case.
1072+
name_idx = 0 if is_super else 1
1073+
value_idx = 1 if is_super else 2
1074+
attr_name = expr.args[name_idx]
1075+
attr_value = expr.args[value_idx]
1076+
value = builder.accept(attr_value)
1077+
1078+
if isinstance(attr_name, StrExpr) and ir and ir.has_attr(attr_name.value):
1079+
name = attr_name.value
1080+
value = builder.coerce(value, ir.attributes[name], expr.line)
1081+
return builder.add(SetAttr(self_reg, name, value, expr.line))
1082+
1083+
name_reg = builder.accept(attr_name)
1084+
return builder.call_c(generic_setattr, [self_reg, name_reg, value], expr.line)

mypyc/lib-rt/CPy.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,9 @@ void CPyTrace_LogEvent(const char *location, const char *line, const char *op, c
952952
static inline PyObject *CPyObject_GenericGetAttr(PyObject *self, PyObject *name) {
953953
return _PyObject_GenericGetAttrWithDict(self, name, NULL, 1);
954954
}
955+
static inline int CPyObject_GenericSetAttr(PyObject *self, PyObject *name, PyObject *value) {
956+
return _PyObject_GenericSetAttrWithDict(self, name, value, NULL);
957+
}
955958

956959
#if CPY_3_11_FEATURES
957960
PyObject *CPy_GetName(PyObject *obj);

mypyc/primitives/generic_ops.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,3 +410,10 @@
410410
error_kind=ERR_NEVER,
411411
returns_null=True,
412412
)
413+
414+
generic_setattr = custom_op(
415+
arg_types=[object_rprimitive, object_rprimitive, object_rprimitive],
416+
return_type=c_int_rprimitive,
417+
c_function_name="CPyObject_GenericSetAttr",
418+
error_kind=ERR_NEG_INT,
419+
)

mypyc/test-data/fixtures/ir.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def __init__(self) -> None: pass
4545
def __eq__(self, x: object) -> bool: pass
4646
def __ne__(self, x: object) -> bool: pass
4747
def __str__(self) -> str: pass
48+
def __setattr__(self, k: str, v: object) -> None: pass
4849

4950
class type:
5051
def __init__(self, o: object) -> None: ...

0 commit comments

Comments
 (0)