Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions Lib/test/test_tkinter/test_misc.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import collections.abc
import functools
import gc
import platform
import sys
import time
import unittest
import tkinter
from tkinter import TclError
import enum
from test import support
from test.support import os_helper
from test.support.script_helper import assert_python_ok
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest,
requires_tk, get_tk_patchlevel,
Expand Down Expand Up @@ -350,6 +353,36 @@ def callback():
self.root.deletecommand(name)
self.assertRaises(TclError, self.root.tk.call, name)

def test_gc_protocol(self):
# gh-116946: _tkinter objects implement the GC protocol.
self.assertTrue(gc.is_tracked(self.root))
tok = self.root.tk.createtimerhandler(10_000_000, lambda: None)
try:
self.assertTrue(gc.is_tracked(tok))
finally:
tok.deletetimerhandler()

def test_timer_fires_after_gc(self):
# gh-116946: a pending timer is kept alive by the Tcl event loop, not by
# the garbage collector, so collecting it must not cancel it -- it must
# still fire even when the Python token has been dropped.
fired = []
self.root.tk.createtimerhandler(1, lambda: fired.append(1))
support.gc_collect()
deadline = time.monotonic() + support.SHORT_TIMEOUT
while not fired and time.monotonic() < deadline:
self.root.update()
self.assertEqual(fired, [1])

def test_pending_timer_at_shutdown(self):
# gh-116946: the final garbage collection at interpreter shutdown must
# not crash when it visits a timer that is still pending (its type has
# already been cleared by the module's tp_clear).
assert_python_ok('-c',
'import tkinter\n'
'interp = tkinter.Tcl()\n'
'interp.tk.createtimerhandler(10_000_000, lambda: None)\n')

def test_option(self):
self.addCleanup(self.root.option_clear)
self.root.option_add('*Button.background', 'red')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The internal :mod:`!_tkinter` ``tkapp`` and ``tktimertoken`` types now
implement the garbage collector protocol, so reference cycles involving a
Tcl interpreter or a timer handler can be collected.
79 changes: 66 additions & 13 deletions Modules/_tkinter.c
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,8 @@ Tkapp_New(const char *screenName, const char *className,
TkappObject *v;
char *argv0;

v = PyObject_New(TkappObject, (PyTypeObject *) Tkapp_Type);
PyTypeObject *tp = (PyTypeObject *)Tkapp_Type;
v = (TkappObject *)tp->tp_alloc(tp, 0);
if (v == NULL)
return NULL;

Expand Down Expand Up @@ -2810,7 +2811,8 @@ Tktt_New(PyObject *func)
{
TkttObject *v;

v = PyObject_New(TkttObject, (PyTypeObject *) Tktt_Type);
PyTypeObject *tp = (PyTypeObject *)Tktt_Type;
v = (TkttObject *)tp->tp_alloc(tp, 0);
if (v == NULL)
return NULL;

Expand All @@ -2821,16 +2823,41 @@ Tktt_New(PyObject *func)
return (TkttObject*)Py_NewRef(v);
}

static void
Tktt_Dealloc(PyObject *self)
/* Plain cast, not TkttObject_CAST: the GC can run at shutdown after
module_clear() has cleared the global Tktt_Type the macro checks against. */

static int
Tktt_Clear(PyObject *op)
{
TkttObject *v = TkttObject_CAST(self);
PyObject *func = v->func;
PyObject *tp = (PyObject *) Py_TYPE(self);
TkttObject *self = (TkttObject *)op;
Py_CLEAR(self->func);
return 0;
}

Py_XDECREF(func);
static int
Tktt_Traverse(PyObject *op, visitproc visit, void *arg)
{
TkttObject *self = (TkttObject *)op;
Py_VISIT(Py_TYPE(op));
/* Not the extra reference of a pending timer (see Tktt_New): it is owned
by the Tcl event loop, so the timer is a GC root, not part of a cycle. */
Py_VISIT(self->func);
return 0;
}

PyObject_Free(self);
static void
Tktt_Dealloc(PyObject *op)
{
TkttObject *self = (TkttObject *)op; /* see GC slots above */
PyTypeObject *tp = Py_TYPE(op);
PyObject_GC_UnTrack(op);
/* Cancel any pending timer so its callback cannot fire on freed memory. */
if (self->token != NULL) {
Tcl_DeleteTimerHandler(self->token);
self->token = NULL;
}
Py_XDECREF(self->func);
tp->tp_free(op);
Py_DECREF(tp);
}

Expand Down Expand Up @@ -3124,17 +3151,37 @@ _tkinter_tkapp_willdispatch_impl(TkappObject *self)

/**** Tkapp Type Methods ****/

/* Plain casts -- see the Tktt GC slots above. */

static int
Tkapp_Clear(PyObject *op)
{
TkappObject *self = (TkappObject *)op;
Py_CLEAR(self->trace);
return 0;
}

static int
Tkapp_Traverse(PyObject *op, visitproc visit, void *arg)
{
TkappObject *self = (TkappObject *)op;
Py_VISIT(Py_TYPE(op));
Py_VISIT(self->trace);
return 0;
}

static void
Tkapp_Dealloc(PyObject *op)
{
TkappObject *self = TkappObject_CAST(op);
PyTypeObject *tp = Py_TYPE(self);
TkappObject *self = (TkappObject *)op;
PyTypeObject *tp = Py_TYPE(op);
PyObject_GC_UnTrack(op);
/*CHECK_TCL_APPARTMENT;*/
ENTER_TCL
Tcl_DeleteInterp(Tkapp_Interp(self));
LEAVE_TCL
Py_XDECREF(self->trace);
PyObject_Free(self);
(void)Tkapp_Clear(op);
tp->tp_free(op);
Py_DECREF(tp);
DisableEventHook();
}
Expand Down Expand Up @@ -3341,6 +3388,8 @@ static PyMethodDef Tktt_methods[] =

static PyType_Slot Tktt_Type_slots[] = {
{Py_tp_dealloc, Tktt_Dealloc},
{Py_tp_traverse, Tktt_Traverse},
{Py_tp_clear, Tktt_Clear},
{Py_tp_repr, Tktt_Repr},
{Py_tp_methods, Tktt_methods},
{0, 0}
Expand All @@ -3353,6 +3402,7 @@ static PyType_Spec Tktt_Type_spec = {
Py_TPFLAGS_DEFAULT
| Py_TPFLAGS_DISALLOW_INSTANTIATION
| Py_TPFLAGS_IMMUTABLETYPE
| Py_TPFLAGS_HAVE_GC
),
.slots = Tktt_Type_slots,
};
Expand Down Expand Up @@ -3400,6 +3450,8 @@ static PyMethodDef Tkapp_methods[] =

static PyType_Slot Tkapp_Type_slots[] = {
{Py_tp_dealloc, Tkapp_Dealloc},
{Py_tp_traverse, Tkapp_Traverse},
{Py_tp_clear, Tkapp_Clear},
{Py_tp_methods, Tkapp_methods},
{0, 0}
};
Expand All @@ -3412,6 +3464,7 @@ static PyType_Spec Tkapp_Type_spec = {
Py_TPFLAGS_DEFAULT
| Py_TPFLAGS_DISALLOW_INSTANTIATION
| Py_TPFLAGS_IMMUTABLETYPE
| Py_TPFLAGS_HAVE_GC
),
.slots = Tkapp_Type_slots,
};
Expand Down
Loading