[Python-checkins] [3.9] bpo-42630: Improve error reporting in Tkinter for absent default root (GH-23781) (GH-23853)

serhiy-storchaka webhook-mailer at python.org
Sat Dec 19 06:08:32 EST 2020


https://github.com/python/cpython/commit/87e7a14ee3bd7dc495e51166598453114342d0bf
commit: 87e7a14ee3bd7dc495e51166598453114342d0bf
branch: 3.9
author: Serhiy Storchaka <storchaka at gmail.com>
committer: serhiy-storchaka <storchaka at gmail.com>
date: 2020-12-19T13:08:07+02:00
summary:

[3.9] bpo-42630: Improve error reporting in Tkinter for absent default root (GH-23781) (GH-23853)

* Tkinter functions and constructors which need a default root window
  raise now RuntimeError with descriptive message instead of obscure
  AttributeError or NameError if it is not created yet or cannot
  be created automatically.

* Add tests for all functions which use default root window.

* Fix import in the pynche script.

(cherry picked from commit 3d569fd6dccf9f582bafaca04d3535094cae393e)

files:
A Lib/tkinter/test/test_tkinter/test_simpledialog.py
A Misc/NEWS.d/next/Library/2020-12-15-17-51-27.bpo-42630.jf4jBl.rst
M Lib/idlelib/pyshell.py
M Lib/test/test_idle.py
M Lib/tkinter/__init__.py
M Lib/tkinter/commondialog.py
M Lib/tkinter/font.py
M Lib/tkinter/simpledialog.py
M Lib/tkinter/test/support.py
M Lib/tkinter/test/test_tkinter/test_font.py
M Lib/tkinter/test/test_tkinter/test_images.py
M Lib/tkinter/test/test_tkinter/test_misc.py
M Lib/tkinter/test/test_tkinter/test_variables.py
M Lib/tkinter/test/test_tkinter/test_widgets.py
M Lib/tkinter/test/test_ttk/test_extensions.py
M Lib/tkinter/test/test_ttk/test_widgets.py
M Lib/tkinter/tix.py
M Lib/tkinter/ttk.py
M Tools/pynche/PyncheWidget.py

diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py
index adc302883ae66..6fa138219a24d 100755
--- a/Lib/idlelib/pyshell.py
+++ b/Lib/idlelib/pyshell.py
@@ -1061,8 +1061,10 @@ def begin(self):
                    (sys.version, sys.platform, self.COPYRIGHT, nosub))
         self.text.focus_force()
         self.showprompt()
+        # User code should use separate default Tk root window
         import tkinter
-        tkinter._default_root = None # 03Jan04 KBK What's this?
+        tkinter._support_default_root = True
+        tkinter._default_root = None
         return True
 
     def stop_readline(self):
diff --git a/Lib/test/test_idle.py b/Lib/test/test_idle.py
index 8bc01deaa3384..310b72c1d72e7 100644
--- a/Lib/test/test_idle.py
+++ b/Lib/test/test_idle.py
@@ -20,5 +20,5 @@
 if __name__ == '__main__':
     tk.NoDefaultRoot()
     unittest.main(exit=False)
-    tk._support_default_root = 1
+    tk._support_default_root = True
     tk._default_root = None
diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py
index 2175afcffa435..bf0342e27b21f 100644
--- a/Lib/tkinter/__init__.py
+++ b/Lib/tkinter/__init__.py
@@ -270,7 +270,7 @@ def __repr__(self):
         )
 
 
-_support_default_root = 1
+_support_default_root = True
 _default_root = None
 
 
