Skip to content

Commit 463121b

Browse files
committed
[mypyc] Support deleting attributes in __setattr__ wrapper
1 parent 1b8841b commit 463121b

File tree

4 files changed

+278
-8
lines changed

4 files changed

+278
-8
lines changed

mypyc/irbuild/function.py

Lines changed: 42 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]
@@ -436,10 +438,39 @@ def generate_setattr_wrapper(builder: IRBuilder, cdef: ClassDef, setattr: FuncDe
436438
if ir.inherits_python:
437439
builder.error(error_base + "it inherits from a non-native class", line)
438440

441+
delattr_symbol = cdef.info.get("__delattr__")
442+
delattr = delattr_symbol.node if delattr_symbol else None
443+
delattr_override = delattr is not None and not delattr.fullname.startswith("builtins.")
444+
if not delattr_override:
445+
builder.warning(
446+
f'Native class "{ir.name}" overrides "__setattr__" but not "__delattr__". '
447+
+ "At runtime, deleting attributes from this class will likely not work as expected. "
448+
+ 'Consider also defining "__delattr__".',
449+
line,
450+
)
451+
439452
with builder.enter_method(ir, name, c_int_rprimitive, internal=True):
440453
attr_arg = builder.add_argument("attr", object_rprimitive)
441454
value_arg = builder.add_argument("value", object_rprimitive)
442455

456+
call_delattr, call_setattr = BasicBlock(), BasicBlock()
457+
null = Integer(0, object_rprimitive, line)
458+
is_delattr = builder.add(ComparisonOp(value_arg, null, ComparisonOp.EQ, line))
459+
builder.add_bool_branch(is_delattr, call_delattr, call_setattr)
460+
461+
builder.activate_block(call_delattr)
462+
if delattr_override:
463+
builder.gen_method_call(builder.self(), "__delattr__", [attr_arg], None, line)
464+
else:
465+
# Call internal function that cpython normally calls when deleting an attribute.
466+
# Cannot call object.__delattr__ here because it calls PyObject_SetAttr internally
467+
# which in turn calls our wrapper and recurses infinitely.
468+
# Note that since native classes don't have __dict__, this will raise AttributeError
469+
# for dynamic attributes.
470+
builder.call_c(generic_setattr, [builder.self(), attr_arg, null], line)
471+
builder.add(Return(Integer(0, c_int_rprimitive), line))
472+
473+
builder.activate_block(call_setattr)
443474
builder.gen_method_call(builder.self(), setattr.name, [attr_arg, value_arg], None, line)
444475
builder.add(Return(Integer(0, c_int_rprimitive), line))
445476

@@ -514,6 +545,14 @@ def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None
514545
generate_getattr_wrapper(builder, cdef, fdef)
515546
elif fdef.name == "__setattr__":
516547
generate_setattr_wrapper(builder, cdef, fdef)
548+
elif fdef.name == "__delattr__":
549+
setattr = cdef.info.get("__setattr__")
550+
if not setattr or not setattr.node or setattr.node.fullname.startswith("builtins."):
551+
builder.error(
552+
'"__delattr__" supported only in classes that also override "__setattr__", '
553+
+ "or inherit from a native class that overrides it.",
554+
fdef.line,
555+
)
517556

518557

519558
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: 157 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2222,18 +2222,42 @@ 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: # W: Native class "SetAttr" overrides "__setattr__" but not "__delattr__". \
2250+
At runtime, deleting attributes from this class will likely not work as expected. Consider also defining "__delattr__".
2251+
pass
2252+
2253+
class NoSetAttr:
2254+
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.
2255+
pass
2256+
2257+
class InheritedSetAttr(SetAttr):
2258+
def __delattr__(self, attr: str) -> None:
2259+
pass
2260+
22372261
[case testSetAttr]
22382262
from typing import ClassVar
22392263
class SetAttr:
@@ -2329,11 +2353,21 @@ L6:
23292353
def SetAttr.__setattr____wrapper(__mypyc_self__, attr, value):
23302354
__mypyc_self__ :: __main__.SetAttr
23312355
attr, value :: object
2332-
r0 :: str
2333-
r1 :: None
2356+
r0 :: bit
2357+
r1 :: i32
2358+
r2 :: bit
2359+
r3 :: str
2360+
r4 :: None
23342361
L0:
2335-
r0 = cast(str, attr)
2336-
r1 = __mypyc_self__.__setattr__(r0, value)
2362+
r0 = value == 0
2363+
if r0 goto L1 else goto L2 :: bool
2364+
L1:
2365+
r1 = CPyObject_GenericSetAttr(__mypyc_self__, attr, 0)
2366+
r2 = r1 >= 0 :: signed
2367+
return 0
2368+
L2:
2369+
r3 = cast(str, attr)
2370+
r4 = __mypyc_self__.__setattr__(r3, value)
23372371
return 0
23382372
def test(attr, val):
23392373
attr :: str
@@ -2372,6 +2406,124 @@ L0:
23722406
r14 = r13 >= 0 :: signed
23732407
return 1
23742408

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

0 commit comments

Comments
 (0)