Skip to content

Commit 87e7a14

Browse files
[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 3d569fd)
1 parent d458d8d commit 87e7a14

19 files changed

+316
-87
lines changed

Lib/idlelib/pyshell.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1061,8 +1061,10 @@ def begin(self):
10611061
(sys.version, sys.platform, self.COPYRIGHT, nosub))
10621062
self.text.focus_force()
10631063
self.showprompt()
1064+
# User code should use separate default Tk root window
10641065
import tkinter
1065-
tkinter._default_root = None # 03Jan04 KBK What's this?
1066+
tkinter._support_default_root = True
1067+
tkinter._default_root = None
10661068
return True
10671069

10681070
def stop_readline(self):

Lib/test/test_idle.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@
2020
if __name__ == '__main__':
2121
tk.NoDefaultRoot()
2222
unittest.main(exit=False)
23-
tk._support_default_root = 1
23+
tk._support_default_root = True
2424
tk._default_root = None

Lib/tkinter/__init__.py

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ def __repr__(self):
270270
)
271271

272272

273-
_support_default_root = 1
273+
_support_default_root = True
274274
_default_root = None
275275

276276

@@ -280,13 +280,26 @@ def NoDefaultRoot():
280280
Call this function to inhibit that the first instance of
281281
Tk is used for windows without an explicit parent window.
282282
"""
283-
global _support_default_root
284-
_support_default_root = 0
285-
global _default_root
283+
global _support_default_root, _default_root
284+
_support_default_root = False
285+
# Delete, so any use of _default_root will immediately raise an exception.
286+
# Rebind before deletion, so repeated calls will not fail.
286287
_default_root = None
287288
del _default_root
288289

289290

291+
def _get_default_root(what=None):
292+
if not _support_default_root:
293+
raise RuntimeError("No master specified and tkinter is "
294+
"configured to not support default root")
295+
if not _default_root:
296+
if what:
297+
raise RuntimeError(f"Too early to {what}: no default root window")
298+
root = Tk()
299+
assert _default_root is root
300+
return _default_root
301+
302+
290303
def _tkerror(err):
291304
"""Internal function."""
292305
pass
@@ -330,7 +343,7 @@ def __init__(self, master=None, value=None, name=None):
330343
raise TypeError("name must be a string")
331344
global _varnum
332345
if not master:
333-
master = _default_root
346+
master = _get_default_root('create variable')
334347
self._root = master._root()
335348
self._tk = master.tk
336349
if name:
@@ -591,7 +604,7 @@ def get(self):
591604

592605
def mainloop(n=0):
593606
"""Run the main loop of Tcl."""
594-
_default_root.tk.mainloop(n)
607+
_get_default_root('run the main loop').tk.mainloop(n)
595608

596609

597610
getint = int
@@ -600,9 +613,9 @@ def mainloop(n=0):
600613

601614

602615
def getboolean(s):
603-
"""Convert true and false to integer values 1 and 0."""
616+
"""Convert Tcl object to True or False."""
604617
try:
605-
return _default_root.tk.getboolean(s)
618+
return _get_default_root('use getboolean()').tk.getboolean(s)
606619
except TclError:
607620
raise ValueError("invalid literal for getboolean()")
608621

@@ -2248,7 +2261,7 @@ def __init__(self, screenName=None, baseName=None, className='Tk',
22482261
is the name of the widget class."""
22492262
self.master = None
22502263
self.children = {}
2251-
self._tkloaded = 0
2264+
self._tkloaded = False
22522265
# to avoid recursions in the getattr code in case of failure, we
22532266
# ensure that self.tk is always _something_.
22542267
self.tk = None
@@ -2272,7 +2285,7 @@ def loadtk(self):
22722285
self._loadtk()
22732286

22742287
def _loadtk(self):
2275-
self._tkloaded = 1
2288+
self._tkloaded = True
22762289
global _default_root
22772290
# Version sanity checks
22782291
tk_version = self.tk.getvar('tk_version')
@@ -2521,12 +2534,8 @@ class BaseWidget(Misc):
25212534

25222535
def _setup(self, master, cnf):
25232536
"""Internal function. Sets up information about children."""
2524-
if _support_default_root:
2525-
global _default_root
2526-
if not master:
2527-
if not _default_root:
2528-
_default_root = Tk()
2529-
master = _default_root
2537+
if not master:
2538+
master = _get_default_root()
25302539
self.master = master
25312540
self.tk = master.tk
25322541
name = None
@@ -3990,9 +3999,7 @@ class Image:
39903999
def __init__(self, imgtype, name=None, cnf={}, master=None, **kw):
39914000
self.name = None
39924001
if not master:
3993-
master = _default_root
3994-
if not master:
3995-
raise RuntimeError('Too early to create image')
4002+
master = _get_default_root('create image')
39964003
self.tk = getattr(master, 'tk', master)
39974004
if not name:
39984005
Image._last_id += 1
@@ -4146,11 +4153,13 @@ def __init__(self, name=None, cnf={}, master=None, **kw):
41464153

