Skip to content

gh-154357: Fix tkinter, ttk and IDLE tests depending on the window manager - #154370

Merged
serhiy-storchaka merged 7 commits into
python:mainfrom
serhiy-storchaka:gh-154357-dialog-focus-race
Sep 7, 2026
Merged

gh-154357: Fix tkinter, ttk and IDLE tests depending on the window manager#154370
serhiy-storchaka merged 7 commits into
python:mainfrom
serhiy-storchaka:gh-154357-dialog-focus-race

Conversation

@serhiy-storchaka

@serhiy-storchaka serhiy-storchaka commented Jul 21, 2026

Copy link
Copy Markdown
Member

The window manager can take the focus from the application, ignore lift() and resize a toplevel on its own. Tk delivers generated keyboard events to the window which has the focus, so a test which lost the focus does not get the event at all, its binding never fires, and it fails with an unset result.

  • Hide the root window in the dialog tests, so that the window manager does not take the focus back from the dialog. AbstractTkTest.setUp() deiconifies the root window, and waiting until the dialog has the focus is not enough, because it is lost again a moment later. The dialogs are toplevels of their own and do not need a visible root.
  • Take the focus right before generating a key event, without update() in between. focus_force() sets the focus synchronously, but the update() is where Tk processes the real FocusOut and forgets which window has the focus.
  • Tolerate additional focus events, generated when the window manager takes the focus away and gives it back.
  • Do not check focus_get() and focus_displayof() when the application does not have the focus.
  • Use override-redirect toplevels on X11 in test_wm_stackorder, and wait until they are mapped before lifting them.
  • Resize the toplevel to fit its content in wait_until_mapped(), if the window manager has resized it. Tk remembers a size set by the window manager as if it were set by wm geometry and no longer resizes the toplevel to fit its content, which made NotebookTest fail after a test which left the root window empty.

This fixes 9 failing tests in test_simpledialog, flaky tests in test_filedialog, test_misc, test_ttk and IDLE test_configdialog on X11 with KWin. Most failures do not reproduce without a window manager, which is why the buildbots do not show them.

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) <noreply@anthropic.com>
@ByteFlowing1337

Copy link
Copy Markdown
Contributor

This fixes the reported issue, but there are still some flaky tests under test_misc.py, for example:

======================================================================
FAIL: test_wm_stackorder (test.test_tkinter.test_misc.WmTest.test_wm_stackorder)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/a/cpython/Lib/test/test_tkinter/test_misc.py", line 1335, in test_wm_stackorder
    self.assertGreater(names.index(str(t1)), names.index(str(t2)))
    ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: 1 not greater than 2

======================================================================
======================================================================
FAIL: test_focus (test.test_tkinter.test_misc.EventTest.test_focus)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/a/cpython/Lib/test/test_tkinter/test_misc.py", line 1364, in test_focus
    self.assertEqual(len(events), 1, events)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: 2 != 1 : [<FocusIn event>, <FocusIn event>]

======================================================================

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) <noreply@anthropic.com>
@serhiy-storchaka

Copy link
Copy Markdown
Member Author

test_wm_stackorder reproduces here when the whole WmTest class runs: lift() is issued while the toplevels are still being mapped. Waiting until they are mapped fixes it.

I cannot reproduce the duplicate FocusIn in test_focus. Could you print e.detail and e.send_event for both events? The test checks e.detail == 'NotifyAncestor' on events[0], so relaxing the count may not be enough. Which window manager do you use?

@ByteFlowing1337

Copy link
Copy Markdown
Contributor

I tested on WSL2, so my window manager environment may not be standard. It's Weston.


In test_focus, for both events, e.detail is NotifyAncestor, e.send_event is False.


I applied your patch for fixing test_wm_stackorder, but it still fails:

======================================================================
FAIL: test_wm_stackorder (test.test_tkinter.test_misc.WmTest.test_wm_stackorder)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/a/cpython/Lib/test/test_tkinter/test_misc.py", line 1336, in test_wm_stackorder
    self.assertGreater(names.index(str(t1)), names.index(str(t2)))
    ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: 0 not greater than 2

----------------------------------------------------------------------

If I remove that assertion, the following assertions can still fail:

self.assertIs(t1.wm_stackorder('isabove', t2), True)
self.assertIs(t1.wm_stackorder('isbelow', t2), False)
self.assertIs(t2.wm_stackorder('isbelow', t1), True)

And test_focus_methods can also fail:

======================================================================
FAIL: test_focus_methods (test.test_tkinter.test_misc.MiscTest.test_focus_methods)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/a/cpython/Lib/test/test_tkinter/test_misc.py", line 519, in test_focus_methods
    self.assertIs(self.root.focus_get(), b)
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: None is not <tkinter.Button object .+frame.+button>

----------------------------------------------------------------------

serhiy-storchaka and others added 2 commits September 2, 2026 20:56
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 <noreply@anthropic.com>
@serhiy-storchaka

Copy link
Copy Markdown
Member Author

It's Weston.

Thank you, this was a key point. I was able to reproduce most (not all) issues on Weston. Can you please test the PR and check if anything remains?

