https://github.com/python/cpython/commit/3736342e3876b4ae79118fa3c6cc53fb3e4bd3b0
commit: 3736342e3876b4ae79118fa3c6cc53fb3e4bd3b0
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-09-29T15:56:42+03:00
summary:

gh-157676: Fix KeyError in tkinter.simpledialog when a foreign window holds the 
grab (GH-157677)

_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 of widgets.

Regression from GH-151848, not present in released versions.

files:
M Lib/test/test_tkinter/test_simpledialog.py
M Lib/tkinter/simpledialog.py

diff --git a/Lib/test/test_tkinter/test_simpledialog.py 
b/Lib/test/test_tkinter/test_simpledialog.py
index 943d6e113ed34cd..3da9375158bd026 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
@@ -245,6 +246,26 @@ 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')
+        # 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')
+
 
 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 1b0cf51317ee082..caef785f936db36 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,11 @@ def _temp_grab_focus(grab, focus=None, destroy=True):
         yield
 
     finally:
-        if old_focus is not None:
+        # The old windows and even the whole application can be destroyed
+        # by now, so catch errors.
+        if old_focus:
             try:
-                old_focus.focus_set()
+                tk.call('focus', old_focus)
             except TclError:
                 pass
         try:
@@ -755,15 +758,17 @@ 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()):
-            # 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':
-                    old_grab.grab_set_global()
-                else:
-                    old_grab.grab_set()
+                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
 

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to