Skip to content

Commit 80c445c

Browse files
[3.8] bpo-42630: Improve error reporting in Tkinter for absent default root (GH-23781) (GH-23854)
* 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 d21d29a commit 80c445c

19 files changed

+317
-88
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:
@@ -589,7 +602,7 @@ def get(self):
589602

590603
def mainloop(n=0):
591604
"""Run the main loop of Tcl."""
592-
_default_root.tk.mainloop(n)
605+
_get_default_root('run the main loop').tk.mainloop(n)
593606

594607

595608
getint = int
@@ -598,9 +611,9 @@ def mainloop(n=0):
598611

599612

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

@@ -2246,7 +2259,7 @@ def __init__(self, screenName=None, baseName=None, className='Tk',
22462259
is the name of the widget class."""
22472260
self.master = None
22482261
self.children = {}
2249-
self._tkloaded = 0
2262+
self._tkloaded = False
22502263
# to avoid recursions in the getattr code in case of failure, we
22512264
# ensure that self.tk is always _something_.
22522265
self.tk = None
@@ -2270,7 +2283,7 @@ def loadtk(self):
22702283
self._loadtk()
22712284

22722285
def _loadtk(self):
2273-
self._tkloaded = 1
2286+
self._tkloaded = True
22742287
global _default_root
22752288
# Version sanity checks
22762289
tk_version = self.tk.getvar('tk_version')
@@ -2519,12 +2532,8 @@ class BaseWidget(Misc):
25192532

25202533
def _setup(self, master, cnf):
25212534
"""Internal function. Sets up information about children."""
2522-
if _support_default_root:
2523-
global _default_root
2524-
if not master:
2525-
if not _default_root:
2526-
_default_root = Tk()
2527-
master = _default_root
2535+
if not master:
2536+
master = _get_default_root()
25282537
self.master = master
25292538
self.tk = master.tk
25302539
name = None
@@ -3988,9 +3997,7 @@ class Image:
39883997
def __init__(self, imgtype, name=None, cnf={}, master=None, **kw):
39893998
self.name = None
39903999
if not master:
3991-
master = _default_root
3992-
if not master:
3993-
raise RuntimeError('Too early to create image')
4000+
master = _get_default_root('create image')
39944001
self.tk = getattr(master, 'tk', master)
39954002
if not name:
39964003
Image._last_id += 1
@@ -4144,11 +4151,13 @@ def __init__(self, name=None, cnf={}, master=None, **kw):
41444151

41454152

41464153
def image_names():
4147-
return _default_root.tk.splitlist(_default_root.tk.call('image', 'names'))
4154+
tk = _get_default_root('use image_names()').tk
4155+
return tk.splitlist(tk.call('image', 'names'))
41484156

41494157

41504158
def image_types():
4151-
return _default_root.tk.splitlist(_default_root.tk.call('image', 'types'))
4159+
tk = _get_default_root('use image_types()').tk
4160+
return tk.splitlist(tk.call('image', 'types'))
41524161

41534162

41544163
class Spinbox(Widget, XView):

Lib/tkinter/commondialog.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ class Dialog:
1616
command = None
1717

1818
def __init__(self, master=None, **options):
19-
self.master = master
19+
if not master:
20+
master = options.get('parent')
21+
self.master = master
2022
self.options = options
21-
if not master and options.get('parent'):
22-
self.master = options['parent']
2323

2424
def _fixoptions(self):
2525
pass # hook

Lib/tkinter/font.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def _mkdict(self, args):
6868
def __init__(self, root=None, font=None, name=None, exists=False,
6969
**options):
7070
if not root:
71-
root = tkinter._default_root
71+
root = tkinter._get_default_root('use font')
7272
tk = getattr(root, 'tk', root)
7373
if font:
7474
# get actual settings corresponding to the given font
@@ -177,7 +177,7 @@ def metrics(self, *options, **kw):
177177
def families(root=None, displayof=None):
178178
"Get font families (as a tuple)"
179179
if not root:
180-
root = tkinter._default_root
180+
root = tkinter._get_default_root('use font.families()')
181181
args = ()
182182
if displayof:
183183
args = ('-displayof', displayof)
@@ -187,7 +187,7 @@ def families(root=None, displayof=None):
187187
def names(root=None):
188188
"Get names of defined fonts (as a tuple)"
189189
if not root:
190-
root = tkinter._default_root
190+
root = tkinter._get_default_root('use font.names()')
191191
return root.tk.splitlist(root.tk.call("font", "names"))
192192

193193

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
5-
from tkinter.test.support import AbstractTkTest
5+
from tkinter.test.support import AbstractTkTest, AbstractDefaultRootTest
66

77
requires('gui')
88

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

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

105136
if __name__ == "__main__":
106137
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)