From 2460120fd196ab8b3ae23c55b196d4b74b186dc4 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 22 Dec 2020 22:48:42 +0200 Subject: [PATCH 1/3] bpo-42721: Improve using simple dialogs without root window 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. --- Lib/tkinter/__init__.py | 23 +++++++++++ Lib/tkinter/commondialog.py | 27 ++++++------- Lib/tkinter/simpledialog.py | 6 ++- .../test/test_tkinter/test_colorchooser.py | 39 +++++++++++++++++++ .../test/test_tkinter/test_messagebox.py | 38 ++++++++++++++++++ .../test/test_tkinter/test_simpledialog.py | 24 +++++++++--- .../2020-12-22-22-47-22.bpo-42721.I5Ai5L.rst | 8 ++++ 7 files changed, 141 insertions(+), 24 deletions(-) create mode 100644 Lib/tkinter/test/test_tkinter/test_colorchooser.py create mode 100644 Lib/tkinter/test/test_tkinter/test_messagebox.py create mode 100644 Misc/NEWS.d/next/Library/2020-12-22-22-47-22.bpo-42721.I5Ai5L.rst diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 1cc18704613809..94e1d834f5eac6 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -300,6 +300,29 @@ def _get_default_root(what=None): return _default_root +def _get_temp_root(): + global _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: + root = Tk() + assert _default_root is root + _default_root = 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 cc3069842c3e46..bb4bd3c12bed72 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) + 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 b882d47c961bdb..58ae848c661f1d 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: @@ -128,7 +129,7 @@ def __init__(self, parent, title = None): ''' master = parent if not master: - master = _get_default_root('create dialog window') + master = _get_temp_root() Toplevel.__init__(self, master) @@ -174,6 +175,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 00000000000000..600c8cde67e762 --- /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 00000000000000..0dec08e9041a0e --- /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 911917258806d1..b64b854c4db7ef 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 00000000000000..3285cd16c06742 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-12-22-22-47-22.bpo-42721.I5Ai5L.rst @@ -0,0 +1,8 @@ +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, 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. From 2611e2b425f2ca11695b39eaf26611d2c8ecafb8 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 24 Dec 2020 15:04:54 +0200 Subject: [PATCH 2/3] Update Lib/tkinter/commondialog.py Co-authored-by: Terry Jan Reedy --- Lib/tkinter/commondialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/tkinter/commondialog.py b/Lib/tkinter/commondialog.py index bb4bd3c12bed72..415e95ac4dcc9a 100644 --- a/Lib/tkinter/commondialog.py +++ b/Lib/tkinter/commondialog.py @@ -41,7 +41,7 @@ def show(self, **options): if master is None: master = _get_temp_root() try: - self._test_callback(master) + 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: From 2dac97c451a9e6b02e89c9d86b6be3d7c8003f74 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 24 Dec 2020 15:43:51 +0200 Subject: [PATCH 3/3] Update the NEWS entry for NoDefaultRoot() and rewrite implementation details. --- Lib/tkinter/__init__.py | 10 ++++++---- .../Library/2020-12-22-22-47-22.bpo-42721.I5Ai5L.rst | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 94e1d834f5eac6..e6a9a4daea6941 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -301,15 +301,17 @@ def _get_default_root(what=None): def _get_temp_root(): - global _default_root + global _support_default_root if not _support_default_root: raise RuntimeError("No master specified and tkinter is " - "configured to not support default root") + "configured to not support default root") root = _default_root if root is None: + assert _support_default_root + _support_default_root = False root = Tk() - assert _default_root is root - _default_root = None + _support_default_root = True + assert _default_root is None root.withdraw() root._temporary = True return root 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 index 3285cd16c06742..58ab180d3bfa31 100644 --- 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 @@ -1,7 +1,8 @@ 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, a new temporary +*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