Skip to content

Commit 8104d01

Browse files
authored
[mypyc] Support __del__ methods (#18519)
Fixes mypyc/mypyc#1035 * Populating `.tp_finalize` if the user has defined `__del__`. * Calling `.tp_finalize` from `.tp_dealloc`.
1 parent d7f15be commit 8104d01

File tree

2 files changed

+132
-2
lines changed

2 files changed

+132
-2
lines changed

mypyc/codegen/emitclass.py

+51-2
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ def generate_class(cl: ClassIR, module: str, emitter: Emitter) -> None:
196196

197197
setup_name = f"{name_prefix}_setup"
198198
new_name = f"{name_prefix}_new"
199+
finalize_name = f"{name_prefix}_finalize"
199200
members_name = f"{name_prefix}_members"
200201
getseters_name = f"{name_prefix}_getseters"
201202
vtable_name = f"{name_prefix}_vtable"
@@ -217,6 +218,10 @@ def generate_class(cl: ClassIR, module: str, emitter: Emitter) -> None:
217218
fields["tp_dealloc"] = f"(destructor){name_prefix}_dealloc"
218219
fields["tp_traverse"] = f"(traverseproc){name_prefix}_traverse"
219220
fields["tp_clear"] = f"(inquiry){name_prefix}_clear"
221+
# Populate .tp_finalize and generate a finalize method only if __del__ is defined for this class.
222+
del_method = next((e.method for e in cl.vtable_entries if e.name == "__del__"), None)
223+
if del_method:
224+
fields["tp_finalize"] = f"(destructor){finalize_name}"
220225
if needs_getseters:
221226
fields["tp_getset"] = getseters_name
222227
fields["tp_methods"] = methods_name
@@ -297,8 +302,11 @@ def emit_line() -> None:
297302
emit_line()
298303
generate_clear_for_class(cl, clear_name, emitter)
299304
emit_line()
300-
generate_dealloc_for_class(cl, dealloc_name, clear_name, emitter)
305+
generate_dealloc_for_class(cl, dealloc_name, clear_name, bool(del_method), emitter)
301306
emit_line()
307+
if del_method:
308+
generate_finalize_for_class(del_method, finalize_name, emitter)
309+
emit_line()
302310

303311
if cl.allow_interpreted_subclasses:
304312
shadow_vtable_name: str | None = generate_vtables(
@@ -765,11 +773,19 @@ def generate_clear_for_class(cl: ClassIR, func_name: str, emitter: Emitter) -> N
765773

766774

767775
def generate_dealloc_for_class(
768-
cl: ClassIR, dealloc_func_name: str, clear_func_name: str, emitter: Emitter
776+
cl: ClassIR,
777+
dealloc_func_name: str,
778+
clear_func_name: str,
779+
has_tp_finalize: bool,
780+
emitter: Emitter,
769781
) -> None:
770782
emitter.emit_line("static void")
771783
emitter.emit_line(f"{dealloc_func_name}({cl.struct_name(emitter.names)} *self)")
772784
emitter.emit_line("{")
785+
if has_tp_finalize:
786+
emitter.emit_line("if (!PyObject_GC_IsFinalized((PyObject *)self)) {")
787+
emitter.emit_line("Py_TYPE(self)->tp_finalize((PyObject *)self);")
788+
emitter.emit_line("}")
773789
emitter.emit_line("PyObject_GC_UnTrack(self);")
774790
# The trashcan is needed to handle deep recursive deallocations
775791
emitter.emit_line(f"CPy_TRASHCAN_BEGIN(self, {dealloc_func_name})")
@@ -779,6 +795,39 @@ def generate_dealloc_for_class(
779795
emitter.emit_line("}")
780796

781797

798+
def generate_finalize_for_class(
799+
del_method: FuncIR, finalize_func_name: str, emitter: Emitter
800+
) -> None:
801+
emitter.emit_line("static void")
802+
emitter.emit_line(f"{finalize_func_name}(PyObject *self)")
803+
emitter.emit_line("{")
804+
emitter.emit_line("PyObject *type, *value, *traceback;")
805+
emitter.emit_line("PyErr_Fetch(&type, &value, &traceback);")
806+
emitter.emit_line(
807+
"{}{}{}(self);".format(
808+
emitter.get_group_prefix(del_method.decl),
809+
NATIVE_PREFIX,
810+
del_method.cname(emitter.names),
811+
)
812+
)
813+
emitter.emit_line("if (PyErr_Occurred() != NULL) {")
814+
emitter.emit_line('PyObject *del_str = PyUnicode_FromString("__del__");')
815+
emitter.emit_line(
816+
"PyObject *del_method = (del_str == NULL) ? NULL : _PyType_Lookup(Py_TYPE(self), del_str);"
817+
)
818+
# CPython interpreter uses PyErr_WriteUnraisable: https://docs.python.org/3/c-api/exceptions.html#c.PyErr_WriteUnraisable
819+
# However, the message is slightly different due to the way mypyc compiles classes.
820+
# CPython interpreter prints: Exception ignored in: <function F.__del__ at 0x100aed940>
821+
# mypyc prints: Exception ignored in: <slot wrapper '__del__' of 'F' objects>
822+
emitter.emit_line("PyErr_WriteUnraisable(del_method);")
823+
emitter.emit_line("Py_XDECREF(del_method);")
824+
emitter.emit_line("Py_XDECREF(del_str);")
825+
emitter.emit_line("}")
826+
# PyErr_Restore also clears exception raised in __del__.
827+
emitter.emit_line("PyErr_Restore(type, value, traceback);")
828+
emitter.emit_line("}")
829+
830+
782831
def generate_methods_table(cl: ClassIR, name: str, emitter: Emitter) -> None:
783832
emitter.emit_line(f"static PyMethodDef {name}[] = {{")
784833
for fn in cl.methods.values():

mypyc/test-data/run-classes.test

+81
Original file line numberDiff line numberDiff line change
@@ -2748,3 +2748,84 @@ def test_function():
27482748
assert(isinstance(d.bitems, BackwardDefinedClass))
27492749
assert(isinstance(d.fitem, ForwardDefinedClass))
27502750
assert(isinstance(d.fitems, ForwardDefinedClass))
2751+
2752+
[case testDel]
2753+
class A:
2754+
def __del__(self):
2755+
print("deleting A...")
2756+
2757+
class B:
2758+
def __del__(self):
2759+
print("deleting B...")
2760+
2761+
class C(B):
2762+
def __init__(self):
2763+
self.a = A()
2764+
2765+
def __del__(self):
2766+
print("deleting C...")
2767+
super().__del__()
2768+
2769+
class D(A):
2770+
pass
2771+
2772+
[file driver.py]
2773+
import native
2774+
native.C()
2775+
native.D()
2776+
2777+
[out]
2778+
deleting C...
2779+
deleting B...
2780+
deleting A...
2781+
deleting A...
2782+
2783+
[case testDelCircular]
2784+
import dataclasses
2785+
import typing
2786+
2787+
i: int = 1
2788+
2789+
@dataclasses.dataclass
2790+
class C:
2791+
var: typing.Optional["C"] = dataclasses.field(default=None)
2792+
2793+
def __del__(self):
2794+
global i
2795+
print(f"deleting C{i}...")
2796+
i = i + 1
2797+
2798+
[file driver.py]
2799+
import native
2800+
import gc
2801+
2802+
c1 = native.C()
2803+
c2 = native.C()
2804+
c1.var = c2
2805+
c2.var = c1
2806+
del c1
2807+
del c2
2808+
gc.collect()
2809+
2810+
[out]
2811+
deleting C1...
2812+
deleting C2...
2813+
2814+
[case testDelException]
2815+
# The error message in the expected output of this test does not match CPython's error message due to the way mypyc compiles Python classes. If the error message is fixed, the expected output of this test will also change.
2816+
class F:
2817+
def __del__(self):
2818+
if True:
2819+
raise Exception("e2")
2820+
2821+
[file driver.py]
2822+
import native
2823+
f = native.F()
2824+
del f
2825+
2826+
[out]
2827+
Exception ignored in: <slot wrapper '__del__' of 'F' objects>
2828+
Traceback (most recent call last):
2829+
File "native.py", line 5, in __del__
2830+
raise Exception("e2")
2831+
Exception: e2

0 commit comments

Comments
 (0)