diff --git a/Lib/idlelib/idle_test/test_iomenu.py b/Lib/idlelib/idle_test/test_iomenu.py index e0642cf0cabef04..80f72bdfe5ff0ef 100644 --- a/Lib/idlelib/idle_test/test_iomenu.py +++ b/Lib/idlelib/idle_test/test_iomenu.py @@ -23,11 +23,10 @@ def setUpClass(cls): 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() diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index b4cb5aaae1b1e5b..63b63e041466246 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -1486,6 +1486,8 @@ def test3(e): pass 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): @@ -1530,8 +1532,8 @@ def test2(e): pass 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 @@ -1559,8 +1561,8 @@ def test3(e): pass 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): @@ -1602,8 +1604,8 @@ def test2(e): pass 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 @@ -1631,8 +1633,8 @@ def test3(e): pass 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): diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 8bdf7cc1e2d96b8..5c267346bc5ec2d 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -1503,13 +1503,26 @@ def bindtags(self, tagList=None): 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 @@ def unbind(self, sequence, funcid=None): 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 @@ def bind_all(self, sequence=None, func=None, add=None): 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 @@ def bind_class(self, className, sequence=None, func=None, add=None): 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 index 000000000000000..d2b2b066837bb1f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-20-15-00-00.gh-issue-75666.Kt9xQ2.rst @@ -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.