8383 dict_new_op ,
8484 exact_dict_set_item_op ,
8585)
86- from mypyc .primitives .generic_ops import generic_getattr , py_setattr_op
86+ from mypyc .primitives .generic_ops import generic_getattr , generic_setattr , py_setattr_op
8787from mypyc .primitives .misc_ops import register_function
8888from mypyc .primitives .registry import builtin_names
8989from mypyc .sametype import is_same_method_signature , is_same_type
@@ -423,8 +423,10 @@ def generate_setattr_wrapper(builder: IRBuilder, cdef: ClassDef, setattr: FuncDe
423423 Returns 0 on success and -1 on failure. Restrictions are similar to the __getattr__
424424 wrapper above.
425425
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.
426+ The wrapper calls the user-defined __setattr__ when the value to set is not NULL.
427+ When it's NULL, this means that the call to tp_setattro comes from a del statement,
428+ so it calls __delattr__ instead. If __delattr__ is not overridden in the native class,
429+ this will call the base implementation in object which doesn't work without __dict__.
428430 """
429431 name = setattr .name + "__wrapper"
430432 ir = builder .mapper .type_to_ir [cdef .info ]
@@ -440,6 +442,27 @@ def generate_setattr_wrapper(builder: IRBuilder, cdef: ClassDef, setattr: FuncDe
440442 attr_arg = builder .add_argument ("attr" , object_rprimitive )
441443 value_arg = builder .add_argument ("value" , object_rprimitive )
442444
445+ call_delattr , call_setattr = BasicBlock (), BasicBlock ()
446+ null = Integer (0 , object_rprimitive , line )
447+ is_delattr = builder .add (ComparisonOp (value_arg , null , ComparisonOp .EQ , line ))
448+ builder .add_bool_branch (is_delattr , call_delattr , call_setattr )
449+
450+ builder .activate_block (call_delattr )
451+ delattr_symbol = cdef .info .get ("__delattr__" )
452+ delattr = delattr_symbol .node if delattr_symbol else None
453+ delattr_override = delattr is not None and not delattr .fullname .startswith ("builtins." )
454+ if delattr_override :
455+ builder .gen_method_call (builder .self (), "__delattr__" , [attr_arg ], None , line )
456+ else :
457+ # Call internal function that cpython normally calls when deleting an attribute.
458+ # Cannot call object.__delattr__ here because it calls PyObject_SetAttr internally
459+ # which in turn calls our wrapper and recurses infinitely.
460+ # Note that since native classes don't have __dict__, this will raise AttributeError
461+ # for dynamic attributes.
462+ builder .call_c (generic_setattr , [builder .self (), attr_arg , null ], line )
463+ builder .add (Return (Integer (0 , c_int_rprimitive ), line ))
464+
465+ builder .activate_block (call_setattr )
443466 builder .gen_method_call (builder .self (), setattr .name , [attr_arg , value_arg ], None , line )
444467 builder .add (Return (Integer (0 , c_int_rprimitive ), line ))
445468
@@ -514,6 +537,14 @@ def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None
514537 generate_getattr_wrapper (builder , cdef , fdef )
515538 elif fdef .name == "__setattr__" :
516539 generate_setattr_wrapper (builder , cdef , fdef )
540+ elif fdef .name == "__delattr__" :
541+ setattr = cdef .info .get ("__setattr__" )
542+ if not setattr or not setattr .node or setattr .node .fullname .startswith ("builtins." ):
543+ builder .error (
544+ '"__delattr__" supported only in classes that also override "__setattr__", '
545+ + "or inherit from a native class that overrides it." ,
546+ fdef .line ,
547+ )
517548
518549
519550def handle_non_ext_method (
0 commit comments