diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index a32eb76d3b9..fec56553e42 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 12e42fe14ac..e595c99defb 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 @@ class Dialog: 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 d9762b1351a..a66fbd6cb98 100644 --- a/Lib/tkinter/simpledialog.py +++ b/Lib/tkinter/simpledialog.py @@ -24,7 +24,8 @@ askstring -- get a string from the user """ 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 @@ class Dialog(Toplevel): ''' 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 @@ class Dialog(Toplevel): '''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 00000000000..600c8cde67e --- /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 00000000000..0dec08e9041 --- /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 91191725880..b64b854c4db 100644 --- a/Lib/tkinter/test/test_tkinter/test_simpledialog.py +++ b/Lib/tkinter/test/test_tkinter/test_simpledialog.py @@ -10,13 +10,25 @@ requires('gui') 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") - root.destroy() - tkinter.NoDefaultRoot() - self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line number") + self.assertEqual(ismapped, False) + + root = tkinter.Tk() + ismapped = None + 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 00000000000..58ab180d3bf --- /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.