Skip to content

Commit 8392e1a

Browse files
authored
[mypyc] Generate __setattr__ wrapper (#19937)
Generate wrapper function for `__setattr__` and set it as the `tp_setattro` slot. The wrapper doesn't have to do anything because interpreted python runs the defined `__setattr__` on every attribute assignment. So the wrapper only reports errors on unsupported uses of `__setattr__` and makes the function signature match with the slot. Since `__setattr__` should run on every attribute assignment, native classes with `__setattr__` generate calls to this function on attribute assignment instead of direct assignment to the underlying C struct. Native classes generally don't have `__dict__` which makes implementing dynamic attributes more challenging. The native class has to manage its own dictionary of names to values and handle assignment to regular attributes specially. With `__dict__`, assigning values to regular and dynamic attributes can be done by simply assigning the value in `__dict__`, ie. `self.__dict__[name] = value` or `object.__setattr__(self, name, value)`. With a custom attribute dictionary, assigning with `name` being a regular attribute doesn't work because it would only update the value in the custom dictionary, not the actual attribute. On the other hand, the `object.__setattr__` call doesn't work for dynamic attributes and raises an `AttributeError` without `__dict__`. So something like this has to be implemented as a work-around: ``` def __setattr__(self, name: str, val: object) -> None: if name == "regular_attribute": object.__setattr__(self, "regular_attribute", val) else: self._attribute_dict[name] = val ``` To make this efficient in native classes, calls to `object.__setattr__` or equivalent `super().__setattr__` are transformed to direct C struct assignments when the name literal matches an attribute name.
1 parent df4ae6a commit 8392e1a

File tree

10 files changed

+1161
-11
lines changed

10 files changed

+1161
-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,
@@ -748,8 +749,15 @@ def assign(self, target: Register | AssignmentTarget, rvalue_reg: Value, line: i
748749
self.add(Assign(target.register, rvalue_reg))
749750
elif isinstance(target, AssignmentTargetAttr):
750751
if isinstance(target.obj_type, RInstance):
751-
rvalue_reg = self.coerce_rvalue(rvalue_reg, target.type, line)
752-
self.add(SetAttr(target.obj, target.attr, rvalue_reg, line))
752+
setattr = target.obj_type.class_ir.get_method("__setattr__")
753+
if setattr:
754+
key = self.load_str(target.attr)
755+
boxed_reg = self.builder.box(rvalue_reg)
756+
call = MethodCall(target.obj, setattr.name, [key, boxed_reg], line)
757+
self.add(call)
758+
else:
759+
rvalue_reg = self.coerce_rvalue(rvalue_reg, target.type, line)
760+
self.add(SetAttr(target.obj, target.attr, rvalue_reg, line))
753761
else:
754762
key = self.load_str(target.attr)
755763
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
@@ -480,6 +481,12 @@ def translate_super_method_call(builder: IRBuilder, expr: CallExpr, callee: Supe
480481
result = translate_object_new(builder, expr, MemberExpr(callee.call, "__new__"))
481482
if result:
482483
return result
484+
elif callee.name == "__setattr__":
485+
result = translate_object_setattr(
486+
builder, expr, MemberExpr(callee.call, "__setattr__")
487+
)
488+
if result:
489+
return result
483490
if ir.is_ext_class and ir.builtin_base is None and not ir.inherits_python:
484491
if callee.name == "__init__" and len(expr.args) == 0:
485492
# Call translates to object.__init__(self), which is a

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: 43 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,24 @@ 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+
"""Returns True for object.<name> calls."""
1014+
return (
10181015
isinstance(callee, MemberExpr)
10191016
and isinstance(callee.expr, NameExpr)
10201017
and callee.expr.fullname == "builtins.object"
10211018
)
1022-
if not (is_super_new or is_object_new):
1019+
1020+
1021+
def is_super_or_object(expr: CallExpr, callee: RefExpr) -> bool:
1022+
"""Returns True for super().<name> or object.<name> calls."""
1023+
return isinstance(expr.callee, SuperExpr) or is_object(callee)
1024+
1025+
1026+
@specialize_function("__new__", object_rprimitive)
1027+
def translate_object_new(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value | None:
1028+
fn = builder.fn_info
1029+
if fn.name != "__new__" or not is_super_or_object(expr, callee):
10231030
return None
10241031

10251032
ir = builder.get_current_class_ir()
@@ -1046,3 +1053,30 @@ def translate_object_new(builder: IRBuilder, expr: CallExpr, callee: RefExpr) ->
10461053
return builder.add(Call(ir.setup, [subtype], expr.line))
10471054

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