@ByteFlowing1337

Copy link
Copy Markdown
Contributor

Thanks! :)

  • test.test_tkinter.test_misc.EventTest.test_focus can still fail on my machine.

  • test.test_tkinter.test_tkinter_pipe.TkinterPipeTest.test_tkinter_pipe_buffered and test.test_tkinter.test_tkinter_pipe.TkinterPipeTest.test_tkinter_pipe_unbuffered fail
    when running test suites from the VSCode terminal, similar to issue test_embed may fail if PYTHONSTARTUP set #157006:

Details
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/a/cpython/Lib/test/test_tkinter/test_tkinter_pipe.py", line 24, in test_tkinter_pipe_buffered
    self.assertEqual(stdout.split(), ['hello', 'goodbye'])
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: Lists differ: ['Ctrl', 'click', 'to', 'launch', 'VS', 'Co[37 chars]bye'] != ['hello', 'goodbye']

First differing element 0:
'Ctrl'
'hello'

First list contains 8 additional elements.
First extra element 2:
'to'

+ ['hello', 'goodbye']
- ['Ctrl',
-  'click',
-  'to',
-  'launch',
-  'VS',
-  'Code',
-  'Native',
-  'REPL',
-  'hello',
-  'goodbye']

======================================================================
FAIL: test_tkinter_pipe_unbuffered (test.test_tkinter.test_tkinter_pipe.TkinterPipeTest.test_tkinter_pipe_unbuffered)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/a/cpython/Lib/test/test_tkinter/test_tkinter_pipe.py", line 39, in test_tkinter_pipe_unbuffered
    self.assertEqual(stdout.strip(), 'hello')
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: 'Ctrl click to launch VS Code Native REPL' != 'hello'
- Ctrl click to launch VS Code Native REPL
+ hello

@serhiy-storchaka

Copy link
Copy Markdown
Member Author

Could you please provide more details about test_focus? I cannot reproduce this.

test_tkinter_pipe_unbuffered is a different issue.

@ByteFlowing1337

Copy link
Copy Markdown
Contributor

For test_focus, the traceback is the same as above:

======================================================================
FAIL: test_focus (test.test_tkinter.test_misc.EventTest.test_focus)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/a/cpython/Lib/test/test_tkinter/test_misc.py", line 1376, in test_focus
    self.assertEqual(len(events), 1, events)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: 2 != 1 : [<FocusIn event>, <FocusIn event>]

----------------------------------------------------------------------

@serhiy-storchaka

Copy link
Copy Markdown
Member Author

Could you please comment out this assertion and check if the test passes? If it fails, try to change the next line e = events[0] to e = events[-1].

All focus events seen by bindings are generated by Tk itself: one in
focus_force(), and one more when the real X focus event arrives and the
window manager had taken the focus away in between.  Only the first
event, generated by focus_force(), is checked.
@ByteFlowing1337

ByteFlowing1337 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Yeah, test_focus passes after I commenting out that assertion.

@serhiy-storchaka

Copy link
Copy Markdown
Member Author

Thank you for your help.

After a test which left the root window without content, the window
manager can resize it to its minimum size.  Tk then remembers that size
as if it were set by "wm geometry" and no longer resizes the toplevel to
fit the content of the following test.
update() between focus_force() and event_generate() gives the window
manager a chance to take the focus away.  Tk then drops the generated
key event, because it delivers it to the window which has the focus.
@serhiy-storchaka serhiy-storchaka added needs backport to 3.13 bugs and security fixes needs backport to 3.14 bugs and security fixes needs backport to 3.15 pre-release feature fixes, bugs and security fixes labels Sep 7, 2026
@serhiy-storchaka serhiy-storchaka changed the title gh-154357: Hide the root window in the tkinter dialog tests gh-154357: Fix tkinter, ttk and IDLE tests depending on the window manager Sep 7, 2026
@serhiy-storchaka
serhiy-storchaka merged commit 3b56438 into python:main Sep 7, 2026
54 checks passed
@miss-islington-app

Copy link
Copy Markdown

Thanks @serhiy-storchaka for the PR 🌮🎉.. I'm working now to backport this PR to: 3.13, 3.14, 3.15.
🐍🍒⛏🤖

@serhiy-storchaka
serhiy-storchaka deleted the gh-154357-dialog-focus-race branch September 7, 2026 11:21
@miss-islington-app

Copy link
Copy Markdown

Sorry, @serhiy-storchaka, I could not cleanly backport this to 3.15 due to a conflict.
Please backport using cherry_picker on command line.

cherry_picker 3b564385e4c966de7f2da9ff8bfbc5e50296dc3d 3.15

@miss-islington-app

Copy link
Copy Markdown

Sorry, @serhiy-storchaka, I could not cleanly backport this to 3.14 due to a conflict.
Please backport using cherry_picker on command line.

cherry_picker 3b564385e4c966de7f2da9ff8bfbc5e50296dc3d 3.14

@miss-islington-app

Copy link
Copy Markdown

