https://github.com/python/cpython/commit/90464d61ecd09e88372c6fe9c19c2346d3c9d6ba
commit: 90464d61ecd09e88372c6fe9c19c2346d3c9d6ba
branch: 3.14
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-09-07T17:06:30+03:00
summary:

[3.14] gh-154357: Fix tkinter, ttk and IDLE tests depending on the window 
manager (GH-154370) (GH-157064)

The window manager can take the focus from the application, ignore
lift() and resize a toplevel on its own.

* Hide the root window in the dialog tests, so that it does not compete
  for the focus.
* Take the focus right before generating a key event.
* Tolerate additional focus events.
* Do not check focus_get() and focus_displayof() without the focus.
* Resize the toplevel to fit its content in wait_until_mapped().

(cherry picked from commit 3b564385e4c966de7f2da9ff8bfbc5e50296dc3d)

files:
M Lib/idlelib/idle_test/test_configdialog.py
M Lib/test/test_tkinter/support.py
M Lib/test/test_tkinter/test_filedialog.py
M Lib/test/test_tkinter/test_misc.py
M Lib/test/test_tkinter/test_simpledialog.py
M Lib/test/test_ttk/test_widgets.py

diff --git a/Lib/idlelib/idle_test/test_configdialog.py 
b/Lib/idlelib/idle_test/test_configdialog.py
index f3e1c785a92674c..24d47854dddd9e1 100644
--- a/Lib/idlelib/idle_test/test_configdialog.py
+++ b/Lib/idlelib/idle_test/test_configdialog.py
@@ -150,8 +150,8 @@ def test_fontlist_key(self):
         font = d.fontlist.get('active')
 
         # Test Down key.
-        fontlist.focus_force()
         fontlist.update()
+        fontlist.focus_force()
         fontlist.event_generate('<Key-Down>')
         fontlist.event_generate('<KeyRelease-Down>')
 
@@ -160,8 +160,8 @@ def test_fontlist_key(self):
         self.assertIn(d.font_name.get(), down_font.lower())
 
         # Test Up key.
-        fontlist.focus_force()
         fontlist.update()
+        fontlist.focus_force()
         fontlist.event_generate('<Key-Up>')
         fontlist.event_generate('<KeyRelease-Up>')
 
diff --git a/Lib/test/test_tkinter/support.py b/Lib/test/test_tkinter/support.py
index 31feee2b8a40ee6..8d4ad2345a16510 100644
--- a/Lib/test/test_tkinter/support.py
+++ b/Lib/test/test_tkinter/support.py
@@ -61,6 +61,16 @@ def require_mapped(self, widget, timeout=None):
                           f'(timed out after {timeout:g}s)')
 
 
+class AbstractDialogTest(AbstractTkTest):
+    # Tk delivers generated keyboard events to the focused window.  Hide the
+    # root window, otherwise the window manager can take the focus back from
+    # the dialog (gh-154357).
+
+    def setUp(self):
+        super().setUp()
+        self.root.withdraw()
+
+
 class AbstractDefaultRootTest:
 
     def setUp(self):
@@ -112,6 +122,7 @@ def wait_until_mapped(widget, timeout=None, *, 
full_size=False):
         timeout = support.LOOPBACK_TIMEOUT
     deadline = time.monotonic() + timeout
     widget.update_idletasks()
+    reset = False
     while True:
         widget.update()  # drain pending Map/Configure events
         if widget.winfo_ismapped():
@@ -123,6 +134,11 @@ def wait_until_mapped(widget, timeout=None, *, 
full_size=False):
                 h_ok = widget.winfo_height() > 1
             if w_ok and h_ok:
                 return True
+            if full_size and not reset:
+                # Tk no longer resizes the toplevel to fit its content if
+                # the window manager has resized it.  Undo this.
+                widget.winfo_toplevel().wm_geometry('')
+                reset = True
         if time.monotonic() >= deadline:
             return False
         time.sleep(0.01)
diff --git a/Lib/test/test_tkinter/test_filedialog.py 
b/Lib/test/test_tkinter/test_filedialog.py
index 054e719a0f883d9..e4e0f2b26c38bb5 100644
--- a/Lib/test/test_tkinter/test_filedialog.py
+++ b/Lib/test/test_tkinter/test_filedialog.py
@@ -4,7 +4,7 @@
 from tkinter.commondialog import Dialog
 from test.support import requires, swap_attr
 from test.test_tkinter.support import setUpModule  # noqa: F401
-from test.test_tkinter.support import AbstractTkTest
+from test.test_tkinter.support import AbstractDialogTest, AbstractTkTest
 
 requires('gui')
 
@@ -37,7 +37,7 @@ def test_directory(self):
         self.check(filedialog.Directory, 'tk_chooseDirectory')
 
 
-class FileDialogTest(AbstractTkTest, unittest.TestCase):
+class FileDialogTest(AbstractDialogTest, unittest.TestCase):
     # The pure-Python FileDialog runs its own modal loop in go(); its logic is
     # exercised here without entering the loop.
 
diff --git a/Lib/test/test_tkinter/test_misc.py 
b/Lib/test/test_tkinter/test_misc.py
index ee13cfd13a703b7..cc44449027f173d 100644
--- a/Lib/test/test_tkinter/test_misc.py
+++ b/Lib/test/test_tkinter/test_misc.py
@@ -451,15 +451,20 @@ def test_focus_methods(self):
         self.root.update_idletasks()
         f.focus_force()
         self.root.update()
