Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mypyc/codegen/emitclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def dunder_attr_slot(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
"__hash__": ("tp_hash", generate_hash_wrapper),
"__get__": ("tp_descr_get", generate_get_wrapper),
"__getattr__": ("tp_getattro", dunder_attr_slot),
"__setattr__": ("tp_setattro", dunder_attr_slot),
}

AS_MAPPING_SLOT_DEFS: SlotTable = {
Expand Down
12 changes: 10 additions & 2 deletions mypyc/irbuild/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
Integer,
IntOp,
LoadStatic,
MethodCall,
Op,
PrimitiveDescription,
RaiseStandardError,
Expand Down Expand Up @@ -735,8 +736,15 @@ def assign(self, target: Register | AssignmentTarget, rvalue_reg: Value, line: i
self.add(Assign(target.register, rvalue_reg))
elif isinstance(target, AssignmentTargetAttr):
if isinstance(target.obj_type, RInstance):
rvalue_reg = self.coerce_rvalue(rvalue_reg, target.type, line)
self.add(SetAttr(target.obj, target.attr, rvalue_reg, line))
setattr = target.obj_type.class_ir.get_method("__setattr__")
if setattr:
key = self.load_str(target.attr)
boxed_reg = self.builder.box(rvalue_reg)
call = MethodCall(target.obj, setattr.name, [key, boxed_reg], line)
self.add(call)
else:
rvalue_reg = self.coerce_rvalue(rvalue_reg, target.type, line)
self.add(SetAttr(target.obj, target.attr, rvalue_reg, line))
else:
key = self.load_str(target.attr)
boxed_reg = self.builder.box(rvalue_reg)
Expand Down
7 changes: 7 additions & 0 deletions mypyc/irbuild/expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
apply_function_specialization,
apply_method_specialization,
translate_object_new,
translate_object_setattr,
)
from mypyc.primitives.bytes_ops import bytes_slice_op
from mypyc.primitives.dict_ops import dict_get_item_op, dict_new_op, exact_dict_set_item_op
Expand Down Expand Up @@ -480,6 +481,12 @@ def translate_super_method_call(builder: IRBuilder, expr: CallExpr, callee: Supe
result = translate_object_new(builder, expr, MemberExpr(callee.call, "__new__"))
if result:
return result
elif callee.name == "__setattr__":
result = translate_object_setattr(
builder, expr, MemberExpr(callee.call, "__setattr__")
)
if result:
return result
if ir.is_ext_class and ir.builtin_base is None and not ir.inherits_python:
if callee.name == "__init__" and len(expr.args) == 0:
# Call translates to object.__init__(self), which is a
Expand Down
31 changes: 31 additions & 0 deletions mypyc/irbuild/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from mypyc.ir.rtypes import (
RInstance,
bool_rprimitive,
c_int_rprimitive,
dict_rprimitive,
int_rprimitive,
object_rprimitive,
Expand Down Expand Up @@ -415,6 +416,34 @@ def generate_getattr_wrapper(builder: IRBuilder, cdef: ClassDef, getattr: FuncDe
builder.add(Return(getattr_result, line))


def generate_setattr_wrapper(builder: IRBuilder, cdef: ClassDef, setattr: FuncDef) -> None:
"""
Generate a wrapper function for __setattr__ that can be put into the tp_setattro slot.
The wrapper takes two arguments besides self - attribute name and the new value.
Returns 0 on success and -1 on failure. Restrictions are similar to the __getattr__
wrapper above.

This one is simpler because to match interpreted python semantics it's enough to always
call the user-provided function, including for names matching regular attributes.
"""
name = setattr.name + "__wrapper"
ir = builder.mapper.type_to_ir[cdef.info]
line = setattr.line

error_base = f'"__setattr__" not supported in class "{cdef.name}" because '
if ir.allow_interpreted_subclasses:
builder.error(error_base + "it allows interpreted subclasses", line)
if ir.inherits_python:
builder.error(error_base + "it inherits from a non-native class", line)

with builder.enter_method(ir, name, c_int_rprimitive, internal=True):
attr_arg = builder.add_argument("attr", object_rprimitive)
value_arg = builder.add_argument("value", object_rprimitive)

builder.gen_method_call(builder.self(), setattr.name, [attr_arg, value_arg], None, line)
builder.add(Return(Integer(0, c_int_rprimitive), line))


def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None:
# Perform the function of visit_method for methods inside extension classes.
name = fdef.name
Expand Down Expand Up @@ -483,6 +512,8 @@ def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None

if fdef.name == "__getattr__":
generate_getattr_wrapper(builder, cdef, fdef)
elif fdef.name == "__setattr__":
generate_setattr_wrapper(builder, cdef, fdef)


def handle_non_ext_method(
Expand Down
52 changes: 43 additions & 9 deletions mypyc/irbuild/specialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
Integer,
RaiseStandardError,
Register,
SetAttr,
Truncate,
Unreachable,
Value,
Expand Down Expand Up @@ -97,6 +98,7 @@
isinstance_dict,
)
from mypyc.primitives.float_ops import isinstance_float
from mypyc.primitives.generic_ops import generic_setattr
from mypyc.primitives.int_ops import isinstance_int
from mypyc.primitives.list_ops import isinstance_list, new_list_set_item_op
from mypyc.primitives.misc_ops import isinstance_bool
Expand Down Expand Up @@ -1007,19 +1009,24 @@ def translate_ord(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value
return None


@specialize_function("__new__", object_rprimitive)
def translate_object_new(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value | None:
fn = builder.fn_info
if fn.name != "__new__":
return None

is_super_new = isinstance(expr.callee, SuperExpr)
is_object_new = (
def is_object(callee: RefExpr) -> bool:
"""Returns True for object.<name> calls."""
return (
isinstance(callee, MemberExpr)
and isinstance(callee.expr, NameExpr)
and callee.expr.fullname == "builtins.object"
)
if not (is_super_new or is_object_new):


def is_super_or_object(expr: CallExpr, callee: RefExpr) -> bool:
"""Returns True for super().<name> or object.<name> calls."""
return isinstance(expr.callee, SuperExpr) or is_object(callee)


@specialize_function("__new__", object_rprimitive)
def translate_object_new(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value | None:
fn = builder.fn_info
if fn.name != "__new__" or not is_super_or_object(expr, callee):
return None

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

return None


@specialize_function("__setattr__", object_rprimitive)
def translate_object_setattr(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value | None:
is_super = isinstance(expr.callee, SuperExpr)
is_object_callee = is_object(callee)
if not ((is_super and len(expr.args) >= 2) or (is_object_callee and len(expr.args) >= 3)):
return None

self_reg = builder.accept(expr.args[0]) if is_object_callee else builder.self()
ir = builder.get_current_class_ir()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if this is a non-native class? Do we need to anything different?

Copy link
Collaborator Author

@p-sawicki p-sawicki Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think in general we can still translate object.__setattr__ calls because the underlying implementation of __setattr__ in cpython just calls the same function that we translate to.
for super().__setattr__ there might be issues when a non-native class inherits from another non-native class that defines __setattr__ as the translation would skip that inherited definition. but if it inherits only from object then we should be fine because we're back to object.__setattr__. i've changed the conditions for translating super().__setattr__ to reflect this.

nevermind, the translation doesn't play well with the fact that the non-native class is really a couple of python objects.

if ir and (not ir.is_ext_class or ir.builtin_base or ir.inherits_python):
return None
# Need to offset by 1 for super().__setattr__ calls because there is no self arg in this case.
name_idx = 0 if is_super else 1
value_idx = 1 if is_super else 2
attr_name = expr.args[name_idx]
attr_value = expr.args[value_idx]
value = builder.accept(attr_value)

if isinstance(attr_name, StrExpr) and ir and ir.has_attr(attr_name.value):
name = attr_name.value
value = builder.coerce(value, ir.attributes[name], expr.line)
return builder.add(SetAttr(self_reg, name, value, expr.line))

name_reg = builder.accept(attr_name)
return builder.call_c(generic_setattr, [self_reg, name_reg, value], expr.line)
3 changes: 3 additions & 0 deletions mypyc/lib-rt/CPy.h
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,9 @@ void CPyTrace_LogEvent(const char *location, const char *line, const char *op, c
static inline PyObject *CPyObject_GenericGetAttr(PyObject *self, PyObject *name) {
return _PyObject_GenericGetAttrWithDict(self, name, NULL, 1);
}
static inline int CPyObject_GenericSetAttr(PyObject *self, PyObject *name, PyObject *value) {
return _PyObject_GenericSetAttrWithDict(self, name, value, NULL);
}

#if CPY_3_11_FEATURES
PyObject *CPy_GetName(PyObject *obj);
Expand Down
7 changes: 7 additions & 0 deletions mypyc/primitives/generic_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,3 +410,10 @@
error_kind=ERR_NEVER,
returns_null=True,
)

generic_setattr = custom_op(
arg_types=[object_rprimitive, object_rprimitive, object_rprimitive],
return_type=c_int_rprimitive,
c_function_name="CPyObject_GenericSetAttr",
error_kind=ERR_NEG_INT,
)
1 change: 1 addition & 0 deletions mypyc/test-data/fixtures/ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def __init__(self) -> None: pass
def __eq__(self, x: object) -> bool: pass
def __ne__(self, x: object) -> bool: pass
def __str__(self) -> str: pass
def __setattr__(self, k: str, v: object) -> None: pass

class type:
def __init__(self, o: object) -> None: ...
Expand Down
Loading
Loading