Sorry, @serhiy-storchaka, I could not cleanly backport this to 3.13 due to a conflict.
Please backport using cherry_picker on command line.

cherry_picker 3b564385e4c966de7f2da9ff8bfbc5e50296dc3d 3.13

@bedevere-app

bedevere-app Bot commented Sep 7, 2026

Copy link
Copy Markdown

GH-157063 is a backport of this pull request to the 3.15 branch.

@bedevere-app bedevere-app Bot removed the needs backport to 3.15 pre-release feature fixes, bugs and security fixes label Sep 7, 2026
@bedevere-app

bedevere-app Bot commented Sep 7, 2026

Copy link
Copy Markdown

GH-157064 is a backport of this pull request to the 3.14 branch.

@bedevere-app bedevere-app Bot removed the needs backport to 3.14 bugs and security fixes label Sep 7, 2026
@bedevere-app

bedevere-app Bot commented Sep 7, 2026

Copy link
Copy Markdown

GH-157065 is a backport of this pull request to the 3.13 branch.

@bedevere-app bedevere-app Bot removed the needs backport to 3.13 bugs and security fixes label Sep 7, 2026
@bedevere-bot

Copy link
Copy Markdown

⚠️⚠️⚠️ Buildbot failure ⚠️⚠️⚠️

Hi! The buildbot AMD64 Debian root 3.x (tier-1) has failed when building commit 3b56438.

What do you need to do:

  1. Don't panic.
  2. Check the buildbot page in the devguide if you don't know what the buildbots are or how they work.
  3. Go to the page of the buildbot that failed (https://buildbot.python.org/#/builders/345/builds/15283) and take a look at the build logs.
  4. Check if the failure is related to this commit (3b56438) or if it is a false positive.
  5. If the failure is related to this commit, please, reflect that on the issue and make a new Pull Request with a fix.

You can take a look at the buildbot page here:

https://buildbot.python.org/#/builders/345/builds/15283

Failed tests:

  • test.test_multiprocessing_spawn.test_misc

Summary of the results of the build (if available):

==

Click to see traceback logs
remote: Enumerating objects: 27, done.        
remote: Counting objects:   3% (1/27)        
remote: Counting objects:   7% (2/27)        
remote: Counting objects:  11% (3/27)        
remote: Counting objects:  14% (4/27)        
remote: Counting objects:  18% (5/27)        
remote: Counting objects:  22% (6/27)        
remote: Counting objects:  25% (7/27)        
remote: Counting objects:  29% (8/27)        
remote: Counting objects:  33% (9/27)        
remote: Counting objects:  37% (10/27)        
remote: Counting objects:  40% (11/27)        
remote: Counting objects:  44% (12/27)        
remote: Counting objects:  48% (13/27)        
remote: Counting objects:  51% (14/27)        
remote: Counting objects:  55% (15/27)        
remote: Counting objects:  59% (16/27)        
remote: Counting objects:  62% (17/27)        
remote: Counting objects:  66% (18/27)        
remote: Counting objects:  70% (19/27)        
remote: Counting objects:  74% (20/27)        
remote: Counting objects:  77% (21/27)        
remote: Counting objects:  81% (22/27)        
remote: Counting objects:  85% (23/27)        
remote: Counting objects:  88% (24/27)        
remote: Counting objects:  92% (25/27)        
remote: Counting objects:  96% (26/27)        
remote: Counting objects: 100% (27/27)        
remote: Counting objects: 100% (27/27), done.        
remote: Compressing objects:   7% (1/14)        
remote: Compressing objects:  14% (2/14)        
remote: Compressing objects:  21% (3/14)        
remote: Compressing objects:  28% (4/14)        
remote: Compressing objects:  35% (5/14)        
remote: Compressing objects:  42% (6/14)        
remote: Compressing objects:  50% (7/14)        
remote: Compressing objects:  57% (8/14)        
remote: Compressing objects:  64% (9/14)        
remote: Compressing objects:  71% (10/14)        
remote: Compressing objects:  78% (11/14)        
remote: Compressing objects:  85% (12/14)        
remote: Compressing objects:  92% (13/14)        
remote: Compressing objects: 100% (14/14)        
remote: Compressing objects: 100% (14/14), done.        
remote: Total 14 (delta 13), reused 1 (delta 0), pack-reused 0 (from 0)        
From https://github.com/python/cpython
 * branch                    main       -> FETCH_HEAD
Note: switching to '3b564385e4c966de7f2da9ff8bfbc5e50296dc3d'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 3b564385e4c gh-154357: Fix tkinter, ttk and IDLE tests depending on the window manager (GH-154370)
Switched to and reset branch 'main'

configure: WARNING: no system libmpdec found; falling back to pure-Python version for the decimal module
configure: WARNING: pkg-config is missing. Some dependencies may not be detected correctly.

make: *** [Makefile:2471: buildbottest] Error 2

serhiy-storchaka added a commit that referenced this pull request Sep 7, 2026
…ndow manager (GH-154370) (GH-157065)

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 3b56438)
serhiy-storchaka added a commit that referenced this pull request Sep 7, 2026
…ndow 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 3b56438)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants