From 7560111cd466d4d55173d2b92559c30b9d73d23d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 17 Sep 2026 14:33:42 +0300 Subject: [PATCH 1/4] gh-157676: Fix KeyError in tkinter.simpledialog when a foreign window holds the grab _temp_grab_focus() used grab_current() and focus_get(), which raise KeyError for windows which were not created by tkinter, such as a native message box. Use Tcl window names instead. --- Lib/test/test_tkinter/test_simpledialog.py | 17 +++++++++++++++ Lib/tkinter/simpledialog.py | 25 +++++++++++----------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_tkinter/test_simpledialog.py b/Lib/test/test_tkinter/test_simpledialog.py index 943d6e113ed34c..af54953bf4275c 100644 --- a/Lib/test/test_tkinter/test_simpledialog.py +++ b/Lib/test/test_tkinter/test_simpledialog.py @@ -245,6 +245,23 @@ def test_go(self): d.root.after(1, lambda: d._buttons[0].invoke()) self.assertEqual(d.go(), 0) + def test_go_foreign_grab(self): + # gh-157676: the grab and the focus can be in a window which was not + # created by tkinter, such as a native message box; they must be + # restored after the dialog. + tk = self.root.tk + tk.call('toplevel', '.foreign') + self.addCleanup(tk.call, 'destroy', '.foreign') + tk.call('wm', 'deiconify', '.foreign') + tk.call('update') + tk.call('grab', 'set', '.foreign') + tk.call('focus', '-force', '.foreign') + d = self.create() + d.root.after(1, lambda: d._buttons[0].invoke()) + self.assertEqual(d.go(), 0) + self.assertEqual(tk.call('grab', 'current', self.root._w), '.foreign') + self.assertEqual(tk.call('focus'), '.foreign') + class DialogTest(AbstractDialogTest, unittest.TestCase): # Dialog's button box is modelled on tk::MessageBox. diff --git a/Lib/tkinter/simpledialog.py b/Lib/tkinter/simpledialog.py index 1b0cf51317ee08..c342396bb0500f 100644 --- a/Lib/tkinter/simpledialog.py +++ b/Lib/tkinter/simpledialog.py @@ -722,12 +722,13 @@ def askstring(title, prompt, **kw): @contextlib.contextmanager def _temp_grab_focus(grab, focus=None, destroy=True): - old_focus = grab.focus_get() - old_grab = grab.grab_current() - if old_grab is not None and old_grab.winfo_exists(): - old_status = old_grab.grab_status() - else: - old_status = None + # Use Tcl window names rather than widgets, because the focus and the + # grab can be in a window which was not created by tkinter, such as + # a native message box. + tk = grab.tk + old_focus = tk.call('focus') + old_grab = tk.call('grab', 'current', grab._w) + old_status = tk.call('grab', 'status', old_grab) if old_grab else None # The "grab" command will fail if another application # already holds the grab. So catch it. try: @@ -741,9 +742,9 @@ def _temp_grab_focus(grab, focus=None, destroy=True): yield finally: - if old_focus is not None: + if old_focus and grab.getboolean(tk.call('winfo', 'exists', old_focus)): try: - old_focus.focus_set() + tk.call('focus', old_focus) except TclError: pass try: @@ -755,15 +756,15 @@ def _temp_grab_focus(grab, focus=None, destroy=True): grab.destroy() except TclError: pass - if (old_grab is not None and old_grab.winfo_exists() - and old_grab.winfo_ismapped()): + if (old_grab and grab.getboolean(tk.call('winfo', 'exists', old_grab)) + and grab.getboolean(tk.call('winfo', 'ismapped', old_grab))): # The "grab" command will fail if another application # already holds the grab. So catch it. try: if old_status == 'global': - old_grab.grab_set_global() + tk.call('grab', 'set', '-global', old_grab) else: - old_grab.grab_set() + tk.call('grab', 'set', old_grab) except TclError: pass From a89df13e667db3de553f2a0f0fb0ea216d4e02ee Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 21 Sep 2026 16:02:38 +0300 Subject: [PATCH 2/4] Catch errors when restoring the focus and the grab The dialog can destroy a temporary default root, and then even "winfo" fails with "application has been destroyed". --- Lib/tkinter/simpledialog.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Lib/tkinter/simpledialog.py b/Lib/tkinter/simpledialog.py index c342396bb0500f..caef785f936db3 100644 --- a/Lib/tkinter/simpledialog.py +++ b/Lib/tkinter/simpledialog.py @@ -742,7 +742,9 @@ def _temp_grab_focus(grab, focus=None, destroy=True): yield finally: - if old_focus and grab.getboolean(tk.call('winfo', 'exists', old_focus)): + # The old windows and even the whole application can be destroyed + # by now, so catch errors. + if old_focus: try: tk.call('focus', old_focus) except TclError: @@ -756,15 +758,17 @@ def _temp_grab_focus(grab, focus=None, destroy=True): grab.destroy() except TclError: pass - if (old_grab and grab.getboolean(tk.call('winfo', 'exists', old_grab)) - and grab.getboolean(tk.call('winfo', 'ismapped', old_grab))): - # The "grab" command will fail if another application - # already holds the grab. So catch it. + if old_grab: + # Do not restore the grab if the window is no longer mapped: + # a local grab on an unmapped window would block the application. + # The "grab" command will fail if another application already + # holds the grab on a window with the same name. So catch it. try: - if old_status == 'global': - tk.call('grab', 'set', '-global', old_grab) - else: - tk.call('grab', 'set', old_grab) + if grab.getboolean(tk.call('winfo', 'ismapped', old_grab)): + if old_status == 'global': + tk.call('grab', 'set', '-global', old_grab) + else: + tk.call('grab', 'set', old_grab) except TclError: pass From 8ba6469495e906e227adb712ec471660441a68e3 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 21 Sep 2026 19:13:02 +0300 Subject: [PATCH 3/4] Do not check the focus on Windows if the application did not get it On the Windows CI runners "focus -force" has no effect, because SetForegroundWindow() is refused for a background process. --- Lib/test/test_tkinter/test_simpledialog.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_tkinter/test_simpledialog.py b/Lib/test/test_tkinter/test_simpledialog.py index af54953bf4275c..46aeec90d69d90 100644 --- a/Lib/test/test_tkinter/test_simpledialog.py +++ b/Lib/test/test_tkinter/test_simpledialog.py @@ -1,3 +1,4 @@ +import sys import unittest import tkinter from tkinter import messagebox, ttk @@ -256,11 +257,15 @@ def test_go_foreign_grab(self): tk.call('update') tk.call('grab', 'set', '.foreign') tk.call('focus', '-force', '.foreign') + # On Windows the application may not get the focus, and then "focus" + # returns an empty string. + has_focus = tk.call('focus') == '.foreign' d = self.create() d.root.after(1, lambda: d._buttons[0].invoke()) self.assertEqual(d.go(), 0) self.assertEqual(tk.call('grab', 'current', self.root._w), '.foreign') - self.assertEqual(tk.call('focus'), '.foreign') + if sys.platform != 'win32' or has_focus: + self.assertEqual(tk.call('focus'), '.foreign') class DialogTest(AbstractDialogTest, unittest.TestCase): From c0a3971ccf818191a929003abecb59462422d234 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 21 Sep 2026 20:09:58 +0300 Subject: [PATCH 4/4] Do not check the focus on Windows The application can lose the focus when the dialog is destroyed, and "focus" without -force does not take it back. --- Lib/test/test_tkinter/test_simpledialog.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_tkinter/test_simpledialog.py b/Lib/test/test_tkinter/test_simpledialog.py index 46aeec90d69d90..3da9375158bd02 100644 --- a/Lib/test/test_tkinter/test_simpledialog.py +++ b/Lib/test/test_tkinter/test_simpledialog.py @@ -257,14 +257,13 @@ def test_go_foreign_grab(self): tk.call('update') tk.call('grab', 'set', '.foreign') tk.call('focus', '-force', '.foreign') - # On Windows the application may not get the focus, and then "focus" - # returns an empty string. - has_focus = tk.call('focus') == '.foreign' d = self.create() d.root.after(1, lambda: d._buttons[0].invoke()) self.assertEqual(d.go(), 0) self.assertEqual(tk.call('grab', 'current', self.root._w), '.foreign') - if sys.platform != 'win32' or has_focus: + # On Windows the application can lose the focus when the dialog is + # destroyed, and then "focus" returns an empty string. + if sys.platform != 'win32': self.assertEqual(tk.call('focus'), '.foreign')