Skip to content

Commit edeaa5e

Browse files
committed
[mypyc] Call __delattr__ of a class that inherits __setattr__
1 parent 47f4844 commit edeaa5e

4 files changed

Lines changed: 233 additions & 10 deletions

File tree

‎mypyc/codegen/emitclass.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ def dunder_attr_slot(cl: ClassIR, fn: FuncIR, emitter: Emitter) -> str:
7979
"__hash__": ("tp_hash", generate_hash_wrapper),
8080
"__get__": ("tp_descr_get", generate_get_wrapper),
8181
"__getattr__": ("tp_getattro", dunder_attr_slot),
82-
"__setattr__": ("tp_setattro", dunder_attr_slot),
82+
# The wrapper is also generated for a class that defines only __delattr__, so fill
83+
# the slot based on the wrapper instead of __setattr__.
84+
"__setattr____wrapper": ("tp_setattro", native_slot),
8385
}
8486

8587
AS_MAPPING_SLOT_DEFS: SlotTable = {

‎mypyc/irbuild/function.py‎

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ def generate_getattr_wrapper(builder: IRBuilder, cdef: ClassDef, getattr: FuncDe
429429
builder.add(Return(getattr_result, line))
430430

431431

432-
def generate_setattr_wrapper(builder: IRBuilder, cdef: ClassDef, setattr: FuncDef) -> None:
432+
def generate_setattr_wrapper(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None:
433433
"""
434434
Generate a wrapper function for __setattr__ that can be put into the tp_setattro slot.
435435
The wrapper takes two arguments besides self - attribute name and the new value.
@@ -440,16 +440,21 @@ def generate_setattr_wrapper(builder: IRBuilder, cdef: ClassDef, setattr: FuncDe
440440
When it's NULL, this means that the call to tp_setattro comes from a del statement,
441441
so it calls __delattr__ instead. If __delattr__ is not overridden in the native class,
442442
this will call the base implementation in object which doesn't work without __dict__.
443+
444+
The wrapper is generated for a class that defines __setattr__, and for a class that
445+
defines only __delattr__ and inherits __setattr__ from a native class. The argument
446+
fdef is the definition of __setattr__ in the first case and of __delattr__ in the second.
443447
"""
444-
name = setattr.name + "__wrapper"
448+
name = "__setattr____wrapper"
445449
ir = builder.mapper.type_to_ir[cdef.info]
446-
line = setattr.line
450+
line = fdef.line
447451

448-
error_base = f'"__setattr__" not supported in class "{cdef.name}" because '
449-
if ir.allow_interpreted_subclasses:
450-
builder.error(error_base + "it allows interpreted subclasses", line)
451-
if ir.inherits_python:
452-
builder.error(error_base + "it inherits from a non-native class", line)
452+
if fdef.name == "__setattr__":
453+
error_base = f'"__setattr__" not supported in class "{cdef.name}" because '
454+
if ir.allow_interpreted_subclasses:
455+
builder.error(error_base + "it allows interpreted subclasses", line)
456+
if ir.inherits_python:
457+
builder.error(error_base + "it inherits from a non-native class", line)
453458

454459
with builder.enter_method(ir, name, c_int_rprimitive, internal=True):
455460
attr_arg = builder.add_argument("attr", object_rprimitive)
@@ -476,7 +481,7 @@ def generate_setattr_wrapper(builder: IRBuilder, cdef: ClassDef, setattr: FuncDe
476481
builder.add(Return(Integer(0, c_int_rprimitive), line))
477482

478483
builder.activate_block(call_setattr)
479-
builder.gen_method_call(builder.self(), setattr.name, [attr_arg, value_arg], None, line)
484+
builder.gen_method_call(builder.self(), "__setattr__", [attr_arg, value_arg], None, line)
480485
builder.add(Return(Integer(0, c_int_rprimitive), line))
481486

482487

@@ -578,6 +583,15 @@ def handle_ext_method(builder: IRBuilder, cdef: ClassDef, fdef: FuncDef) -> None
578583
+ "or inherit from a native class that overrides it.",
579584
fdef.line,
580585
)
586+
elif "__setattr__" not in class_ir.method_decls:
587+
# The class inherits __setattr__. If that comes from a native class, the class
588+
# would also inherit its tp_setattro wrapper, which was generated without this
589+
# __delattr__ and may not call it. So generate a wrapper for this class as well.
590+
setattr_cls = next(
591+
(base for base in class_ir.mro if "__setattr__" in base.method_decls), None
592+
)
593+
if setattr_cls is not None and setattr_cls.is_ext_class:
594+
generate_setattr_wrapper(builder, cdef, fdef)
581595

582596

583597
def handle_non_ext_method(

‎mypyc/test-data/irbuild-classes.test‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2498,6 +2498,116 @@ class InheritedSetAttr(SetAttr):
24982498
def __delattr__(self, attr: str) -> None:
24992499
pass
25002500

2501+
[case testDelAttrWithInheritedSetAttr]
2502+
from mypy_extensions import mypyc_attr
2503+
2504+
class SetAttr:
2505+
_attributes: dict[str, object]
2506+
2507+
def __setattr__(self, key: str, val: object) -> None:
2508+
self._attributes[key] = val
2509+
2510+
class DelAttr(SetAttr):
2511+
def __delattr__(self, key: str) -> None:
2512+
del self._attributes[key]
2513+
2514+
@mypyc_attr(native_class=False)
2515+
class NonNative:
2516+
def __setattr__(self, key: str, val: object) -> None:
2517+
pass
2518+
2519+
class InheritsNonNative(NonNative):
2520+
def __delattr__(self, key: str) -> None:
2521+
pass
2522+
2523+
[out]
2524+
def SetAttr.__setattr__(self, key, val):
2525+
self :: __main__.SetAttr
2526+
key :: str
2527+
val :: object
2528+
r0 :: dict
2529+
r1 :: i32
2530+
r2 :: bit
2531+
L0:
2532+
r0 = self._attributes
2533+
r1 = CPyDict_SetItem(r0, key, val)
2534+
r2 = r1 >= 0 :: signed
2535+
return 1
2536+
def SetAttr.__setattr____wrapper(__mypyc_self__, attr, value):
2537+
__mypyc_self__ :: __main__.SetAttr
2538+
attr, value :: object
2539+
r0 :: bit
2540+
r1 :: i32
2541+
r2 :: bit
2542+
r3 :: str
2543+
r4 :: None
2544+
L0:
2545+
r0 = value == 0
2546+
if r0 goto L1 else goto L2 :: bool
2547+
L1:
2548+
r1 = CPyObject_GenericSetAttr(__mypyc_self__, attr, 0)
2549+
r2 = r1 >= 0 :: signed
2550+
return 0
2551+
L2:
2552+
r3 = cast(str, attr)
2553+
r4 = __mypyc_self__.__setattr__(r3, value)
2554+
return 0
2555+
def DelAttr.__delattr__(self, key):
2556+
self :: __main__.DelAttr
2557+
key :: str
2558+
r0 :: dict
2559+
r1 :: i32
2560+
r2 :: bit
2561+
L0:
2562+
r0 = self._attributes
2563+
r1 = PyObject_DelItem(r0, key)
2564+
r2 = r1 >= 0 :: signed
2565+
return 1
2566+
def DelAttr.__setattr____wrapper(__mypyc_self__, attr, value):
2567+
__mypyc_self__ :: __main__.DelAttr
2568+
attr, value :: object
2569+
r0 :: bit
2570+
r1 :: str
2571+
r2 :: None
2572+
r3 :: str
2573+
r4 :: None
2574+
L0:
2575+
r0 = value == 0
2576+
if r0 goto L1 else goto L2 :: bool
2577+
L1:
2578+
r1 = cast(str, attr)
2579+
r2 = __mypyc_self__.__delattr__(r1)
2580+
return 0
2581+
L2:
2582+
r3 = cast(str, attr)
2583+
r4 = __mypyc_self__.__setattr__(r3, value)
2584+
return 0
2585+
def __setattr___NonNative_obj.__get__(__mypyc_self__, instance, owner):
2586+
__mypyc_self__, instance, owner, r0 :: object
2587+
r1 :: bit
2588+
r2 :: object
2589+
L0:
2590+
r0 = load_address _Py_NoneStruct
2591+
r1 = instance == r0
2592+
if r1 goto L1 else goto L2 :: bool
2593+
L1:
2594+
return __mypyc_self__
2595+
L2:
2596+
r2 = PyMethod_New(__mypyc_self__, instance)
2597+
return r2
2598+
def __setattr___NonNative_obj.__call__(__mypyc_self__, self, key, val):
2599+
__mypyc_self__ :: __main__.__setattr___NonNative_obj
2600+
self :: __main__.NonNative
2601+
key :: str
2602+
val :: object
2603+
L0:
2604+
return 1
2605+
def InheritsNonNative.__delattr__(self, key):
2606+
self :: __main__.InheritsNonNative
2607+
key :: str
2608+
L0:
2609+
return 1
2610+
25012611
[case testSetAttr]
25022612
from typing import ClassVar
25032613
class SetAttr:

‎mypyc/test-data/run-classes.test‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6160,6 +6160,103 @@ def test_deletable_attr() -> None:
61606160

61616161
test_deletable_attr()
61626162

6163+
[case testDunderDelAttrWithInheritedSetAttr]
6164+
from mypy_extensions import mypyc_attr
6165+
from testutil import assertRaises
6166+
from typing import Any
6167+
6168+
log: list[str] = []
6169+
6170+
class SetAttr:
6171+
_attributes: dict[str, object]
6172+
6173+
def __init__(self) -> None:
6174+
object.__setattr__(self, "_attributes", {})
6175+
6176+
def __setattr__(self, key: str, val: object) -> None:
6177+
self._attributes[key] = val
6178+
6179+
class SetAttrInherited(SetAttr):
6180+
pass
6181+
6182+
class DelAttr(SetAttr):
6183+
def __delattr__(self, key: str) -> None:
6184+
log.append("DelAttr")
6185+
del self._attributes[key]
6186+
6187+
class DelAttrInherited(DelAttr):
6188+
pass
6189+
6190+
class DelAttrOverridden(DelAttr):
6191+
def __delattr__(self, key: str) -> None:
6192+
log.append("DelAttrOverridden")
6193+
super().__delattr__(key)
6194+
6195+
class DelAttrInSubSubclass(SetAttrInherited):
6196+
def __delattr__(self, key: str) -> None:
6197+
log.append("DelAttrInSubSubclass")
6198+
del self._attributes[key]
6199+
6200+
class SetAttrOverridden(DelAttr):
6201+
def __setattr__(self, key: str, val: object) -> None:
6202+
super().__setattr__(key, val)
6203+
6204+
@mypyc_attr(native_class=False)
6205+
class NonNative:
6206+
_attributes: dict[str, object]
6207+
6208+
def __init__(self) -> None:
6209+
object.__setattr__(self, "_attributes", {})
6210+
6211+
def __setattr__(self, key: str, val: object) -> None:
6212+
self._attributes[key] = val
6213+
6214+
class DelAttrNonNativeBase(NonNative):
6215+
def __delattr__(self, key: str) -> None:
6216+
log.append("DelAttrNonNativeBase")
6217+
del self._attributes[key]
6218+
6219+
def check_delattr(i: Any, calls: list[str]) -> None:
6220+
i.one = 1
6221+
i.two = 2
6222+
i.three = 3
6223+
assert i._attributes == {"one": 1, "two": 2, "three": 3}
6224+
6225+
log.clear()
6226+
del i.one
6227+
assert log == calls
6228+
assert i._attributes == {"two": 2, "three": 3}
6229+
6230+
delattr(i, "two")
6231+
assert log == calls * 2
6232+
assert i._attributes == {"three": 3}
6233+
6234+
i.__delattr__("three")
6235+
assert log == calls * 3
6236+
assert i._attributes == {}
6237+
6238+
with assertRaises(KeyError):
6239+
del i.one
6240+
6241+
def test_delattr_with_inherited_setattr() -> None:
6242+
check_delattr(DelAttr(), ["DelAttr"])
6243+
check_delattr(DelAttrInherited(), ["DelAttr"])
6244+
check_delattr(DelAttrOverridden(), ["DelAttrOverridden", "DelAttr"])
6245+
check_delattr(DelAttrInSubSubclass(), ["DelAttrInSubSubclass"])
6246+
check_delattr(SetAttrOverridden(), ["DelAttr"])
6247+
check_delattr(DelAttrNonNativeBase(), ["DelAttrNonNativeBase"])
6248+
6249+
def test_inherited_setattr_without_delattr() -> None:
6250+
i: Any = SetAttr()
6251+
i.one = 1
6252+
with assertRaises(AttributeError):
6253+
del i.one
6254+
6255+
i = SetAttrInherited()
6256+
i.one = 1
6257+
with assertRaises(AttributeError):
6258+
del i.one
6259+
61636260
[case testBufferCorruptedData_librt_internal]
61646261
from librt.internal import (
61656262
ReadBuffer, read_bool, read_str, read_float, read_int, read_tag, read_bytes

0 commit comments

Comments
 (0)