41474154

41484155
def image_names():
4149-
return _default_root.tk.splitlist(_default_root.tk.call('image', 'names'))
4156+
tk = _get_default_root('use image_names()').tk
4157+
return tk.splitlist(tk.call('image', 'names'))
41504158

41514159

41524160
def image_types():
4153-
return _default_root.tk.splitlist(_default_root.tk.call('image', 'types'))
4161+
tk = _get_default_root('use image_types()').tk
4162+
return tk.splitlist(tk.call('image', 'types'))
41544163

41554164

41564165
class Spinbox(Widget, XView):

Lib/tkinter/commondialog.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ class Dialog:
1818
command = None
1919

2020
def __init__(self, master=None, **options):
21+
if not master:
22+
master = options.get('parent')
2123
self.master = master
2224
self.options = options
23-
if not master and options.get('parent'):
24-
self.master = options['parent']
2525

2626
def _fixoptions(self):
2727
pass # hook

Lib/tkinter/font.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def _mkdict(self, args):
6969
def __init__(self, root=None, font=None, name=None, exists=False,
7070
**options):
7171
if not root:
72-
root = tkinter._default_root
72+
root = tkinter._get_default_root('use font')
7373
tk = getattr(root, 'tk', root)
7474
if font:
7575
# get actual settings corresponding to the given font
@@ -180,7 +180,7 @@ def metrics(self, *options, **kw):
180180
def families(root=None, displayof=None):
181181
"Get font families (as a tuple)"
182182
if not root:
183-
root = tkinter._default_root
183+
root = tkinter._get_default_root('use font.families()')
184184
args = ()
185185
if displayof:
186186
args = ('-displayof', displayof)
@@ -190,7 +190,7 @@ def families(root=None, displayof=None):
190190
def names(root=None):
191191
"Get names of defined fonts (as a tuple)"
192192
if not root:
193-
root = tkinter._default_root
193+
root = tkinter._get_default_root('use font.names()')
194194
return root.tk.splitlist(root.tk.call("font", "names"))
195195

196196

