[Python-checkins] bpo-42721: Improve using simple dialogs without root window (GH-23897)

serhiy-storchaka webhook-mailer at python.org
Fri Dec 25 13:19:28 EST 2020


https://github.com/python/cpython/commit/675c97eb6c7c14c6a68ebf476c52931c1e5c1220
commit: 675c97eb6c7c14c6a68ebf476c52931c1e5c1220
branch: master
author: Serhiy Storchaka <storchaka at gmail.com>
committer: serhiy-storchaka <storchaka at gmail.com>
date: 2020-12-25T20:19:20+02:00
summary:

bpo-42721: Improve using simple dialogs without root window (GH-23897)

When simple query dialogs (tkinter.simpledialog), message boxes
(tkinter.messagebox) or color choose dialog (tkinter.colorchooser)
are created without arguments master and parent, and the default
root window is not yet created, a new temporary hidden root window
will be created automatically. It will not be set as the default root
window and will be destroyed right after closing the dialog window.
It will help to use these simple dialog windows in programs which do
not need other GUI.

Previously, message boxes and color chooser created the blank root
window and left it after closing the dialog window, and query dialogs
just raised an exception.

Co-authored-by: Terry Jan Reedy <tjreedy at udel.edu>

files:
A Lib/tkinter/test/test_tkinter/test_colorchooser.py
A Lib/tkinter/test/test_tkinter/test_messagebox.py
A Misc/NEWS.d/next/Library/2020-12-22-22-47-22.bpo-42721.I5Ai5L.rst
M Lib/tkinter/__init__.py
M Lib/tkinter/commondialog.py
M Lib/tkinter/simpledialog.py
M Lib/tkinter/test/test_tkinter/test_simpledialog.py

diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py
index a32eb76d3b97a..fec56553e4208 100644
--- a/Lib/tkinter/__init__.py
+++ b/Lib/tkinter/__init__.py
@@ -300,6 +300,31 @@ def _get_default_root(what=None):
     return _default_root
 
 
+def _get_temp_root():
+    global _support_default_root
+    if not _support_default_root:
+        raise RuntimeError("No master specified and tkinter is "
+                           "configured to not support default root")
+    root = _default_root
+    if root is None:
+        assert _support_default_root
+        _support_default_root = False
+        root = Tk()
+        _support_default_root = True
+        assert _default_root is None
+        root.withdraw()
+        root._temporary = True
+    return root
+
+
+def _destroy_temp_root(master):
+    if getattr(master, '_temporary', False):
+        try:
+            master.destroy()
+        except TclError:
+            pass
+
+
 def _tkerror(err):
     """Internal function."""
     pass
diff --git a/Lib/tkinter/commondialog.py b/Lib/tkinter/commondialog.py
index 12e42fe14ac45..e595c99defb99 100644
--- a/Lib/tkinter/commondialog.py
+++ b/Lib/tkinter/commondialog.py
@@ -10,7 +10,7 @@
 
 __all__ = ["Dialog"]
 
-from tkinter import Frame
+from tkinter import Frame, _get_temp_root, _destroy_temp_root
 
 
 class Dialog:
@@ -37,22 +37,17 @@ def show(self, **options):
 
         self._fixoptions()
 
-        # we need a dummy widget to properly process the options
-        # (at least as long as we use Tkinter 1.63)
-        w = Frame(self.master)
-
+        master = self.master
+        if master is None:
+            master = _get_temp_root()
         try:
-
-            s = w.tk.call(self.command, *w._options(self.options))
-
-            s = self._fixresult(w, s)
-
+            self._test_callback(master)  # The function below is replaced for some tests.
+            s = master.tk.call(self.command, *master._options(self.options))
+            s = self._fixresult(master, s)
         finally:
-
-            try:
-                # get rid of the widget
-                w.destroy()
-            except:
-                pass
+            _destroy_temp_root(master)
 
         return s
