Skip to content

Commit 3afa68b

Browse files
gh-40440: Add a timeout parameter to tkinter wait_variable()
Without a timeout wait_variable() blocks forever if the variable is never set, for example because the window that would set it was destroyed. The new keyword-only timeout parameter bounds the wait and returns whether the variable was modified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7ccdbab commit 3afa68b

5 files changed

Lines changed: 87 additions & 6 deletions

File tree

Doc/library/tkinter.rst

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,18 +1524,25 @@ Base and mixin classes
15241524
This updates the display of windows, for example after geometry changes,
15251525
but does not process events caused by the user.
15261526

1527-
.. method:: waitvar(name)
1527+
.. method:: waitvar(name, *, timeout=None)
15281528
:no-typesetting:
15291529

1530-
.. method:: wait_variable(name)
1530+
.. method:: wait_variable(name, *, timeout=None)
15311531

15321532
Wait until the Tcl variable *name* is modified, continuing to process
15331533
events in the meantime so that the application stays responsive.
15341534
*name* is usually a :class:`Variable` instance, such as an
15351535
:class:`IntVar` or :class:`StringVar`.
15361536

1537+
If *timeout* is given, it is the maximum time to wait in seconds.
1538+
Return ``True`` if the variable was modified, or ``False`` if the timeout elapsed before that (or the application was destroyed while waiting).
1539+
Without a *timeout* the call blocks until the variable is modified and always returns ``True``.
1540+
15371541
:meth:`waitvar` is an alias of :meth:`!wait_variable`.
15381542

1543+
.. versionchanged:: next
1544+
Added the *timeout* parameter and the return value.
1545+
15391546
.. method:: wait_window(window=None)
15401547

15411548
Wait until *window* is destroyed, continuing to process events in the

Doc/whatsnew/3.16.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,11 @@ tkinter
371371
ttk version, and accepts mappings of button options as *buttons* entries.
372372
(Contributed by Serhiy Storchaka in :gh:`59396`.)
373373

374+
* The :meth:`~tkinter.Misc.wait_variable` method now accepts an optional
375+
keyword-only *timeout* argument that bounds how long it waits, instead of
376+
blocking indefinitely.
377+
(Contributed by Serhiy Storchaka in :gh:`40440`.)
378+
374379
xml
375380
---
376381

Lib/test/test_tkinter/test_misc.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import platform
44
import sys
55
import textwrap
6+
import time
67
import unittest
78
import weakref
89
import tkinter
@@ -600,11 +601,26 @@ def test_wait_variable(self):
600601
var = tkinter.StringVar(self.root)
601602
self.assertEqual(self.root.waitvar, self.root.wait_variable)
602603
self.root.after(1, var.set, 'done')
603-
self.root.wait_variable(var) # Returns once the variable is set.
604+
self.assertIs(self.root.wait_variable(var), True) # Returns once set.
604605
self.assertEqual(var.get(), 'done')
605606
# The name is required (gh-152587).
606607
self.assertRaises(TypeError, self.root.wait_variable)
607608

609+
def test_wait_variable_timeout(self):
610+
var = tkinter.StringVar(self.root)
611+
# The variable is set before the timeout elapses.
612+
self.root.after(1, var.set, 'done')
613+
self.assertIs(
614+
self.root.wait_variable(var, timeout=support.SHORT_TIMEOUT), True)
615+
self.assertEqual(var.get(), 'done')
616+
# The variable is never set: give up when the timeout elapses instead
617+
# of blocking forever (gh-40440).
618+
var.set('')
619+
start = time.monotonic()
620+
self.assertIs(self.root.wait_variable(var, timeout=0.2), False)
621+
self.assertGreaterEqual(time.monotonic() - start, 0.2)
622+
self.assertEqual(var.get(), '')
623+
608624
def test_wait_window(self):
609625
top = tkinter.Toplevel(self.root)
610626
self.root.after(1, top.destroy)

Lib/tkinter/__init__.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -818,12 +818,62 @@ def tk_inactive(self, reset=False, *, displayof=0):
818818
else:
819819
return self.tk.getint(self.tk.call(args))
820820

821-
def wait_variable(self, name):
821+
def wait_variable(self, name, *, timeout=None):
822822
"""Wait until the variable is modified.
823823
824824
A parameter of type IntVar, StringVar, DoubleVar or
825-
BooleanVar must be given."""
826-
self.tk.call('tkwait', 'variable', name)
825+
BooleanVar must be given.
826+
827+
If timeout is given, it specifies the maximum time to wait in
828+
seconds. Return True if the variable was modified, or False if
829+
the timeout elapsed before that (or the application was destroyed
830+
while waiting). Without a timeout the call blocks until the
831+
variable is modified and always returns True."""
832+
if timeout is None:
833+
self.tk.call('tkwait', 'variable', name)
834+
return True
835+
836+
name = str(name)
837+
done = []
838+
timed_out = []
839+
cb = self.register(lambda *args: done.append(True))
840+
try:
841+
self.tk.call('trace', 'add', 'variable', name,
842+
('write', 'unset'), (cb,))
843+
try:
844+
# A one-shot timer guarantees that dooneevent() wakes up at the
845+
# deadline even if no other event arrives.
846+
timer = self.after(max(int(timeout * 1000), 1),
847+
lambda: timed_out.append(True))
848+
try:
849+
while not done and not timed_out:
850+
self.tk.dooneevent(_tkinter.ALL_EVENTS)
851+
# Stop instead of blocking forever if the application was
852+
# destroyed (which also cancels our timer) while waiting.
853+
if not self.tk.getboolean(
854+
self.tk.call('winfo', 'exists', '.')):
855+
break
856+
return bool(done)
857+
except TclError:
858+
# the interpreter or application was torn down
859+
return bool(done)
860+
finally:
861+
# Cleanup may fail if the application was torn down.
862+
try:
863+
self.after_cancel(timer)
864+
except TclError:
865+
pass
866+
finally:
867+
try:
868+
self.tk.call('trace', 'remove', 'variable', name,
869+
('write', 'unset'), cb)
870+
except TclError:
871+
pass
872+
finally:
873+
try:
874+
self.deletecommand(cb)
875+
except TclError:
876+
pass
827877
waitvar = wait_variable # XXX b/w compat
828878

829879
def wait_window(self, window=None):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add an optional *timeout* parameter to :meth:`tkinter.Misc.wait_variable`.
2+
If the variable is not modified within the given time, the call returns
3+
``False`` instead of blocking indefinitely.

0 commit comments

Comments
 (0)