diff --git a/Lib/test/test_tkinter/test_font.py b/Lib/test/test_tkinter/test_font.py index 3d76ae630d97e38..16c9c1dc22bbca7 100644 --- a/Lib/test/test_tkinter/test_font.py +++ b/Lib/test/test_tkinter/test_font.py @@ -24,6 +24,16 @@ def actual_size(self, desc): # The requested size is not always available (e.g. bitmap fonts). return self.root.tk.call('font', 'actual', desc, '-size') + def tcl_font_object(self, desc): + # Return a font name or description as a Tcl object representing a + # font, as Tk returns for example from ttk.Style().lookup(). + tk = self.root.tk + tk.call('set', '_font', desc) + tk.eval('font measure $_font x') # convert the Tcl object to a font + obj = tk.call('set', '_font') + tk.call('unset', '_font') + return obj + def test_configure(self): self.assertEqual(self.font.config, self.font.configure) options = self.font.configure() @@ -150,6 +160,36 @@ def test_existing(self): # A name or a description is required. self.assertRaises(TypeError, font.Font, root=self.root, exists=True) + def test_tcl_object(self): + # Tk can return a font as a Tcl object (gh-156961). + if not self.wantobjects: + self.skipTest('Tcl objects are converted to strings') + obj = self.tcl_font_object(fontname) + self.assertEqual(obj.typename, 'font') + + # It can be used as the name of an existing named font. + for f in (font.Font(root=self.root, name=obj, exists=True), + font.nametofont(obj, root=self.root)): + # The Tcl object is kept as is, so that it is passed back to Tk. + self.assertIs(f.name, obj) + self.assertEqual(str(f), fontname) + self.assertEqual(f.actual(), self.font.actual()) + self.assertEqual(f, self.font) + self.assertEqual(self.font, f) + # Referring to a non-existent named font still fails. + self.assertRaisesRegex(tkinter.TclError, 'named font nosuchfont', + font.Font, root=self.root, exists=True, + name=self.tcl_font_object('nosuchfont')) + + # It can also be wrapped as a font description. + obj = self.tcl_font_object(('Times', 20, 'bold')) + f = font.Font(root=self.root, font=obj, exists=True) + self.assertIs(f.name, obj) + self.assertEqual(str(f), 'Times 20 bold') + self.assertNotIn(f.name, font.names(self.root)) + self.assertEqual(f.actual('weight'), 'bold') + self.assertEqual(f.actual('size'), self.actual_size(('Times', 20, 'bold'))) + def test_copy(self): # size=-20 (pixels): copy() copies the configured options, so the # size is preserved rather than resolved (gh-143990). diff --git a/Lib/tkinter/font.py b/Lib/tkinter/font.py index 5b663a4e456456b..1349e49fbd68a5b 100644 --- a/Lib/tkinter/font.py +++ b/Lib/tkinter/font.py @@ -104,7 +104,8 @@ def __init__(self, root=None, font=None, name=None, exists=False, if exists: self.name = name # confirm font exists - if self.name not in tk.splitlist(tk.call("font", "names")): + name = getattr(name, 'string', name) # can be a Tcl object + if name not in tk.splitlist(tk.call("font", "names")): raise tkinter._tkinter.TclError( "named font %s does not already exist" % (self.name,)) # if font config info supplied, apply it @@ -123,11 +124,11 @@ def __init__(self, root=None, font=None, name=None, exists=False, self._call = tk.call def __str__(self): - # A wrapped description is a list or tuple, not a string; format it as - # a Tcl word so it can be used as an option value (as ttk does). - if isinstance(self.name, str): - return self.name - return tkinter._join(self.name) + # A wrapped description can be a list or tuple; format it as a Tcl + # word so it can be used as an option value (as ttk does). + if isinstance(self.name, (list, tuple)): + return tkinter._join(self.name) + return str(self.name) def __repr__(self): return f"<{self.__class__.__module__}.{self.__class__.__qualname__}" \ @@ -136,7 +137,13 @@ def __repr__(self): def __eq__(self, other): if not isinstance(other, Font): return NotImplemented - return self.name == other.name and self._tk == other._tk + name = self.name + other_name = other.name + if type(name) is not type(other_name): + # A Tcl object does not compare equal to a string. + name = getattr(name, 'string', name) + other_name = getattr(other_name, 'string', other_name) + return name == other_name and self._tk == other._tk def __getitem__(self, key): return self.cget(key) diff --git a/Misc/NEWS.d/next/Library/2026-09-05-22-39-22.gh-issue-156961.ZtYqoA.rst b/Misc/NEWS.d/next/Library/2026-09-05-22-39-22.gh-issue-156961.ZtYqoA.rst new file mode 100644 index 000000000000000..9df2868c30f16d1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-05-22-39-22.gh-issue-156961.ZtYqoA.rst @@ -0,0 +1,3 @@ +Fix :func:`tkinter.font.nametofont` and the :class:`tkinter.font.Font` +constructor for a font name or description returned by Tk as a Tcl object, +for example by :meth:`ttk.Style.lookup() `.