Skip to content

bpo-41176: revise Tkinter mainloop dispatching flag behavior #21299

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
90a6343
bpo-41176: Expose tkinter dispatching state
richardsheridan Jul 3, 2020
f426278
bpo-41176: Deprecate implicit mainloop waiting behavior
richardsheridan Jul 3, 2020
3350dbc
bpo-41176: additional docstring corrections
richardsheridan Jul 3, 2020
9db47cf
bpo-41176: Correct tkinter test usage of `update`
richardsheridan Jul 3, 2020
2817b2a
bpo-41176: Rename according to comment and use Clinic
richardsheridan Jul 4, 2020
8e4371f
bpo-41176: Address PR comments, add willdispatch test
richardsheridan Jul 5, 2020
3732864
bpo-41176: deduplicate warning code
richardsheridan Jul 5, 2020
6df6433
bpo-41176: rectify typo
richardsheridan Jul 5, 2020
0f609d7
bpo-41176: ACKS
richardsheridan Jul 5, 2020
ad89a27
bpo-41176: fix future behavior
richardsheridan Jul 5, 2020
49ab712
bpo-41176: inline thread flag checks
richardsheridan Jul 5, 2020
ba079da
bpo-41176: internal flag should flip, not just be true
richardsheridan Jul 5, 2020
7c5d9fb
bpo-41176: improve dispatching docstring
richardsheridan Jul 5, 2020
2f22316
bpo-41176: Update whatsnew
richardsheridan Jul 5, 2020
5e9965c
bpo-41176: whitespace
richardsheridan Jul 5, 2020
5872f5b
📜🤖 Added by blurb_it.
blurb-it[bot] Jul 5, 2020
d130740
bpo-41176: data to method
richardsheridan Jul 5, 2020
648434f
bpo-41176: fix future behavior
richardsheridan Jul 6, 2020
44fe5fa
bpo-41176: Make dispatching return an int
richardsheridan Jul 6, 2020
fbddd0f
bpo-41176: attempt to expose CI error
richardsheridan Jul 6, 2020
b58f8a1
bpo-41176: revert internal memory of previous dispatching
richardsheridan Jul 6, 2020
ad7d6ec
bpo-41176: Attempt Ubuntu CI fix
richardsheridan Jul 7, 2020
a5bb180
bpo-41176: Attempt Ubuntu CI fix
richardsheridan Jul 7, 2020
2d557c5
bpo-41176: Quit, mainloop, dispatch docstrings
richardsheridan Jul 8, 2020
ff39387
bpo-41176: WIP Docs in tkinter.rst
richardsheridan Jul 9, 2020
8ea6c5f
bpo-41176: satisfy Doc make check
richardsheridan Jul 9, 2020
e7a0611
bpo-41176: use local max attempts variable
richardsheridan Jul 11, 2020
3165806
bpo-41176: dispatching returns PyBool
richardsheridan Jul 11, 2020
3912603
bpo-41176: update docstrings
richardsheridan Jul 11, 2020
4d81d59
bpo-41176: remove old deprecation warning
richardsheridan Jul 11, 2020
1f16b80
bpo-41176: update test with new deprecation and new future behavior
richardsheridan Jul 11, 2020
4ec839a
bpo-41176: update Docs
richardsheridan Jul 11, 2020
a5e4a86
bpo-41176: Comment new post-deprecation plan
richardsheridan Jul 11, 2020
8efe3fe
bpo-41176: New behavior plan: Doc and Warn fixes
richardsheridan Jul 11, 2020
bf2776c
bpo-41176: Resolve review issues
richardsheridan Jul 12, 2020
1be91e0
bpo-41176: Title underlines should be same length as title
richardsheridan Jul 12, 2020
ad6e882
bpo-41176: reset dispatching flag in test
richardsheridan Jul 12, 2020
6143a18
bpo-41176: layout Tk methods
richardsheridan Jul 12, 2020
516a385
bpo-41176: clarify dispatching() limitations
richardsheridan Jul 12, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions Doc/library/tkinter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,7 @@ Tkinter Modules

