From bf749089ebda53eb440efeaa400be989df8f9f4c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 21 Jul 2026 17:54:27 +0300 Subject: [PATCH 1/3] gh-154357: Hide the root window in the tkinter dialog tests Tk delivers generated keyboard events to the window which has the focus. While the root window is visible, the window manager can take the focus back from the dialog, and the events are dropped. Co-Authored-By: Claude Opus 4.8 (1M context) --- Lib/test/test_tkinter/support.py | 10 ++++++++++ Lib/test/test_tkinter/test_filedialog.py | 4 ++-- Lib/test/test_tkinter/test_simpledialog.py | 8 ++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_tkinter/support.py b/Lib/test/test_tkinter/support.py index 31feee2b8a40ee6..233a8f96ec46454 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 window which has the + # focus. 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): diff --git a/Lib/test/test_tkinter/test_filedialog.py b/Lib/test/test_tkinter/test_filedialog.py index f65ed19895a1ed4..c62b8c150e132d4 100644 --- a/Lib/test/test_tkinter/test_filedialog.py +++ b/Lib/test/test_tkinter/test_filedialog.py @@ -6,7 +6,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') @@ -72,7 +72,7 @@ def test_results_preserved(self): ('/a', '/b')) -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_simpledialog.py b/Lib/test/test_tkinter/test_simpledialog.py index be0be8d3f546311..10d0cce057ccf31 100644 --- a/Lib/test/test_tkinter/test_simpledialog.py +++ b/Lib/test/test_tkinter/test_simpledialog.py @@ -3,7 +3,7 @@ from tkinter import messagebox, ttk 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 from tkinter.simpledialog import (Dialog, SimpleDialog, askinteger, askfloat, askstring, _QueryInteger, _QueryFloat, _QueryString, @@ -12,7 +12,7 @@ requires('gui') -class SimpleDialogTest(AbstractTkTest, unittest.TestCase): +class SimpleDialogTest(AbstractDialogTest, unittest.TestCase): # SimpleDialog's modal loop is in go(); its bindings are exercised here by # generating events on the constructed dialog, without entering the loop. @@ -246,7 +246,7 @@ def test_go(self): self.assertEqual(d.go(), 0) -class DialogTest(AbstractTkTest, unittest.TestCase): +class DialogTest(AbstractDialogTest, unittest.TestCase): # Dialog's button box is modelled on tk::MessageBox. def open(self, **kw): @@ -432,7 +432,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 / bindings and the validation. From 5f7b0362daf74d0bf1052d8373b84c0dc58a8980 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 21 Jul 2026 18:56:03 +0300 Subject: [PATCH 2/3] gh-154357: Fix more flaky tkinter and ttk tests The window manager can reorder the toplevel windows while they are still being mapped, and can take the focus back while events are processed. Wait until the windows are mapped before changing the stacking order, and force the focus right before generating a keyboard event. Co-Authored-By: Claude Opus 4.8 (1M context) --- Lib/test/test_tkinter/test_misc.py | 6 ++++-- Lib/test/test_ttk/test_widgets.py | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index a225079dd99091b..b95b33943c731e7 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -17,7 +17,8 @@ from test.test_tkinter.support import setUpModule # noqa: F401 from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest, requires_tk, get_tk_patchlevel, - tcl_version, tk_version) + tcl_version, tk_version, + wait_until_mapped) support.requires('gui') @@ -1321,7 +1322,8 @@ def test_wm_stackorder(self): t2 = tkinter.Toplevel(self.root) t1.deiconify() t2.deiconify() - self.root.update() + wait_until_mapped(t1) + wait_until_mapped(t2) t1.lift(t2) # Raise t1 above t2. self.root.update() order = self.root.wm_stackorder() diff --git a/Lib/test/test_ttk/test_widgets.py b/Lib/test/test_ttk/test_widgets.py index 4fa6a032e97c5f8..9fcde4d87c0b109 100644 --- a/Lib/test/test_ttk/test_widgets.py +++ b/Lib/test/test_ttk/test_widgets.py @@ -2024,19 +2024,23 @@ def test_virtual_events(self): lambda e: selects.append(self.tv.selection())) self.tv.bind('<>', lambda e: opens.append(self.tv.focus())) self.tv.bind('<>', 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 while the events are processed. + self.tv.focus_force() self.tv.event_generate('') # Open the focused parent. self.tv.update() self.assertEqual(opens, [parent]) + self.tv.focus_force() self.tv.event_generate('') # Close it again. self.tv.update() self.assertEqual(closes, [parent]) + self.tv.focus_force() self.tv.event_generate('') # Move the selection. self.tv.update() self.assertEqual(self.tv.selection(), (item2,)) From 94c32f101861679cc2bb5a17ae036e1ad9b227c0 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 2 Sep 2026 20:52:11 +0300 Subject: [PATCH 3/3] gh-154357: Fix more flaky tkinter focus and stacking tests Some window managers ignore lift() or reorder the toplevel windows while they are being mapped, and can take the focus away from the application. In test_wm_stackorder, make the windows override-redirect on X11, so that the requested stacking order is honored regardless of the window manager. In test_focus_methods, focus_get() and focus_displayof() return None when the application does not hold the focus; check them only when it does, and test the window-manager-independent focus_lastfor() too. Force the focus right before generating the event in test_type_ahead. Co-Authored-By: Claude Opus 4.8 --- Lib/test/test_tkinter/support.py | 6 +++--- Lib/test/test_tkinter/test_filedialog.py | 4 +++- Lib/test/test_tkinter/test_misc.py | 16 +++++++++++++--- Lib/test/test_ttk/test_widgets.py | 2 +- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_tkinter/support.py b/Lib/test/test_tkinter/support.py index 233a8f96ec46454..aeab75db0561455 100644 --- a/Lib/test/test_tkinter/support.py +++ b/Lib/test/test_tkinter/support.py @@ -62,9 +62,9 @@ def require_mapped(self, widget, timeout=None): class AbstractDialogTest(AbstractTkTest): - # Tk delivers generated keyboard events to the window which has the - # focus. Hide the root window, otherwise the window manager can take - # the focus back from the dialog (gh-154357). + # 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() diff --git a/Lib/test/test_tkinter/test_filedialog.py b/Lib/test/test_tkinter/test_filedialog.py index c62b8c150e132d4..4f256a70640b5d6 100644 --- a/Lib/test/test_tkinter/test_filedialog.py +++ b/Lib/test/test_tkinter/test_filedialog.py @@ -195,8 +195,10 @@ def test_type_ahead(self): d.files.delete(0, 'end') for name in ('alpha', 'bravo', 'charlie'): d.files.insert('end', name) - d.files.focus_force() d.top.update() + # Force the focus right before generating the event: the window + # manager can take it back. + d.files.focus_force() d.files.event_generate('', keysym='c') d.top.update() sel = d.files.curselection() diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index b95b33943c731e7..a87a4ddd4b579bb 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -509,15 +509,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 @@ -1320,6 +1325,11 @@ def test_wm_transient(self): def test_wm_stackorder(self): t1 = tkinter.Toplevel(self.root) t2 = tkinter.Toplevel(self.root) + if self.root._windowingsystem == 'x11': + # Bypass the window manager, which may ignore lift() or reorder + # the windows while they are being mapped. + t1.overrideredirect(True) + t2.overrideredirect(True) t1.deiconify() t2.deiconify() wait_until_mapped(t1) diff --git a/Lib/test/test_ttk/test_widgets.py b/Lib/test/test_ttk/test_widgets.py index 9fcde4d87c0b109..6ed593e3e43bb07 100644 --- a/Lib/test/test_ttk/test_widgets.py +++ b/Lib/test/test_ttk/test_widgets.py @@ -2029,7 +2029,7 @@ def test_virtual_events(self): self.tv.update() # Force the focus right before generating the event: the window - # manager can take it back while the events are processed. + # manager can take it back. self.tv.focus_force() self.tv.event_generate('') # Open the focused parent. self.tv.update()