+
+    def _test_callback(self, master):
+        pass
diff --git a/Lib/tkinter/simpledialog.py b/Lib/tkinter/simpledialog.py
index d9762b1351a24..a66fbd6cb9885 100644
--- a/Lib/tkinter/simpledialog.py
+++ b/Lib/tkinter/simpledialog.py
@@ -24,7 +24,8 @@
 """
 
 from tkinter import *
-from tkinter import messagebox, _get_default_root
+from tkinter import _get_temp_root, _destroy_temp_root
+from tkinter import messagebox
 
 
 class SimpleDialog:
@@ -100,7 +101,7 @@ def __init__(self, parent, title = None):
         '''
         master = parent
         if master is None:
-            master = _get_default_root('create dialog window')
+            master = _get_temp_root()
 
         Toplevel.__init__(self, master)
 
@@ -142,6 +143,7 @@ def destroy(self):
         '''Destroy the window'''
         self.initial_focus = None
         Toplevel.destroy(self)
+        _destroy_temp_root(self.master)
 
     #
     # construction hooks
diff --git a/Lib/tkinter/test/test_tkinter/test_colorchooser.py b/Lib/tkinter/test/test_tkinter/test_colorchooser.py
new file mode 100644
index 0000000000000..600c8cde67e76
--- /dev/null
+++ b/Lib/tkinter/test/test_tkinter/test_colorchooser.py
@@ -0,0 +1,39 @@
+import unittest
+import tkinter
+from test.support import requires, run_unittest, swap_attr
+from tkinter.test.support import AbstractDefaultRootTest
+from tkinter.commondialog import Dialog
+from tkinter.colorchooser import askcolor
+
+requires('gui')
+
+
+class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
+
+    def test_askcolor(self):
+        def test_callback(dialog, master):
+            nonlocal ismapped
+            master.update()
+            ismapped = master.winfo_ismapped()
+            raise ZeroDivisionError
+
+        with swap_attr(Dialog, '_test_callback', test_callback):
+            ismapped = None
+            self.assertRaises(ZeroDivisionError, askcolor)
+            #askcolor()
+            self.assertEqual(ismapped, False)
+
+            root = tkinter.Tk()
+            ismapped = None
+            self.assertRaises(ZeroDivisionError, askcolor)
+            self.assertEqual(ismapped, True)
+            root.destroy()
+
+            tkinter.NoDefaultRoot()
+            self.assertRaises(RuntimeError, askcolor)
+
+
+tests_gui = (DefaultRootTest,)
+
+if __name__ == "__main__":
+    run_unittest(*tests_gui)
diff --git a/Lib/tkinter/test/test_tkinter/test_messagebox.py b/Lib/tkinter/test/test_tkinter/test_messagebox.py
new file mode 100644
index 0000000000000..0dec08e9041a0
--- /dev/null
+++ b/Lib/tkinter/test/test_tkinter/test_messagebox.py
@@ -0,0 +1,38 @@
+import unittest
+import tkinter
+from test.support import requires, run_unittest, swap_attr
+from tkinter.test.support import AbstractDefaultRootTest
+from tkinter.commondialog import Dialog
+from tkinter.messagebox import showinfo
+
+requires('gui')
+
+
+class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
+
+    def test_showinfo(self):
+        def test_callback(dialog, master):
+            nonlocal ismapped
+            master.update()
+            ismapped = master.winfo_ismapped()
+            raise ZeroDivisionError
+
+        with swap_attr(Dialog, '_test_callback', test_callback):
+            ismapped = None
+            self.assertRaises(ZeroDivisionError, showinfo, "Spam", "Egg Information")
+            self.assertEqual(ismapped, False)
+
+            root = tkinter.Tk()
+            ismapped = None
+            self.assertRaises(ZeroDivisionError, showinfo, "Spam", "Egg Information")
+            self.assertEqual(ismapped, True)
+            root.destroy()
+
+            tkinter.NoDefaultRoot()
+            self.assertRaises(RuntimeError, showinfo, "Spam", "Egg Information")
+
+
+tests_gui = (DefaultRootTest,)
+
+if __name__ == "__main__":
+    run_unittest(*tests_gui)
diff --git a/Lib/tkinter/test/test_tkinter/test_simpledialog.py b/Lib/tkinter/test/test_tkinter/test_simpledialog.py
index 911917258806d..b64b854c4db7e 100644
--- a/Lib/tkinter/test/test_tkinter/test_simpledialog.py
+++ b/Lib/tkinter/test/test_tkinter/test_simpledialog.py
@@ -10,13 +10,25 @@
 class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
 
     def test_askinteger(self):
-        self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line number")
-        root = tkinter.Tk()
-        with swap_attr(Dialog, 'wait_window', lambda self, w: w.destroy()):
+        @staticmethod
+        def mock_wait_window(w):
+            nonlocal ismapped
+            ismapped = w.master.winfo_ismapped()
+            w.destroy()
+
+        with swap_attr(Dialog, 'wait_window', mock_wait_window):
+            ismapped = None
+            askinteger("Go To Line", "Line number")
+            self.assertEqual(ismapped, False)
+
+            root = tkinter.Tk()
+            ismapped = None
             askinteger("Go To Line", "Line number")
-        root.destroy()
-        tkinter.NoDefaultRoot()
-        self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line number")
+            self.assertEqual(ismapped, True)
+            root.destroy()
+
+            tkinter.NoDefaultRoot()
+            self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line number")
 
 
 tests_gui = (DefaultRootTest,)
diff --git a/Misc/NEWS.d/next/Library/2020-12-22-22-47-22.bpo-42721.I5Ai5L.rst b/Misc/NEWS.d/next/Library/2020-12-22-22-47-22.bpo-42721.I5Ai5L.rst
new file mode 100644
index 0000000000000..58ab180d3bfa3
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2020-12-22-22-47-22.bpo-42721.I5Ai5L.rst
@@ -0,0 +1,9 @@
+When simple query dialogs (:mod:`tkinter.simpledialog`), message boxes
+(:mod:`tkinter.messagebox`) or color choose dialog
+(:mod:`tkinter.colorchooser`) are created without arguments *master* and
+*parent*, and the default root window is not yet created, and
+:func:`~tkinter.NoDefaultRoot` was not called, a new temporal
+hidden root window will be created automatically. It will not be set as the
+default root window and will be destroyed right after closing the dialog
+window. It will help to use these simple dialog windows in programs which
+do not need other GUI.



More information about the Python-checkins mailing list