Skip to content

Commit 9133fda

Browse files
gh-75666: Fix a reference leak in tkinter event bindings
The Tcl commands created for event callbacks are now deleted when a binding is replaced or unbound, instead of being leaked.
1 parent e51b616 commit 9133fda

4 files changed

Lines changed: 32 additions & 15 deletions

File tree

Lib/idlelib/idle_test/test_iomenu.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,10 @@ def setUpClass(cls):
2323
cls.root = Tk()
2424
cls.root.withdraw()
2525
cls.editwin = EditorWindow(root=cls.root)
26-
cls.io = iomenu.IOBinding(cls.editwin)
26+
cls.io = cls.editwin.io
2727

2828
@classmethod
2929
def tearDownClass(cls):
30-
cls.io.close()
3130
cls.editwin._close()
3231
del cls.editwin
3332
cls.root.update_idletasks()

Lib/test/test_tkinter/test_misc.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,8 @@ def test3(e): pass
14861486
self.assertNotIn(funcid, script)
14871487
self.assertNotIn(funcid2, script)
14881488
self.assertIn(funcid3, script)
1489+
self.assertCommandNotExist(funcid)
1490+
self.assertCommandNotExist(funcid2)
14891491
self.assertCommandExist(funcid3)
14901492

14911493
def test_bind_class(self):
@@ -1530,8 +1532,8 @@ def test2(e): pass
15301532
unbind_class('Test', event)
15311533
self.assertEqual(bind_class('Test', event), '')
15321534
self.assertEqual(bind_class('Test'), ())
1533-
self.assertCommandExist(funcid)
1534-
self.assertCommandExist(funcid2)
1535+
self.assertCommandNotExist(funcid)
1536+
self.assertCommandNotExist(funcid2)
15351537

15361538
unbind_class('Test', event) # idempotent
15371539

@@ -1559,8 +1561,8 @@ def test3(e): pass
15591561
self.assertNotIn(funcid, script)
15601562
self.assertNotIn(funcid2, script)
15611563
self.assertIn(funcid3, script)
1562-
self.assertCommandExist(funcid)
1563-
self.assertCommandExist(funcid2)
1564+
self.assertCommandNotExist(funcid)
1565+
self.assertCommandNotExist(funcid2)
15641566
self.assertCommandExist(funcid3)
15651567

15661568
def test_bind_all(self):
@@ -1602,8 +1604,8 @@ def test2(e): pass
16021604
unbind_all(event)
16031605
self.assertEqual(bind_all(event), '')
16041606
self.assertNotIn(event, bind_all())
1605-
self.assertCommandExist(funcid)
1606-
self.assertCommandExist(funcid2)
1607+
self.assertCommandNotExist(funcid)
1608+
self.assertCommandNotExist(funcid2)
16071609

16081610
unbind_all(event) # idempotent
16091611

@@ -1631,8 +1633,8 @@ def test3(e): pass
16311633
self.assertNotIn(funcid, script)
16321634
self.assertNotIn(funcid2, script)
16331635
self.assertIn(funcid3, script)
1634-
self.assertCommandExist(funcid)
1635-
self.assertCommandExist(funcid2)
1636+
self.assertCommandNotExist(funcid)
1637+
self.assertCommandNotExist(funcid2)
16361638
self.assertCommandExist(funcid3)
16371639

16381640
def _test_tag_bind(self, w):

Lib/tkinter/__init__.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,13 +1503,26 @@ def bindtags(self, tagList=None):
15031503
else:
15041504
self.tk.call('bindtags', self._w, tagList)
15051505

1506-
def _bind(self, what, sequence, func, add, needcleanup=1):
1506+
def _delete_bind_commands(self, *what):
1507+
lines = self.tk.call(what).split('\n')
1508+
p = re.compile(r'if \{"\[([^ ]+) .*\]" == "break"\} break')
1509+
for line in lines:
1510+
m = p.fullmatch(line)
1511+
if m:
1512+
funcid = m[1]
1513+
try:
1514+
self.deletecommand(funcid)
1515+
except TclError:
1516+
pass
1517+
1518+
def _bind(self, what, sequence, func, add):
15071519
"""Internal function."""
15081520
if isinstance(func, str):
15091521
self.tk.call(what + (sequence, func))
15101522
elif func:
1511-
funcid = self._register(func, self._substitute,
1512-
needcleanup)
1523+
if not add:
1524+
self._delete_bind_commands(*what, sequence)
1525+
funcid = self._register(func, self._substitute, needcleanup=True)
15131526
cmd = ('%sif {"[%s %s]" == "break"} break\n'
15141527
%
15151528
(add and '+' or '',
@@ -1575,6 +1588,7 @@ def unbind(self, sequence, funcid=None):
15751588

15761589
def _unbind(self, what, funcid=None):
15771590
if funcid is None:
1591+
self._delete_bind_commands(*what)
15781592
self.tk.call(*what, '')
15791593
else:
15801594
lines = self.tk.call(what).split('\n')
@@ -1591,7 +1605,7 @@ def bind_all(self, sequence=None, func=None, add=None):
15911605
An additional boolean parameter ADD specifies whether FUNC will
15921606
be called additionally to the other bound function or whether
15931607
it will replace the previous function. See bind for the return value."""
1594-
return self._root()._bind(('bind', 'all'), sequence, func, add, True)
1608+
return self._root()._bind(('bind', 'all'), sequence, func, add)
15951609

15961610
def unbind_all(self, sequence):
15971611
"""Unbind for all widgets for event SEQUENCE all functions."""
@@ -1605,7 +1619,7 @@ def bind_class(self, className, sequence=None, func=None, add=None):
16051619
whether it will replace the previous function. See bind for
16061620
the return value."""
16071621

1608-
return self._root()._bind(('bind', className), sequence, func, add, True)
1622+
return self._root()._bind(('bind', className), sequence, func, add)
16091623

16101624
def unbind_class(self, className, sequence):
16111625
"""Unbind for all widgets with bindtag CLASSNAME for event SEQUENCE
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a reference leak in :mod:`tkinter`: the Tcl commands created for event
2+
callbacks are now deleted when a binding is replaced or unbound.

0 commit comments

Comments
 (0)