]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.15] gh-75666: Fix a reference leak in tkinter event bindings (GH-151808) (GH-151958)
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Tue, 23 Jun 2026 04:26:53 +0000 (06:26 +0200)
committerGitHub <noreply@github.com>
Tue, 23 Jun 2026 04:26:53 +0000 (04:26 +0000)
The Tcl commands created for event callbacks are now deleted when a
binding is replaced or unbound, instead of being leaked.
(cherry picked from commit 3f09a175ad022ca7ccdbb8583a0c137d493533ef)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Lib/idlelib/idle_test/test_iomenu.py
Lib/test/test_tkinter/test_misc.py
Lib/tkinter/__init__.py
Misc/NEWS.d/next/Library/2026-06-20-15-00-00.gh-issue-75666.Kt9xQ2.rst [new file with mode: 0644]

index e0642cf0cabef0470424d9e04cfc4aa68c4adb26..80f72bdfe5ff0efa2e79b011e0c5cbf1599beb33 100644 (file)
@@ -23,11 +23,10 @@ class IOBindingTest(unittest.TestCase):
         cls.root = Tk()
         cls.root.withdraw()
         cls.editwin = EditorWindow(root=cls.root)
-        cls.io = iomenu.IOBinding(cls.editwin)
+        cls.io = cls.editwin.io
 
     @classmethod
     def tearDownClass(cls):
-        cls.io.close()
         cls.editwin._close()
         del cls.editwin
         cls.root.update_idletasks()
index 96cc7fb85929d054e1a45b782d2910a45c8f9ea1..fba528764415abaa9b7ab648b5cb681aacc9e18b 100644 (file)
@@ -1469,6 +1469,8 @@ class BindTest(AbstractTkTest, unittest.TestCase):
         self.assertNotIn(funcid, script)
         self.assertNotIn(funcid2, script)
         self.assertIn(funcid3, script)
+        self.assertCommandNotExist(funcid)
+        self.assertCommandNotExist(funcid2)
         self.assertCommandExist(funcid3)
 
     def test_bind_class(self):
@@ -1513,8 +1515,8 @@ class BindTest(AbstractTkTest, unittest.TestCase):
         unbind_class('Test', event)
         self.assertEqual(bind_class('Test', event), '')
         self.assertEqual(bind_class('Test'), ())
-        self.assertCommandExist(funcid)
-        self.assertCommandExist(funcid2)
+        self.assertCommandNotExist(funcid)
+        self.assertCommandNotExist(funcid2)
 
         unbind_class('Test', event)  # idempotent
 
@@ -1542,8 +1544,8 @@ class BindTest(AbstractTkTest, unittest.TestCase):
         self.assertNotIn(funcid, script)
         self.assertNotIn(funcid2, script)
         self.assertIn(funcid3, script)
-        self.assertCommandExist(funcid)
-        self.assertCommandExist(funcid2)
+        self.assertCommandNotExist(funcid)
+        self.assertCommandNotExist(funcid2)
         self.assertCommandExist(funcid3)
 
     def test_bind_all(self):
@@ -1585,8 +1587,8 @@ class BindTest(AbstractTkTest, unittest.TestCase):
         unbind_all(event)
         self.assertEqual(bind_all(event), '')
         self.assertNotIn(event, bind_all())
-        self.assertCommandExist(funcid)
-        self.assertCommandExist(funcid2)
+        self.assertCommandNotExist(funcid)
+        self.assertCommandNotExist(funcid2)
 
         unbind_all(event)  # idempotent
 
@@ -1614,8 +1616,8 @@ class BindTest(AbstractTkTest, unittest.TestCase):
         self.assertNotIn(funcid, script)
         self.assertNotIn(funcid2, script)
         self.assertIn(funcid3, script)
-        self.assertCommandExist(funcid)
-        self.assertCommandExist(funcid2)
+        self.assertCommandNotExist(funcid)
+        self.assertCommandNotExist(funcid2)
         self.assertCommandExist(funcid3)
 
     def _test_tag_bind(self, w):
index b0152778cb54855f920eccc1e9fc8119acce88b8..6875deaf5a5cca3e81a890b0335a64180418a4e1 100644 (file)
@@ -1503,13 +1503,26 @@ class Misc:
         else:
             self.tk.call('bindtags', self._w, tagList)
 
-    def _bind(self, what, sequence, func, add, needcleanup=1):
+    def _delete_bind_commands(self, *what):
+        lines = self.tk.call(what).split('\n')
+        p = re.compile(r'if \{"\[([^ ]+) .*\]" == "break"\} break')
+        for line in lines:
+            m = p.fullmatch(line)
+            if m:
+                funcid = m[1]
+                try:
+                    self.deletecommand(funcid)
+                except TclError:
+                    pass
+
+    def _bind(self, what, sequence, func, add):
         """Internal function."""
         if isinstance(func, str):
             self.tk.call(what + (sequence, func))
         elif func:
-            funcid = self._register(func, self._substitute,
-                        needcleanup)
+            if not add:
+                self._delete_bind_commands(*what, sequence)
+            funcid = self._register(func, self._substitute, needcleanup=True)
             cmd = ('%sif {"[%s %s]" == "break"} break\n'
                    %
                    (add and '+' or '',
@@ -1575,6 +1588,7 @@ class Misc:
 
     def _unbind(self, what, funcid=None):
         if funcid is None:
+            self._delete_bind_commands(*what)
             self.tk.call(*what, '')
         else:
             lines = self.tk.call(what).split('\n')
@@ -1591,7 +1605,7 @@ class Misc:
         An additional boolean parameter ADD specifies whether FUNC will
         be called additionally to the other bound function or whether
         it will replace the previous function. See bind for the return value."""
-        return self._root()._bind(('bind', 'all'), sequence, func, add, True)
+        return self._root()._bind(('bind', 'all'), sequence, func, add)
 
     def unbind_all(self, sequence):
         """Unbind for all widgets for event SEQUENCE all functions."""
@@ -1605,7 +1619,7 @@ class Misc:
         whether it will replace the previous function. See bind for
         the return value."""
 
-        return self._root()._bind(('bind', className), sequence, func, add, True)
+        return self._root()._bind(('bind', className), sequence, func, add)
 
     def unbind_class(self, className, sequence):
         """Unbind for all widgets with bindtag CLASSNAME for event SEQUENCE
diff --git a/Misc/NEWS.d/next/Library/2026-06-20-15-00-00.gh-issue-75666.Kt9xQ2.rst b/Misc/NEWS.d/next/Library/2026-06-20-15-00-00.gh-issue-75666.Kt9xQ2.rst
new file mode 100644 (file)
index 0000000..d2b2b06
--- /dev/null
@@ -0,0 +1,2 @@
+Fix a reference leak in :mod:`tkinter`: the Tcl commands created for event
+callbacks are now deleted when a binding is replaced or unbound.