Skip to content

Commit 3d569fd

Browse files
bpo-42630: Improve error reporting in Tkinter for absent default root (GH-23781)
* 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.
1 parent 1e27b57 commit 3d569fd

19 files changed

+315
-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
@@ -184,7 +184,7 @@ def metrics(self, *options, **kw):
184184
def families(root=None, displayof=None):
185185
"Get font families (as a tuple)"
186186
if not root:
187-
root = tkinter._default_root
187+
root = tkinter._get_default_root('use font.families()')
188188
args = ()
189189
if displayof:
190190
args = ('-displayof', displayof)
@@ -194,7 +194,7 @@ def families(root=None, displayof=None):
194194
def names(root=None):
195195
"Get names of defined fonts (as a tuple)"
196196
if not root:
197-
root = tkinter._default_root
197+
root = tkinter._get_default_root('use font.names()')
198198
return root.tk.splitlist(root.tk.call("font", "names"))
199199

200200

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: 32 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

@@ -107,7 +107,37 @@ def test_repr(self):
107107
)
108108

109109

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

112142
if __name__ == "__main__":
113143
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
@@ -2,7 +2,7 @@
22
import tkinter
33
from test import support
44
from test.support import os_helper
5-
from tkinter.test.support import AbstractTkTest, requires_tcl
5+
from tkinter.test.support import AbstractTkTest, AbstractDefaultRootTest, requires_tcl
66

77
support.requires('gui')
88

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

2222

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

2566
@classmethod
@@ -331,7 +372,7 @@ def test_transparency(self):
331372
self.assertEqual(image.transparency_get(4, 6), False)
332373

333374

334-
tests_gui = (MiscTest, BitmapImageTest, PhotoImageTest,)
375+
tests_gui = (MiscTest, DefaultRootTest, BitmapImageTest, PhotoImageTest,)
335376

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

0 commit comments

Comments
 (0)