Lib/tkinter/simpledialog.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@
2424
"""
2525

2626
from tkinter import *
27-
from tkinter import messagebox
28-
29-
import tkinter # used at _QueryDialog for tkinter._default_root
27+
from tkinter import messagebox, _get_default_root
3028

3129

3230
class SimpleDialog:
@@ -128,13 +126,17 @@ def __init__(self, parent, title = None):
128126
129127
title -- the dialog title
130128
'''
131-
Toplevel.__init__(self, parent)
129+
master = parent
130+
if not master:
131+
master = _get_default_root('create dialog window')
132+
133+
Toplevel.__init__(self, master)
132134

133135
self.withdraw() # remain invisible for now
134-
# If the master is not viewable, don't
136+
# If the parent is not viewable, don't
135137
# make the child transient, or else it
136138
# would be opened withdrawn
137-
if parent.winfo_viewable():
139+
if parent is not None and parent.winfo_viewable():
138140
self.transient(parent)
139141

140142
if title:
@@ -155,7 +157,7 @@ def __init__(self, parent, title = None):
155157

156158
self.protocol("WM_DELETE_WINDOW", self.cancel)
157159

158-
if self.parent is not None:
160+
if parent is not None:
159161
self.geometry("+%d+%d" % (parent.winfo_rootx()+50,
160162
parent.winfo_rooty()+50))
161163

@@ -259,9 +261,6 @@ def __init__(self, title, prompt,
259261
minvalue = None, maxvalue = None,
260262
parent = None):
261263

262-
if not parent:
263-
parent = tkinter._default_root
264-
265264
self.prompt = prompt
266265
self.minvalue = minvalue
267266
self.maxvalue = maxvalue

Lib/tkinter/test/support.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,33 @@ def tearDown(self):
3636
w.destroy()
3737
self.root.withdraw()
3838

39+
40+
class AbstractDefaultRootTest:
41+
42+
def setUp(self):
43+
self._old_support_default_root = tkinter._support_default_root
44+
destroy_default_root()
45+
tkinter._support_default_root = True
46+
self.wantobjects = tkinter.wantobjects
47+
48+
def tearDown(self):
49+
destroy_default_root()
50+
tkinter._default_root = None
51+
tkinter._support_default_root = self._old_support_default_root
52+
53+
def _test_widget(self, constructor):
54+
# no master passing
55+
x = constructor()
56+
self.assertIsNotNone(tkinter._default_root)
57+
self.assertIs(x.master, tkinter._default_root)
58+
self.assertIs(x.tk, tkinter._default_root.tk)
59+
x.destroy()
60+
destroy_default_root()
61+
tkinter.NoDefaultRoot()
62+
self.assertRaises(RuntimeError, constructor)
63+
self.assertFalse(hasattr(tkinter, '_default_root'))
64+
65+
3966
def destroy_default_root():
4067
if getattr(tkinter, '_default_root', None):
4168
tkinter._default_root.update_idletasks()

Lib/tkinter/test/test_tkinter/test_font.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import tkinter
33
from tkinter import font
44
from test.support import requires, run_unittest, gc_collect, ALWAYS_EQ
5-
from tkinter.test.support import AbstractTkTest
5+
from tkinter.test.support import AbstractTkTest, AbstractDefaultRootTest
66

77
requires('gui')
88

@@ -101,7 +101,38 @@ def test_names(self):
101101
self.assertTrue(name)
102102
self.assertIn(fontname, names)
103103

104-
tests_gui = (FontTest, )
104+
105+
class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
106+
107+
def test_families(self):
108+
self.assertRaises(RuntimeError, font.families)
109+
root = tkinter.Tk()
110+
families = font.families()
111+
self.assertIsInstance(families, tuple)
112+
self.assertTrue(families)
113+
for family in families:
114+
self.assertIsInstance(family, str)
115+
self.assertTrue(family)
116+
root.destroy()
117+
tkinter.NoDefaultRoot()
118+
self.assertRaises(RuntimeError, font.families)
119+
120+
def test_names(self):
121+
self.assertRaises(RuntimeError, font.names)
122+
root = tkinter.Tk()
123+
names = font.names()
124+
self.assertIsInstance(names, tuple)
125+
self.assertTrue(names)
126+
for name in names:
127+
self.assertIsInstance(name, str)
128+
self.assertTrue(name)
129+
self.assertIn(fontname, names)
130+
root.destroy()
131+
tkinter.NoDefaultRoot()
132+
self.assertRaises(RuntimeError, font.names)
133+
134+
135+
tests_gui = (FontTest, DefaultRootTest)
105136

106137
if __name__ == "__main__":
107138
run_unittest(*tests_gui)

Lib/tkinter/test/test_tkinter/test_images.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import unittest
22
import tkinter
33
from test import support
4-
from tkinter.test.support import AbstractTkTest, requires_tcl
4+
from tkinter.test.support import AbstractTkTest, AbstractDefaultRootTest, requires_tcl
55

66
support.requires('gui')
77

@@ -19,6 +19,47 @@ def test_image_names(self):
1919
self.assertIsInstance(image_names, tuple)
2020

2121

22+
class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
23+
24+
def test_image_types(self):
25+
self.assertRaises(RuntimeError, tkinter.image_types)
26+
root = tkinter.Tk()
27+
image_types = tkinter.image_types()
28+
self.assertIsInstance(image_types, tuple)
29+
self.assertIn('photo', image_types)
30+
self.assertIn('bitmap', image_types)
31+
root.destroy()
32+
tkinter.NoDefaultRoot()
33+
self.assertRaises(RuntimeError, tkinter.image_types)
34+
35+
def test_image_names(self):
36+
self.assertRaises(RuntimeError, tkinter.image_names)
37+
root = tkinter.Tk()
38+
image_names = tkinter.image_names()
39+
self.assertIsInstance(image_names, tuple)
40+
root.destroy()
41+
tkinter.NoDefaultRoot()
42+
self.assertRaises(RuntimeError, tkinter.image_names)
43+
44+
def test_image_create_bitmap(self):
45+
self.assertRaises(RuntimeError, tkinter.BitmapImage)
46+
root = tkinter.Tk()
47+
image = tkinter.BitmapImage()
48+
self.assertIn(image.name, tkinter.image_names())
49+
root.destroy()
50+
tkinter.NoDefaultRoot()
51+
self.assertRaises(RuntimeError, tkinter.BitmapImage)
52+
53+
def test_image_create_photo(self):
54+
self.assertRaises(RuntimeError, tkinter.PhotoImage)
55+
root = tkinter.Tk()
56+
image = tkinter.PhotoImage()
57+
self.assertIn(image.name, tkinter.image_names())
58+
root.destroy()
59+
tkinter.NoDefaultRoot()
60+
self.assertRaises(RuntimeError, tkinter.PhotoImage)
61+
62+
2263
class BitmapImageTest(AbstractTkTest, unittest.TestCase):
2364

2465
@classmethod
@@ -330,7 +371,7 @@ def test_transparency(self):
330371
self.assertEqual(image.transparency_get(4, 6), False)
331372

332373

333-
tests_gui = (MiscTest, BitmapImageTest, PhotoImageTest,)
374+
tests_gui = (MiscTest, DefaultRootTest, BitmapImageTest, PhotoImageTest,)
334375

335376
if __name__ == "__main__":
336377
support.run_unittest(*tests_gui)

0 commit comments

Comments
 (0)