Most of the time, :mod:`tkinter` is all you really need, but a number of
additional modules are available as well. The Tk interface is located in a
binary module named :mod:`_tkinter`. This module contains the low-level
interface to Tk, and should never be used directly by application programmers.
It is usually a shared library (or DLL), but might in some cases be statically
linked with the Python interpreter.
binary module named :mod:`_tkinter`.

In addition to the Tk interface module, :mod:`tkinter` includes a number of
Python modules, :mod:`tkinter.constants` being one of the most important.
Expand All @@ -82,10 +79,6 @@ so, usually, to use Tkinter all you need is a simple import statement::

import tkinter

Or, more often::

from tkinter import *


.. class:: Tk(screenName=None, baseName=None, className='Tk', useTk=1)

Expand All @@ -95,6 +88,38 @@ Or, more often::

.. FIXME: The following keyword arguments are currently recognized:

Below are a few of the methods provided by the :class:`Tk` class.

.. sectionauthor:: Richard Sheridan

.. method:: mainloop(threshold)

Enters the main loop of Tkinter. This repeatedly dispatches Tcl events
until either :meth:`Tk.quit` is called, the number of open windows drops
below ``threshold``, or an error occurs while executing events. Usually
the default threshold of 0 is appropriate.

.. method:: quit()

Signals the Tkinter main loop to stop dispatching.
The main loop will exit AFTER the current Tcl event handler is
finished calling. If quit is called outside the context of a Tcl
event, for example from a thread, the main loop will not exit
until after the NEXT event is dispatched. If no more events are
forthcoming, main loop will keep blocking even if quit has been
called. In that case, call ``after(0, quit)`` instead.

.. method:: dispatching()

Determines if the Tkinter main loop is running. Returns True if the main
loop is running, or returns False if the main loop is not running. It is
possible for some entity other than the main loop to be dispatching
events. Some examples are: calling the :meth:`Tk.update` command,
:meth:`_tkinter.tkapp.doonevent`, and the python command line EventHook.
:meth:`Tk.dispatching` does not provide any information about entities
dispatching other than :meth:`Tk.mainloop`.

.. versionadded:: 3.10

.. function:: Tcl(screenName=None, baseName=None, className='Tk', useTk=0)

Expand All @@ -106,7 +131,6 @@ Or, more often::
created by the :func:`Tcl` object can have a Toplevel window created (and the Tk
subsystem initialized) by calling its :meth:`loadtk` method.


Other modules that provide Tk support include:

:mod:`tkinter.colorchooser`
Expand Down
23 changes: 23 additions & 0 deletions Doc/whatsnew/3.10.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,22 @@ Add :data:`sys.orig_argv` attribute: the list of the original command line
arguments passed to the Python executable.
(Contributed by Victor Stinner in :issue:`23427`.)

tkinter
-------

XXX Added :meth:`tkinter.dispatching`, :meth:`tkinter.Misc.dispatching`, and
:meth:`_tkinter.tkapp.dispatching`.
You can reliably call these to determine if the tkinter mainloop is running.

Some internal thread waiting behavior has been deprecated to preserve the
correctness of the new dispatching methods.

Added another new method :meth:`_tkinter.tkapp.setmainloopwaitattempts` which
can be used to obtain future thread waiting behavior.

(see also Deprecated section)
(Contributed by Richard Sheridan in :issue:`41176`.)


Optimizations
=============
Expand All @@ -130,6 +146,13 @@ Optimizations
Deprecated
==========

XXX The :mod:`tkinter` module has deprected the undocumented behavior where a
thread will implicitly wait up to one second for the main thread to enter
the main loop and then fail.
The undocumented :meth:`_tkinter.tkapp.willdispatch` method which sidesteps
this behavior has also been deprecated.
(See also tkinter in the Improved Modules section)
(Contributed by Richard Sheridan in :issue:`41176`.)