@@ -280,13 +280,26 @@ def NoDefaultRoot():
     Call this function to inhibit that the first instance of
     Tk is used for windows without an explicit parent window.
     """
-    global _support_default_root
-    _support_default_root = 0
-    global _default_root
+    global _support_default_root, _default_root
+    _support_default_root = False
+    # Delete, so any use of _default_root will immediately raise an exception.
+    # Rebind before deletion, so repeated calls will not fail.
     _default_root = None
     del _default_root
 
 
+def _get_default_root(what=None):
+    if not _support_default_root:
+        raise RuntimeError("No master specified and tkinter is "
+                           "configured to not support default root")
+    if not _default_root:
+        if what:
+            raise RuntimeError(f"Too early to {what}: no default root window")
+        root = Tk()
+        assert _default_root is root
+    return _default_root
+
+
 def _tkerror(err):
     """Internal function."""
     pass
@@ -330,7 +343,7 @@ def __init__(self, master=None, value=None, name=None):
             raise TypeError("name must be a string")
         global _varnum
         if not master:
-            master = _default_root
+            master = _get_default_root('create variable')
         self._root = master._root()
         self._tk = master.tk
         if name:
@@ -591,7 +604,7 @@ def get(self):
 
 def mainloop(n=0):
     """Run the main loop of Tcl."""
-    _default_root.tk.mainloop(n)
+    _get_default_root('run the main loop').tk.mainloop(n)
 
 
 getint = int
@@ -600,9 +613,9 @@ def mainloop(n=0):
 
 
 def getboolean(s):
-    """Convert true and false to integer values 1 and 0."""
+    """Convert Tcl object to True or False."""
     try:
-        return _default_root.tk.getboolean(s)
+        return _get_default_root('use getboolean()').tk.getboolean(s)
     except TclError:
         raise ValueError("invalid literal for getboolean()")
 
@@ -2248,7 +2261,7 @@ def __init__(self, screenName=None, baseName=None, className='Tk',
         is the name of the widget class."""
         self.master = None
         self.children = {}
-        self._tkloaded = 0
+        self._tkloaded = False
         # to avoid recursions in the getattr code in case of failure, we
         # ensure that self.tk is always _something_.
         self.tk = None
@@ -2272,7 +2285,7 @@ def loadtk(self):
             self._loadtk()
 
     def _loadtk(self):
-        self._tkloaded = 1
+        self._tkloaded = True
         global _default_root
         # Version sanity checks
         tk_version = self.tk.getvar('tk_version')
@@ -2521,12 +2534,8 @@ class BaseWidget(Misc):
 
     def _setup(self, master, cnf):
         """Internal function. Sets up information about children."""
-        if _support_default_root:
-            global _default_root
-            if not master:
-                if not _default_root:
-                    _default_root = Tk()
-                master = _default_root
+        if not master:
+            master = _get_default_root()
         self.master = master
         self.tk = master.tk
         name = None
@@ -3990,9 +3999,7 @@ class Image:
     def __init__(self, imgtype, name=None, cnf={}, master=None, **kw):
         self.name = None
         if not master:
-            master = _default_root
-            if not master:
-                raise RuntimeError('Too early to create image')
+            master = _get_default_root('create image')
         self.tk = getattr(master, 'tk', master)
         if not name:
             Image._last_id += 1
@@ -4146,11 +4153,13 @@ def __init__(self, name=None, cnf={}, master=None, **kw):
 
 
 def image_names():
-    return _default_root.tk.splitlist(_default_root.tk.call('image', 'names'))
+    tk = _get_default_root('use image_names()').tk
+    return tk.splitlist(tk.call('image', 'names'))
 
 
 def image_types():
-    return _default_root.tk.splitlist(_default_root.tk.call('image', 'types'))
+    tk = _get_default_root('use image_types()').tk
+    return tk.splitlist(tk.call('image', 'types'))
 
 
 class Spinbox(Widget, XView):
diff --git a/Lib/tkinter/commondialog.py b/Lib/tkinter/commondialog.py
index e56b5baf7d1e1..cc3069842c3e4 100644
--- a/Lib/tkinter/commondialog.py
+++ b/Lib/tkinter/commondialog.py
@@ -18,10 +18,10 @@ class Dialog:
     command = None
 
     def __init__(self, master=None, **options):
+        if not master:
+            master = options.get('parent')
         self.master = master
         self.options = options
-        if not master and options.get('parent'):
-            self.master = options['parent']
 
     def _fixoptions(self):
         pass # hook
diff --git a/Lib/tkinter/font.py b/Lib/tkinter/font.py
index 15ad7ab4b63a8..2b58030c1fb5a 100644
--- a/Lib/tkinter/font.py
+++ b/Lib/tkinter/font.py
@@ -69,7 +69,7 @@ def _mkdict(self, args):
     def __init__(self, root=None, font=None, name=None, exists=False,
                  **options):
         if not root:
-            root = tkinter._default_root
+            root = tkinter._get_default_root('use font')
         tk = getattr(root, 'tk', root)
         if font:
             # get actual settings corresponding to the given font
@@ -180,7 +180,7 @@ def metrics(self, *options, **kw):
 def families(root=None, displayof=None):
     "Get font families (as a tuple)"
     if not root:
-        root = tkinter._default_root
+        root = tkinter._get_default_root('use font.families()')
     args = ()
     if displayof:
         args = ('-displayof', displayof)
@@ -190,7 +190,7 @@ def families(root=None, displayof=None):
 def names(root=None):
     "Get names of defined fonts (as a tuple)"
     if not root:
-        root = tkinter._default_root
+        root = tkinter._get_default_root('use font.names()')
     return root.tk.splitlist(root.tk.call("font", "names"))
 
 
diff --git a/Lib/tkinter/simpledialog.py b/Lib/tkinter/simpledialog.py
index 85244171117b6..b882d47c961bd 100644
--- a/Lib/tkinter/simpledialog.py
+++ b/Lib/tkinter/simpledialog.py
@@ -24,9 +24,7 @@
 """
 
 from tkinter import *
-from tkinter import messagebox
-
-import tkinter # used at _QueryDialog for tkinter._default_root
+from tkinter import messagebox, _get_default_root
 
 
 class SimpleDialog:
@@ -128,13 +126,17 @@ def __init__(self, parent, title = None):
 
             title -- the dialog title
         '''
-        Toplevel.__init__(self, parent)
+        master = parent
+        if not master:
+            master = _get_default_root('create dialog window')
+
+        Toplevel.__init__(self, master)
 
         self.withdraw() # remain invisible for now
-        # If the master is not viewable, don't
+        # If the parent is not viewable, don't
         # make the child transient, or else it
         # would be opened withdrawn
-        if parent.winfo_viewable():
+        if parent is not None and parent.winfo_viewable():
             self.transient(parent)
 
         if title:
@@ -155,7 +157,7 @@ def __init__(self, parent, title = None):
 
         self.protocol("WM_DELETE_WINDOW", self.cancel)
 
-        if self.parent is not None:
+        if parent is not None:
             self.geometry("+%d+%d" % (parent.winfo_rootx()+50,
                                       parent.winfo_rooty()+50))
 
@@ -259,9 +261,6 @@ def __init__(self, title, prompt,
                  minvalue = None, maxvalue = None,
                  parent = None):
 
-        if not parent:
-            parent = tkinter._default_root
-
         self.prompt   = prompt
         self.minvalue = minvalue
         self.maxvalue = maxvalue
diff --git a/Lib/tkinter/test/support.py b/Lib/tkinter/test/support.py
index 467a0b66c265c..dbc47a81e6515 100644
--- a/Lib/tkinter/test/support.py
+++ b/Lib/tkinter/test/support.py
@@ -36,6 +36,33 @@ def tearDown(self):
             w.destroy()
         self.root.withdraw()
 
+
+class AbstractDefaultRootTest:
+
+    def setUp(self):
+        self._old_support_default_root = tkinter._support_default_root
+        destroy_default_root()
+        tkinter._support_default_root = True
+        self.wantobjects = tkinter.wantobjects
+
+    def tearDown(self):
+        destroy_default_root()
+        tkinter._default_root = None
+        tkinter._support_default_root = self._old_support_default_root
+
+    def _test_widget(self, constructor):
+        # no master passing
+        x = constructor()
+        self.assertIsNotNone(tkinter._default_root)
+        self.assertIs(x.master, tkinter._default_root)
+        self.assertIs(x.tk, tkinter._default_root.tk)
+        x.destroy()
+        destroy_default_root()
+        tkinter.NoDefaultRoot()
+        self.assertRaises(RuntimeError, constructor)
+        self.assertFalse(hasattr(tkinter, '_default_root'))
+
+
 def destroy_default_root():
     if getattr(tkinter, '_default_root', None):
         tkinter._default_root.update_idletasks()
diff --git a/Lib/tkinter/test/test_tkinter/test_font.py b/Lib/tkinter/test/test_tkinter/test_font.py
index a021ea336807b..28d8c7b2f8963 100644
--- a/Lib/tkinter/test/test_tkinter/test_font.py
+++ b/Lib/tkinter/test/test_tkinter/test_font.py
@@ -2,7 +2,7 @@
 import tkinter
 from tkinter import font
 from test.support import requires, run_unittest, gc_collect, ALWAYS_EQ
-from tkinter.test.support import AbstractTkTest
+from tkinter.test.support import AbstractTkTest, AbstractDefaultRootTest
 
 requires('gui')
 
@@ -101,7 +101,38 @@ def test_names(self):
             self.assertTrue(name)
         self.assertIn(fontname, names)
 
-tests_gui = (FontTest, )
+
+class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
+
+    def test_families(self):
+        self.assertRaises(RuntimeError, font.families)
+        root = tkinter.Tk()
+        families = font.families()
+        self.assertIsInstance(families, tuple)
+        self.assertTrue(families)
+        for family in families:
+            self.assertIsInstance(family, str)
+            self.assertTrue(family)
+        root.destroy()
+        tkinter.NoDefaultRoot()
+        self.assertRaises(RuntimeError, font.families)
+
+    def test_names(self):
+        self.assertRaises(RuntimeError, font.names)
+        root = tkinter.Tk()
+        names = font.names()
+        self.assertIsInstance(names, tuple)
+        self.assertTrue(names)
+        for name in names:
+            self.assertIsInstance(name, str)
+            self.assertTrue(name)
+        self.assertIn(fontname, names)
+        root.destroy()
+        tkinter.NoDefaultRoot()
+        self.assertRaises(RuntimeError, font.names)
+
+
+tests_gui = (FontTest, DefaultRootTest)
 
 if __name__ == "__main__":
     run_unittest(*tests_gui)
diff --git a/Lib/tkinter/test/test_tkinter/test_images.py b/Lib/tkinter/test/test_tkinter/test_images.py
index 2805d35a1f5b1..94bba8518fdaa 100644
--- a/Lib/tkinter/test/test_tkinter/test_images.py
+++ b/Lib/tkinter/test/test_tkinter/test_images.py
@@ -1,7 +1,7 @@
 import unittest
 import tkinter
 from test import support
-from tkinter.test.support import AbstractTkTest, requires_tcl
+from tkinter.test.support import AbstractTkTest, AbstractDefaultRootTest, requires_tcl
 
 support.requires('gui')
 
@@ -19,6 +19,47 @@ def test_image_names(self):
         self.assertIsInstance(image_names, tuple)
 
 
+class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
+
+    def test_image_types(self):
+        self.assertRaises(RuntimeError, tkinter.image_types)
+        root = tkinter.Tk()
+        image_types = tkinter.image_types()
+        self.assertIsInstance(image_types, tuple)
+        self.assertIn('photo', image_types)
+        self.assertIn('bitmap', image_types)
+        root.destroy()
+        tkinter.NoDefaultRoot()
+        self.assertRaises(RuntimeError, tkinter.image_types)
+
+    def test_image_names(self):
+        self.assertRaises(RuntimeError, tkinter.image_names)
+        root = tkinter.Tk()
+        image_names = tkinter.image_names()
+        self.assertIsInstance(image_names, tuple)
+        root.destroy()
+        tkinter.NoDefaultRoot()
+        self.assertRaises(RuntimeError, tkinter.image_names)
+
+    def test_image_create_bitmap(self):
+        self.assertRaises(RuntimeError, tkinter.BitmapImage)
+        root = tkinter.Tk()
+        image = tkinter.BitmapImage()
+        self.assertIn(image.name, tkinter.image_names())
+        root.destroy()
+        tkinter.NoDefaultRoot()
+        self.assertRaises(RuntimeError, tkinter.BitmapImage)
+
+    def test_image_create_photo(self):
+        self.assertRaises(RuntimeError, tkinter.PhotoImage)
+        root = tkinter.Tk()
+        image = tkinter.PhotoImage()
+        self.assertIn(image.name, tkinter.image_names())
+        root.destroy()
+        tkinter.NoDefaultRoot()
+        self.assertRaises(RuntimeError, tkinter.PhotoImage)
+
+
 class BitmapImageTest(AbstractTkTest, unittest.TestCase):
 
     @classmethod
@@ -330,7 +371,7 @@ def test_transparency(self):
         self.assertEqual(image.transparency_get(4, 6), False)
 
 
-tests_gui = (MiscTest, BitmapImageTest, PhotoImageTest,)
+tests_gui = (MiscTest, DefaultRootTest, BitmapImageTest, PhotoImageTest,)
 
 if __name__ == "__main__":
     support.run_unittest(*tests_gui)
diff --git a/Lib/tkinter/test/test_tkinter/test_misc.py b/Lib/tkinter/test/test_tkinter/test_misc.py
index b8eea2544f522..585d81ddf9f2d 100644
--- a/Lib/tkinter/test/test_tkinter/test_misc.py
+++ b/Lib/tkinter/test/test_tkinter/test_misc.py
@@ -1,7 +1,7 @@
 import unittest
 import tkinter
 from test import support
-from tkinter.test.support import AbstractTkTest
+from tkinter.test.support import AbstractTkTest, AbstractDefaultRootTest
 
 support.requires('gui')
 
@@ -241,7 +241,85 @@ def test_event_repr(self):
                          " num=3 delta=-1 focus=True"
                          " x=10 y=20 width=300 height=200>")
 
-tests_gui = (MiscTest, )
+    def test_getboolean(self):
+        for v in 'true', 'yes', 'on', '1', 't', 'y', 1, True:
+            self.assertIs(self.root.getboolean(v), True)
+        for v in 'false', 'no', 'off', '0', 'f', 'n', 0, False:
+            self.assertIs(self.root.getboolean(v), False)
+        self.assertRaises(ValueError, self.root.getboolean, 'yea')
+        self.assertRaises(ValueError, self.root.getboolean, '')
+        self.assertRaises(TypeError, self.root.getboolean, None)
+        self.assertRaises(TypeError, self.root.getboolean, ())
+
+    def test_mainloop(self):
+        log = []
+        def callback():
+            log.append(1)
+            self.root.after(100, self.root.quit)
+        self.root.after(100, callback)
+        self.root.mainloop(1)
+        self.assertEqual(log, [])
+        self.root.mainloop(0)
+        self.assertEqual(log, [1])
+        self.assertTrue(self.root.winfo_exists())
+
+
+class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
+
+    def test_default_root(self):
+        self.assertIs(tkinter._support_default_root, True)
+        self.assertIsNone(tkinter._default_root)
+        root = tkinter.Tk()
+        root2 = tkinter.Tk()
+        root3 = tkinter.Tk()
+        self.assertIs(tkinter._default_root, root)
+        root2.destroy()
+        self.assertIs(tkinter._default_root, root)
+        root.destroy()
+        self.assertIsNone(tkinter._default_root)
+        root3.destroy()
+        self.assertIsNone(tkinter._default_root)
+
+    def test_no_default_root(self):
+        self.assertIs(tkinter._support_default_root, True)
+        self.assertIsNone(tkinter._default_root)
+        root = tkinter.Tk()
+        self.assertIs(tkinter._default_root, root)
+        tkinter.NoDefaultRoot()
+        self.assertIs(tkinter._support_default_root, False)
+        self.assertFalse(hasattr(tkinter, '_default_root'))
+        # repeated call is no-op
+        tkinter.NoDefaultRoot()
+        self.assertIs(tkinter._support_default_root, False)
+        self.assertFalse(hasattr(tkinter, '_default_root'))
+        root.destroy()
+        self.assertIs(tkinter._support_default_root, False)
+        self.assertFalse(hasattr(tkinter, '_default_root'))
+        root = tkinter.Tk()
+        self.assertIs(tkinter._support_default_root, False)
+        self.assertFalse(hasattr(tkinter, '_default_root'))
+        root.destroy()
+
+    def test_getboolean(self):
+        self.assertRaises(RuntimeError, tkinter.getboolean, '1')
+        root = tkinter.Tk()
+        self.assertIs(tkinter.getboolean('1'), True)
+        self.assertRaises(ValueError, tkinter.getboolean, 'yea')
+        root.destroy()
+        tkinter.NoDefaultRoot()
+        self.assertRaises(RuntimeError, tkinter.getboolean, '1')
+
+    def test_mainloop(self):
+        self.assertRaises(RuntimeError, tkinter.mainloop)
+        root = tkinter.Tk()
+        root.after_idle(root.quit)
+        tkinter.mainloop()
+        root.destroy()
+        tkinter.NoDefaultRoot()
+        self.assertRaises(RuntimeError, tkinter.mainloop)
+
+
+tests_gui = (MiscTest, DefaultRootTest)
 
 if __name__ == "__main__":
     support.run_unittest(*tests_gui)
diff --git a/Lib/tkinter/test/test_tkinter/test_simpledialog.py b/Lib/tkinter/test/test_tkinter/test_simpledialog.py
new file mode 100644
index 0000000000000..911917258806d
--- /dev/null
+++ b/Lib/tkinter/test/test_tkinter/test_simpledialog.py
@@ -0,0 +1,25 @@
+import unittest
+import tkinter
+from test.support import requires, run_unittest, swap_attr
+from tkinter.test.support import AbstractDefaultRootTest
+from tkinter.simpledialog import Dialog, askinteger
+
+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()):
+            askinteger("Go To Line", "Line number")
+        root.destroy()
+        tkinter.NoDefaultRoot()
+        self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line number")
+
+
+tests_gui = (DefaultRootTest,)
+
+if __name__ == "__main__":
+    run_unittest(*tests_gui)
diff --git a/Lib/tkinter/test/test_tkinter/test_variables.py b/Lib/tkinter/test/test_tkinter/test_variables.py
index 08b7dedcaf933..63d7c21059e38 100644
--- a/Lib/tkinter/test/test_tkinter/test_variables.py
+++ b/Lib/tkinter/test/test_tkinter/test_variables.py
@@ -1,8 +1,10 @@
 import unittest
 import gc
+import tkinter
 from tkinter import (Variable, StringVar, IntVar, DoubleVar, BooleanVar, Tcl,
                      TclError)
 from test.support import ALWAYS_EQ
+from tkinter.test.support import AbstractDefaultRootTest
 
 
 class Var(Variable):
@@ -308,8 +310,21 @@ def test_invalid_value_domain(self):
             v.get()
 
 
+class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
+
+    def test_variable(self):
+        self.assertRaises(RuntimeError, Variable)
+        root = tkinter.Tk()
+        v = Variable()
+        v.set("value")
+        self.assertEqual(v.get(), "value")
+        root.destroy()
+        tkinter.NoDefaultRoot()
+        self.assertRaises(RuntimeError, Variable)
+
+
 tests_gui = (TestVariable, TestStringVar, TestIntVar,
-             TestDoubleVar, TestBooleanVar)
+             TestDoubleVar, TestBooleanVar, DefaultRootTest)
 
 
 if __name__ == "__main__":
diff --git a/Lib/tkinter/test/test_tkinter/test_widgets.py b/Lib/tkinter/test/test_tkinter/test_widgets.py
index 4b9b6ebdda04e..54eddbf821611 100644
--- a/Lib/tkinter/test/test_tkinter/test_widgets.py
+++ b/Lib/tkinter/test/test_tkinter/test_widgets.py
@@ -5,7 +5,8 @@
 from test.support import requires
 
 from tkinter.test.support import (tcl_version, requires_tcl,
-                                  get_tk_patchlevel, widget_eq)
+                                  get_tk_patchlevel, widget_eq,
+                                  AbstractDefaultRootTest)
 from tkinter.test.widget_tests import (
     add_standard_options, noconv, pixels_round,
     AbstractWidgetTest, StandardOptionsTests, IntegerSizeTests, PixelSizeTests,
@@ -1295,12 +1296,21 @@ def test_aspect(self):
         self.checkIntegerParam(widget, 'aspect', 250, 0, -300)
 
 
+class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
+
+    def test_frame(self):
+        self._test_widget(tkinter.Frame)
+
+    def test_label(self):
+        self._test_widget(tkinter.Label)
+
+
 tests_gui = (
         ButtonTest, CanvasTest, CheckbuttonTest, EntryTest,
         FrameTest, LabelFrameTest,LabelTest, ListboxTest,
         MenubuttonTest, MenuTest, MessageTest, OptionMenuTest,
         PanedWindowTest, RadiobuttonTest, ScaleTest, ScrollbarTest,
-        SpinboxTest, TextTest, ToplevelTest,
+        SpinboxTest, TextTest, ToplevelTest, DefaultRootTest,
 )
 
 if __name__ == '__main__':
diff --git a/Lib/tkinter/test/test_ttk/test_extensions.py b/Lib/tkinter/test/test_ttk/test_extensions.py
index 6937ba1ca9be4..1a70e0befe623 100644
--- a/Lib/tkinter/test/test_ttk/test_extensions.py
+++ b/Lib/tkinter/test/test_ttk/test_extensions.py
@@ -2,8 +2,8 @@
 import unittest
 import tkinter
 from tkinter import ttk
-from test.support import requires, run_unittest, swap_attr
-from tkinter.test.support import AbstractTkTest, destroy_default_root
+from test.support import requires, run_unittest
+from tkinter.test.support import AbstractTkTest, AbstractDefaultRootTest
 
 requires('gui')
 
@@ -46,20 +46,6 @@ def test_widget_destroy(self):
         if hasattr(sys, 'last_type'):
             self.assertNotEqual(sys.last_type, tkinter.TclError)
 
-
-    def test_initialization_no_master(self):
-        # no master passing
-        with swap_attr(tkinter, '_default_root', None), \
-             swap_attr(tkinter, '_support_default_root', True):
-            try:
-                x = ttk.LabeledScale()
-                self.assertIsNotNone(tkinter._default_root)
-                self.assertEqual(x.master, tkinter._default_root)
-                self.assertEqual(x.tk, tkinter._default_root.tk)
-                x.destroy()
-            finally:
-                destroy_default_root()
-
     def test_initialization(self):
         # master passing
         master = tkinter.Frame(self.root)
@@ -311,7 +297,13 @@ def test_unique_radiobuttons(self):
         optmenu2.destroy()
 
 
-tests_gui = (LabeledScaleTest, OptionMenuTest)
+class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
+
+    def test_labeledscale(self):
+        self._test_widget(ttk.LabeledScale)
+
+
+tests_gui = (LabeledScaleTest, OptionMenuTest, DefaultRootTest)
 
 if __name__ == "__main__":
     run_unittest(*tests_gui)
diff --git a/Lib/tkinter/test/test_ttk/test_widgets.py b/Lib/tkinter/test/test_ttk/test_widgets.py
index 157ef0e8f87bb..de30e2476b4eb 100644
--- a/Lib/tkinter/test/test_ttk/test_widgets.py
+++ b/Lib/tkinter/test/test_ttk/test_widgets.py
@@ -6,7 +6,7 @@
 
 from tkinter.test.test_ttk.test_functions import MockTclObj
 from tkinter.test.support import (AbstractTkTest, tcl_version, get_tk_patchlevel,
-                                  simulate_mouse_click)
+                                  simulate_mouse_click, AbstractDefaultRootTest)
 from tkinter.test.widget_tests import (add_standard_options, noconv,
     AbstractWidgetTest, StandardOptionsTests, IntegerSizeTests, PixelSizeTests,
     setUpModule)
@@ -1860,12 +1860,22 @@ class SizegripTest(AbstractWidgetTest, unittest.TestCase):
     def create(self, **kwargs):
         return ttk.Sizegrip(self.root, **kwargs)
 
+
+class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
+
+    def test_frame(self):
+        self._test_widget(ttk.Frame)
+
+    def test_label(self):
+        self._test_widget(ttk.Label)
+
+
 tests_gui = (
         ButtonTest, CheckbuttonTest, ComboboxTest, EntryTest,
         FrameTest, LabelFrameTest, LabelTest, MenubuttonTest,
         NotebookTest, PanedWindowTest, ProgressbarTest,
         RadiobuttonTest, ScaleTest, ScrollbarTest, SeparatorTest,
-        SizegripTest, SpinboxTest, TreeviewTest, WidgetTest,
+        SizegripTest, SpinboxTest, TreeviewTest, WidgetTest, DefaultRootTest,
         )
 
 if __name__ == "__main__":
diff --git a/Lib/tkinter/tix.py b/Lib/tkinter/tix.py
index ac545502e45c3..ef1e7406bc1ae 100644
--- a/Lib/tkinter/tix.py
+++ b/Lib/tkinter/tix.py
@@ -387,9 +387,7 @@ def config_all(self, option, value):
     # These are missing from Tkinter
     def image_create(self, imgtype, cnf={}, master=None, **kw):
         if not master:
-            master = tkinter._default_root
-            if not master:
-                raise RuntimeError('Too early to create image')
+            master = self
         if kw and cnf: cnf = _cnfmerge((cnf, kw))
         elif kw: cnf = kw
         options = ()
@@ -475,10 +473,7 @@ def __init__(self, itemtype, cnf={}, *, master=None, **kw):
             elif 'refwindow' in cnf:
                 master = cnf['refwindow']
             else:
-                master = tkinter._default_root
-                if not master:
-                    raise RuntimeError("Too early to create display style: "
-                                       "no root window")
+                master = tkinter._get_default_root('create display style')
         self.tk = master.tk
         self.stylename = self.tk.call('tixDisplayStyle', itemtype,
                             *self._options(cnf,kw) )
diff --git a/Lib/tkinter/ttk.py b/Lib/tkinter/ttk.py
index 968fd54dce1ee..523edb9715f08 100644
--- a/Lib/tkinter/ttk.py
+++ b/Lib/tkinter/ttk.py
@@ -349,12 +349,7 @@ def setup_master(master=None):
     If it is not allowed to use the default root and master is None,
     RuntimeError is raised."""
     if master is None:
-        if tkinter._support_default_root:
-            master = tkinter._default_root or tkinter.Tk()
-        else:
-            raise RuntimeError(
-                    "No master specified and tkinter is "
-                    "configured to not support default root")
+        master = tkinter._get_default_root()
     return master
 
 
diff --git a/Misc/NEWS.d/next/Library/2020-12-15-17-51-27.bpo-42630.jf4jBl.rst b/Misc/NEWS.d/next/Library/2020-12-15-17-51-27.bpo-42630.jf4jBl.rst
new file mode 100644
index 0000000000000..4b4a520931fda
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2020-12-15-17-51-27.bpo-42630.jf4jBl.rst
@@ -0,0 +1,4 @@
+:mod:`tkinter` functions and constructors which need a default root window
+raise now :exc:`RuntimeError` with descriptive message instead of obscure
+:exc:`AttributeError` or :exc:`NameError` if it is not created yet or cannot
+be created automatically.
diff --git a/Tools/pynche/PyncheWidget.py b/Tools/pynche/PyncheWidget.py
index ef12198a21838..ea456e577e12a 100644
--- a/Tools/pynche/PyncheWidget.py
+++ b/Tools/pynche/PyncheWidget.py
@@ -36,15 +36,11 @@ def __init__(self, version, switchboard, master=None, extrapath=[]):
         else:
             # Is there already a default root for Tk, say because we're
             # running under Guido's IDE? :-) Two conditions say no, either the
-            # import fails or _default_root is None.
-            tkroot = None
-            try:
-                from Tkinter import _default_root
-                tkroot = self.__tkroot = _default_root
-            except ImportError:
-                pass
+            # _default_root is None or it is unset.
+            tkroot = getattr(tkinter, '_default_root', None)
             if not tkroot:
-                tkroot = self.__tkroot = Tk(className='Pynche')
+                tkroot = Tk(className='Pynche')
+            self.__tkroot = tkroot
             # but this isn't our top level widget, so make it invisible
             tkroot.withdraw()
         # create the menubar



More information about the Python-checkins mailing list