Skip to content

Commit 555b24f

Browse files
gh-156961: Fix tkinter.font.Font for a font name returned as a Tcl object
Tk can return a font name or description as a Tcl object, for example from ttk.Style().lookup("TButton", "font"), Menu.entrycget("font"), ttk.Entry.cget("font"), or the default value in the result of configure(). Such an object does not compare equal to a string, so it was not recognized as the name of an existing named font. Keep it as is, so that it is passed back to Tk, and only convert it where it is compared with a string.
1 parent e56f86f commit 555b24f

3 files changed

Lines changed: 57 additions & 7 deletions

File tree

Lib/test/test_tkinter/test_font.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ def actual_size(self, desc):
2424
# The requested size is not always available (e.g. bitmap fonts).
2525
return self.root.tk.call('font', 'actual', desc, '-size')
2626

27+
def tcl_font_object(self, desc):
28+
# Return a font name or description as a Tcl object representing a
29+
# font, as Tk returns for example from ttk.Style().lookup().
30+
tk = self.root.tk
31+
tk.call('set', '_font', desc)
32+
tk.eval('font measure $_font x') # convert the Tcl object to a font
33+
obj = tk.call('set', '_font')
34+
tk.call('unset', '_font')
35+
return obj
36+
2737
def test_configure(self):
2838
self.assertEqual(self.font.config, self.font.configure)
2939
options = self.font.configure()
@@ -150,6 +160,36 @@ def test_existing(self):
150160
# A name or a description is required.
151161
self.assertRaises(TypeError, font.Font, root=self.root, exists=True)
152162

163+
def test_tcl_object(self):
164+
# Tk can return a font as a Tcl object (gh-156961).
165+
if not self.wantobjects:
166+
self.skipTest('Tcl objects are converted to strings')
167+
obj = self.tcl_font_object(fontname)
168+
self.assertEqual(obj.typename, 'font')
169+
170+
# It can be used as the name of an existing named font.
171+
for f in (font.Font(root=self.root, name=obj, exists=True),
172+
font.nametofont(obj, root=self.root)):
173+
# The Tcl object is kept as is, so that it is passed back to Tk.
174+
self.assertIs(f.name, obj)
175+
self.assertEqual(str(f), fontname)
176+
self.assertEqual(f.actual(), self.font.actual())
177+
self.assertEqual(f, self.font)
178+
self.assertEqual(self.font, f)
179+
# Referring to a non-existent named font still fails.
180+
self.assertRaisesRegex(tkinter.TclError, 'named font nosuchfont',
181+
font.Font, root=self.root, exists=True,
182+
name=self.tcl_font_object('nosuchfont'))
183+
184+
# It can also be wrapped as a font description.
185+
obj = self.tcl_font_object(('Times', 20, 'bold'))
186+
f = font.Font(root=self.root, font=obj, exists=True)
187+
self.assertIs(f.name, obj)
188+
self.assertEqual(str(f), 'Times 20 bold')
189+
self.assertNotIn(f.name, font.names(self.root))
190+
self.assertEqual(f.actual('weight'), 'bold')
191+
self.assertEqual(f.actual('size'), self.actual_size(('Times', 20, 'bold')))
192+
153193
def test_copy(self):
154194
# size=-20 (pixels): copy() copies the configured options, so the
155195
# size is preserved rather than resolved (gh-143990).

Lib/tkinter/font.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ def __init__(self, root=None, font=None, name=None, exists=False,
104104
if exists:
105105
self.name = name
106106
# confirm font exists
107-
if self.name not in tk.splitlist(tk.call("font", "names")):
107+
name = getattr(name, 'string', name) # can be a Tcl object
108+
if name not in tk.splitlist(tk.call("font", "names")):
108109
raise tkinter._tkinter.TclError(
109110
"named font %s does not already exist" % (self.name,))
110111
# if font config info supplied, apply it
@@ -123,11 +124,11 @@ def __init__(self, root=None, font=None, name=None, exists=False,
123124
self._call = tk.call
124125

125126
def __str__(self):
126-
# A wrapped description is a list or tuple, not a string; format it as
127-
# a Tcl word so it can be used as an option value (as ttk does).
128-
if isinstance(self.name, str):
129-
return self.name
130-
return tkinter._join(self.name)
127+
# A wrapped description can be a list or tuple; format it as a Tcl
128+
# word so it can be used as an option value (as ttk does).
129+
if isinstance(self.name, (list, tuple)):
130+
return tkinter._join(self.name)
131+
return str(self.name)
131132

132133
def __repr__(self):
133134
return f"<{self.__class__.__module__}.{self.__class__.__qualname__}" \
@@ -136,7 +137,13 @@ def __repr__(self):
136137
def __eq__(self, other):
137138
if not isinstance(other, Font):
138139
return NotImplemented
139-
return self.name == other.name and self._tk == other._tk
140+
name = self.name
141+
other_name = other.name
142+
if type(name) is not type(other_name):
143+
# A Tcl object does not compare equal to a string.
144+
name = getattr(name, 'string', name)
145+
other_name = getattr(other_name, 'string', other_name)
146+
return name == other_name and self._tk == other._tk
140147

141148
def __getitem__(self, key):
142149
return self.cget(key)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :func:`tkinter.font.nametofont` and the :class:`tkinter.font.Font`
2+
constructor for a font name or description returned by Tk as a Tcl object,
3+
for example by :meth:`ttk.Style.lookup() <tkinter.ttk.Style.lookup>`.

0 commit comments

Comments
 (0)