Removed
=======
Expand Down
51 changes: 40 additions & 11 deletions Lib/tkinter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,9 +590,20 @@ def get(self):
raise ValueError("invalid literal for getboolean()")


def mainloop(n=0):
"""Run the main loop of Tcl."""
_default_root.tk.mainloop(n)
def mainloop(threshold=0):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an API change that should be flagged?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, this was just meant to make the variable name a bit more descriptive, but it could break (rare) user code that relied on setting n as a keyword argument.

"""Call the main loop of Tkinter."""
_default_root.tk.mainloop(threshold)


def dispatching():
"""Determine if the Tkinter main loop is running.

Returns True if the main loop is running.
Returns False if the main loop is not running.

NOTE: Using update will dispatch events without the main
loop. Dispatching will return False in these cases."""
return _default_root.tk.dispatching()


getint = int
Expand Down Expand Up @@ -1303,13 +1314,13 @@ def winfo_y(self):
self.tk.call('winfo', 'y', self._w))

def update(self):
"""Enter event loop until all pending events have been processed by Tcl."""
"""Process all events, including idle tasks, in the Tcl queue."""
self.tk.call('update')

def update_idletasks(self):
"""Enter event loop until all idle callbacks have been called. This
will update the display of windows but not process events caused by
the user."""
"""Process all idle callbacks in the Tcl queue.
This will update the display of windows but not process events
caused by the user."""
self.tk.call('update', 'idletasks')

def bindtags(self, tagList=None):
Expand Down Expand Up @@ -1417,12 +1428,30 @@ def unbind_class(self, className, sequence):
all functions."""
self.tk.call('bind', className , sequence, '')

def mainloop(self, n=0):
"""Call the mainloop of Tk."""
self.tk.mainloop(n)
def mainloop(self, threshold=0):
"""Call the main loop of Tkinter."""
self.tk.mainloop(threshold)

def dispatching(self):
"""Determine if the Tkinter main loop is running.

Returns True if the main loop is running.
Returns False if the main loop is not running.

NOTE: Using update will dispatch events without the main
loop. Dispatching will return False in these cases."""
return self.tk.dispatching()

def quit(self):
"""Quit the Tcl interpreter. All widgets will be destroyed."""
"""Signal the Tkinter main loop to stop dispatching.

