Skip to content

Commit 374fefb

Browse files
authored
[mypyc] Support deleting attributes in __setattr__ wrapper (#19997)
The `__setattr__` wrapper that mypyc generates needs to handle deleting attributes as well because `del` statements go through the same `tp_setattro` pointer but with the value argument set to `NULL`. The wrapper calls `__delattr__` in this case if it's overridden in the native class (or its parent). Handling of dynamic attributes is different without `__dict__` which makes a custom `__delattr__` required if the dynamic attributes are stored in a custom dictionary. If `__delattr__` is not overridden it calls the implementation of `object.__delattr__` which results in `AttributeError` because there's no `__dict__`. If it's defined without `__setattr__`, mypyc reports an error. It's possible to support just `__delattr__` but since it shares a slot with `__setattr__`, the wrapper generation would be more complicated. It seems like an unlikely use case to only need `__delattr__` so I think it makes sense to leave it for later.
1 parent 3807423 commit 374fefb

File tree

4 files changed

+333
-8
lines changed

4 files changed

+333
-8
lines changed

mypyc/irbuild/function.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
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
8787
from mypyc.primitives.misc_ops import register_function
8888
from mypyc.primitives.registry import builtin_names
8989
from 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

519550
def handle_non_ext_method(

mypyc/test-data/fixtures/ir.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def __eq__(self, x: object) -> bool: pass
4646
def __ne__(self, x: object) -> bool: pass
4747
def __str__(self) -> str: pass
4848
def __setattr__(self, k: str, v: object) -> None: pass
49+
def __delattr__(self, k: str) -> None: pass
4950

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

mypyc/test-data/irbuild-classes.test

Lines changed: 156 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2222,18 +2222,41 @@ class AllowsInterpreted:
22222222
def __setattr__(self, attr: str, val: object) -> None: # E: "__setattr__" not supported in class "AllowsInterpreted" because it allows interpreted subclasses
22232223
pass
22242224

2225+
def __delattr__(self, attr: str) -> None:
2226+
pass
2227+
22252228
class InheritsInterpreted(dict):
22262229
def __setattr__(self, attr: str, val: object) -> None: # E: "__setattr__" not supported in class "InheritsInterpreted" because it inherits from a non-native class
22272230
pass
22282231

2232+
def __delattr__(self, attr: str) -> None:
2233+
pass
2234+
22292235
@mypyc_attr(native_class=False)
22302236
class NonNative:
2231-
pass
2237+
def __setattr__(self, attr: str, val: object) -> None:
2238+
pass
22322239

22332240
class InheritsNonNative(NonNative):
22342241
def __setattr__(self, attr: str, val: object) -> None: # E: "__setattr__" not supported in class "InheritsNonNative" because it inherits from a non-native class
22352242
pass
22362243

2244+
def __delattr__(self, attr: str) -> None:
2245+
pass
2246+
2247+
[case testUnsupportedDelAttr]
2248+
class SetAttr:
2249+
def __setattr__(self, attr: str, val: object) -> None:
2250+
pass
2251+
2252+
class NoSetAttr:
2253+
def __delattr__(self, attr: str) -> None: # E: "__delattr__" supported only in classes that also override "__setattr__", or inherit from a native class that overrides it.
2254+
pass
2255+
2256+
class InheritedSetAttr(SetAttr):
2257+
def __delattr__(self, attr: str) -> None:
2258+
pass
2259+
22372260
[case testSetAttr]
22382261
from typing import ClassVar
22392262
class SetAttr:
@@ -2329,11 +2352,21 @@ L6:
23292352
def SetAttr.__setattr____wrapper(__mypyc_self__, attr, value):
23302353
__mypyc_self__ :: __main__.SetAttr
23312354
attr, value :: object
2332-
r0 :: str
2333-
r1 :: None
2355+
r0 :: bit
2356+
r1 :: i32
2357+
r2 :: bit
2358+
r3 :: str
2359+
r4 :: None
23342360
L0:
2335-
r0 = cast(str, attr)
2336-
r1 = __mypyc_self__.__setattr__(r0, value)
2361+
r0 = value == 0
2362+
if r0 goto L1 else goto L2 :: bool
2363+
L1:
2364+
r1 = CPyObject_GenericSetAttr(__mypyc_self__, attr, 0)
2365+
r2 = r1 >= 0 :: signed
2366+
return 0
2367+
L2:
2368+
r3 = cast(str, attr)
2369+
r4 = __mypyc_self__.__setattr__(r3, value)
23372370
return 0
23382371
def test(attr, val):
23392372
attr :: str
@@ -2372,6 +2405,124 @@ L0:
23722405
r14 = r13 >= 0 :: signed
23732406
return 1
23742407

2408+
[case testSetAttrAndDelAttr]
2409+
from typing import ClassVar
2410+
class SetAttr:
2411+
_attributes: dict[str, object]
2412+
regular_attr: int
2413+
class_var: ClassVar[str] = "x"
2414+
2415+
def __init__(self, regular_attr: int, extra_attrs: dict[str, object], new_attr: str, new_val: object) -> None:
2416+
super().__setattr__("_attributes", extra_attrs)
2417+
object.__setattr__(self, "regular_attr", regular_attr)
2418+
2419+
super().__setattr__(new_attr, new_val)
2420+
object.__setattr__(self, new_attr, new_val)
2421+
2422+
def __setattr__(self, key: str, val: object) -> None:
2423+
if key == "regular_attr":
2424+
super().__setattr__("regular_attr", val)
2425+
elif key == "class_var":
2426+
raise AttributeError()
2427+
else:
2428+
self._attributes[key] = val
2429+
2430+
def __delattr__(self, key: str) -> None:
2431+
del self._attributes[key]
2432+
2433+
[typing fixtures/typing-full.pyi]
2434+
[out]
2435+
def SetAttr.__init__(self, regular_attr, extra_attrs, new_attr, new_val):
2436+
self :: __main__.SetAttr
2437+
regular_attr :: int
2438+
extra_attrs :: dict
2439+
new_attr :: str
2440+
new_val :: object
2441+
r0 :: i32
2442+
r1 :: bit
2443+
r2 :: i32
2444+
r3 :: bit
2445+
L0:
2446+
self._attributes = extra_attrs
2447+
self.regular_attr = regular_attr
2448+
r0 = CPyObject_GenericSetAttr(self, new_attr, new_val)
2449+
r1 = r0 >= 0 :: signed
2450+
r2 = CPyObject_GenericSetAttr(self, new_attr, new_val)
2451+
r3 = r2 >= 0 :: signed
2452+
return 1
2453+
def SetAttr.__setattr__(self, key, val):
2454+
self :: __main__.SetAttr
2455+
key :: str
2456+
val :: object
2457+
r0 :: str
2458+
r1 :: bool
2459+
r2 :: int
2460+
r3 :: bool
2461+
r4 :: str
2462+
r5 :: bool
2463+
r6 :: object
2464+
r7 :: str
2465+
r8, r9 :: object
2466+
r10 :: dict
2467+
r11 :: i32
2468+
r12 :: bit
2469+
L0:
2470+
r0 = 'regular_attr'
2471+
r1 = CPyStr_Equal(key, r0)
2472+
if r1 goto L1 else goto L2 :: bool
2473+
L1:
2474+
r2 = unbox(int, val)
2475+
self.regular_attr = r2; r3 = is_error
2476+
goto L6
2477+
L2:
2478+
r4 = 'class_var'
2479+
r5 = CPyStr_Equal(key, r4)
2480+
if r5 goto L3 else goto L4 :: bool
2481+
L3:
2482+
r6 = builtins :: module
2483+
r7 = 'AttributeError'
2484+
r8 = CPyObject_GetAttr(r6, r7)
2485+
r9 = PyObject_Vectorcall(r8, 0, 0, 0)
2486+
CPy_Raise(r9)
2487+
unreachable
2488+
L4:
2489+
r10 = self._attributes
2490+
r11 = CPyDict_SetItem(r10, key, val)
2491+
r12 = r11 >= 0 :: signed
2492+
L5:
2493+
L6:
2494+
return 1
2495+
def SetAttr.__setattr____wrapper(__mypyc_self__, attr, value):
2496+
__mypyc_self__ :: __main__.SetAttr
2497+
attr, value :: object
2498+
r0 :: bit
2499+
r1 :: str
2500+
r2 :: None
2501+
r3 :: str
2502+
r4 :: None
2503+
L0:
2504+
r0 = value == 0
2505+
if r0 goto L1 else goto L2 :: bool
2506+
L1:
2507+
r1 = cast(str, attr)
2508+
r2 = __mypyc_self__.__delattr__(r1)
2509+
return 0
2510+
L2:
2511+
r3 = cast(str, attr)
2512+
r4 = __mypyc_self__.__setattr__(r3, value)
2513+
return 0
2514+
def SetAttr.__delattr__(self, key):
2515+
self :: __main__.SetAttr
2516+
key :: str
2517+
r0 :: dict
2518+
r1 :: i32
2519+
r2 :: bit
2520+
L0:
2521+
r0 = self._attributes
2522+
r1 = PyObject_DelItem(r0, key)
2523+
r2 = r1 >= 0 :: signed
2524+
return 1
2525+
23752526
[case testUntransformedSetAttr_64bit]
23762527
from mypy_extensions import mypyc_attr
23772528

0 commit comments

Comments
 (0)