-        self.assertIs(self.root.focus_get(), f)
-        self.assertIs(self.root.focus_displayof(), f)
+        # The window manager can take the focus away, and then focus_get()
+        # and focus_displayof() return None.
+        if self.root.focus_displayof() is not None:
+            self.assertIs(self.root.focus_get(), f)
+            self.assertIs(self.root.focus_displayof(), f)
         self.assertIs(f.focus_lastfor(), f)
         b = tkinter.Button(f)
         b.pack()
         self.root.update()
         b.focus_set()
         self.root.update()
-        self.assertIs(self.root.focus_get(), b)
+        if self.root.focus_displayof() is not None:
+            self.assertIs(self.root.focus_get(), b)
+        self.assertIs(f.focus_lastfor(), b)
 
     def test_focus_methods_unresolvable(self):
         # The focus may be on a widget that tkinter did not create and so
@@ -1164,7 +1169,9 @@ def test_focus(self):
 
         f.focus_force()
         self.root.update()
-        self.assertEqual(len(events), 1, events)
+        # The window manager can take the focus away and give it back,
+        # which makes Tk generate additional focus events.
+        self.assertGreaterEqual(len(events), 1, events)
         e = events[0]
         self.assertIs(e.type, tkinter.EventType.FocusIn)
         self.assertIs(e.widget, f)
diff --git a/Lib/test/test_tkinter/test_simpledialog.py 
b/Lib/test/test_tkinter/test_simpledialog.py
index 64edbb645bc80fa..1cb03074d89a76f 100644
--- a/Lib/test/test_tkinter/test_simpledialog.py
+++ b/Lib/test/test_tkinter/test_simpledialog.py
@@ -3,7 +3,8 @@
 from tkinter import messagebox
 from test.support import requires, swap_attr
 from test.test_tkinter.support import setUpModule  # noqa: F401
-from test.test_tkinter.support import AbstractDefaultRootTest, AbstractTkTest
+from test.test_tkinter.support import (AbstractDefaultRootTest,
+                                       AbstractDialogTest, AbstractTkTest)
 from tkinter.simpledialog import (Dialog, SimpleDialog,
                                   askinteger, askfloat, askstring,
                                   _QueryInteger, _QueryFloat, _QueryString)
@@ -12,6 +13,10 @@
 
 
 class SimpleDialogTest(AbstractTkTest, unittest.TestCase):
+    # The root window is not hidden here (cf. AbstractDialogTest): SimpleDialog
+    # makes its window transient for the master unconditionally, and a
+    # transient of a hidden window is never mapped on Windows, so that go()
+    # would block in wait_visibility().
     # SimpleDialog's modal loop is in go(); its bindings are exercised here by
     # generating events on the constructed dialog, without entering the loop.
 
@@ -50,8 +55,8 @@ def test_return_activates_default(self):
         # <Return> invokes the default button.
         d = self.create()  # default 0
         self.require_mapped(d.root)
-        d.root.focus_force()
         d.root.update()
+        d.root.focus_force()
         d.root.event_generate('<Return>')
         d.root.update()
         self.assertEqual(d.num, 0)
@@ -61,10 +66,9 @@ def test_return_no_default(self):
         # open instead of activating a button.
         d = self.create(default=None)
         self.require_mapped(d.root)
-        d.root.focus_force()
-        d.root.update()
         bells = []
         with swap_attr(d.root, 'bell', lambda *a, **k: bells.append(True)):
+            d.root.focus_force()
             d.root.event_generate('<Return>')
             d.root.update()
         self.assertTrue(bells)  # rang the bell
@@ -98,7 +102,7 @@ def test_go(self):
         self.assertEqual(d.go(), 0)
 
 
-class DialogTest(AbstractTkTest, unittest.TestCase):
+class DialogTest(AbstractDialogTest, unittest.TestCase):
     # Dialog is a base class for custom dialogs; exercise it via _QueryInteger.
 
     def open(self, **kw):
@@ -167,7 +171,7 @@ def mock_wait_window(w):
             self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line 
number")
 
 
-class QueryDialogTest(AbstractTkTest, unittest.TestCase):
+class QueryDialogTest(AbstractDialogTest, unittest.TestCase):
     # The query dialogs are modal: their __init__ blocks in wait_window().
     # Mock that out so the dialog stays alive and can be driven with generated
     # events, exercising the <Return>/<Escape> bindings and the validation.
diff --git a/Lib/test/test_ttk/test_widgets.py 
b/Lib/test/test_ttk/test_widgets.py
index 53bcd5ed2aab1b0..9fd7a7c9f06ba3e 100644
--- a/Lib/test/test_ttk/test_widgets.py
+++ b/Lib/test/test_ttk/test_widgets.py
@@ -2030,19 +2030,23 @@ def test_virtual_events(self):
                      lambda e: selects.append(self.tv.selection()))
         self.tv.bind('<<TreeviewOpen>>', lambda e: 
opens.append(self.tv.focus()))
         self.tv.bind('<<TreeviewClose>>', lambda e: 
closes.append(self.tv.focus()))
-        self.tv.focus_force()
         self.tv.focus(parent)
         self.tv.selection_set(parent)
         self.tv.update()
 
+        # Force the focus right before generating the event: the window
+        # manager can take it back.
+        self.tv.focus_force()
         self.tv.event_generate('<Right>')  # Open the focused parent.
         self.tv.update()
         self.assertEqual(opens, [parent])
 
+        self.tv.focus_force()
         self.tv.event_generate('<Left>')  # Close it again.
         self.tv.update()
         self.assertEqual(closes, [parent])
 
+        self.tv.focus_force()
         self.tv.event_generate('<Down>')  # Move the selection.
         self.tv.update()
         self.assertEqual(self.tv.selection(), (item2,))

_______________________________________________
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