The main loop will exit AFTER the current Tcl event handler is
finished calling. If quit is called outside the context of a Tcl
event, for example from a thread, the main loop will not exit
until after the NEXT event is dispatched. If no more events are
forthcoming, main loop will keep blocking even if quit has been
called. In that case, have the thread call after(0, quit) instead.
"""
self.tk.quit()

def _getints(self, string):
Expand Down
4 changes: 2 additions & 2 deletions Lib/tkinter/test/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def setUpClass(cls):

@classmethod
def tearDownClass(cls):
cls.root.update_idletasks()
cls.root.update()
cls.root.destroy()
del cls.root
tkinter._default_root = None
Expand All @@ -38,7 +38,7 @@ def tearDown(self):

def destroy_default_root():
if getattr(tkinter, '_default_root', None):
tkinter._default_root.update_idletasks()
tkinter._default_root.update()
tkinter._default_root.destroy()
tkinter._default_root = None

Expand Down
100 changes: 98 additions & 2 deletions Lib/tkinter/test/test_tkinter/test_misc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import unittest
import tkinter
import threading
import time
from test import support
from tkinter.test.support import AbstractTkTest

Expand Down Expand Up @@ -109,15 +111,15 @@ def callback(start=0, step=1):
idle1 = root.after_idle(callback)
self.assertIn(idle1, root.tk.call('after', 'info'))
(script, _) = root.tk.splitlist(root.tk.call('after', 'info', idle1))
root.update_idletasks() # Process all pending events.
root.update() # Process all pending events.
self.assertEqual(count, 1)
with self.assertRaises(tkinter.TclError):
root.tk.call(script)

# Set up with callback with args.
count = 0
idle1 = root.after_idle(callback, 42, 11)
root.update_idletasks() # Process all pending events.
root.update() # Process all pending events.
self.assertEqual(count, 53)

# Cancel before called.
Expand Down Expand Up @@ -192,6 +194,100 @@ def test_clipboard_astral(self):
with self.assertRaises(tkinter.TclError):
root.clipboard_get()

def test_mainloop_dispatching(self):
# reconstruct default root destroyed by AbstractTkTest
root = tkinter._default_root = self.root
for obj in (root.tk, root, tkinter):
self.assertFalse(obj.dispatching())
root.after(0, lambda:self.assertTrue(obj.dispatching()))

# guarantees mainloop end after first call to Tcl_DoOneEvent
root.after(0, root.quit)

root.mainloop()
self.assertFalse(obj.dispatching())

def test_willdispatch(self):
root = self.root
self.assertFalse(root.dispatching())
with self.assertWarns(DeprecationWarning):
root.tk.willdispatch()
self.assertTrue(root.dispatching())
# reset dispatching flag
root.after(0,root.quit)
root.mainloop()
self.assertFalse(root.dispatching())

def test_thread_must_wait_for_mainloop(self):
# remove test on eventual WaitForMainloop removal
sentinel = object()
thread_properly_raises = sentinel
thread_dispatching_early = sentinel
thread_dispatching_eventually = sentinel

def target():
nonlocal thread_dispatching_early
nonlocal thread_properly_raises
nonlocal thread_dispatching_eventually

try:
thread_dispatching_early = root.dispatching()
root.after(0) # Null op
except RuntimeError as e:
if str(e) == "main thread is not in main loop":
thread_properly_raises=True
else:
thread_properly_raises=False
raise
else:
thread_properly_raises=False
return
finally:
# must guarantee that any reason not to run mainloop
# is flagged in the above try/except/else and will
# keep the main thread from calling root.mainloop()
ready_for_mainloop.set()

# self.assertTrue(root.dispatching()) but patient
for i in range(1000):
if root.dispatching():
thread_dispatching_eventually = True
break
time.sleep(0.01)
else: # if not break
thread_dispatching_eventually = False
root.after(0, root.quit)

root = self.root

with self.assertWarns(DeprecationWarning):
root.tk.setmainloopwaitattempts(0)

try:
ready_for_mainloop = threading.Event()
thread = threading.Thread(target=target)
self.assertFalse(root.dispatching())
thread.start()
ready_for_mainloop.wait()

# if these fail we don't want to risk starting mainloop
self.assertFalse(thread_dispatching_early is sentinel)
self.assertFalse(thread_dispatching_early)
self.assertFalse(thread_properly_raises is sentinel)
self.assertTrue(thread_properly_raises)

root.mainloop()
self.assertFalse(root.dispatching())
thread.join()
finally:
# this global *must* be reset
with self.assertWarns(DeprecationWarning):
root.tk.setmainloopwaitattempts(10)

self.assertFalse(thread_dispatching_eventually is sentinel)
self.assertTrue(thread_dispatching_eventually)



tests_gui = (MiscTest, )

Expand Down
4 changes: 2 additions & 2 deletions Lib/tkinter/test/test_ttk/test_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
class LabeledScaleTest(AbstractTkTest, unittest.TestCase):

def tearDown(self):
self.root.update_idletasks()
self.root.update()
super().tearDown()

def test_widget_destroy(self):
Expand Down Expand Up @@ -219,7 +219,7 @@ def test_widget_destroy(self):
var = tkinter.StringVar(self.root)
optmenu = ttk.OptionMenu(self.root, var)
name = var._name
optmenu.update_idletasks()
optmenu.update()
optmenu.destroy()
self.assertEqual(optmenu.tk.globalgetvar(name), var.get())
del var
Expand Down
Loading