From b7b573d8f219da2eccaca9c99aa0bc1440e53fd4 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 23 Jun 2026 21:05:54 +0300 Subject: [PATCH 1/3] gh-143990: Allow tkinter.font.Font to wrap a font description With exists=True and no name, Font now wraps the font description as is, without creating a new named font, so that it is used without loss of precision by actual(), measure() and metrics(). Its name attribute is then the description rather than a string. Keyword options now override the corresponding settings of the given font instead of being ignored. Co-Authored-By: Claude Opus 4.8 --- Doc/library/tkinter.font.rst | 70 +++++++++++----- Lib/test/test_tkinter/test_font.py | 82 ++++++++++++++++++- Lib/tkinter/font.py | 63 +++++++++----- ...-06-23-18-42-37.gh-issue-143990.o65v7O.rst | 6 ++ 4 files changed, 174 insertions(+), 47 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-23-18-42-37.gh-issue-143990.o65v7O.rst diff --git a/Doc/library/tkinter.font.rst b/Doc/library/tkinter.font.rst index 2a9563fffab1f71..7fff7f3ee1e3f76 100644 --- a/Doc/library/tkinter.font.rst +++ b/Doc/library/tkinter.font.rst @@ -20,33 +20,53 @@ The different font weights and slants are: .. class:: Font(root=None, font=None, name=None, exists=False, **options) - The :class:`Font` class represents a named font. *Font* instances are given - unique names and can be specified by their family, size, and style - configuration. Named fonts are Tk's method of creating and identifying - fonts as a single object, rather than specifying a font by its attributes - with each occurrence. + The :class:`Font` class represents a font used by Tk widgets. + It either creates a new *named font* or refers to an existing font. + A named font is Tk's way of identifying a font as a single object that can + be referred to by name and reconfigured in place, + rather than respecifying its attributes at each use. + + With *exists* false (the default), a new named font is created. + Its attributes are taken from the font description *font* if it is given, + overridden by any keyword *options*. + The new font is named *name*, or a generated unique name if *name* is + omitted. + + With *exists* true, an existing font is referred to instead of being + created. + If *name* is given, it is the name of the font, + which is reconfigured by *font* and *options* if either is given. + If *name* is omitted, the font description *font* is wrapped as is, + without creating a named font, + so that it is used without loss of precision by :meth:`actual`, + :meth:`measure` and :meth:`metrics`. + In this case no keyword options are accepted, + and the :attr:`!name` attribute is the description itself rather than a + string. + + The font description *font* is a tuple of the family name, the size and + zero or more styles, + or any other form accepted by Tk, such as the name of a named font. + + The keyword *options* are: + + | *family* - font family, for example, Courier, Times + | *size* - font size + | If *size* is positive it is interpreted as size in points. + | If *size* is a negative number its absolute value is treated + | as size in pixels. + | *weight* - font emphasis (NORMAL, BOLD) + | *slant* - ROMAN, ITALIC + | *underline* - font underlining (0 - none, 1 - underline) + | *overstrike* - font strikeout (0 - none, 1 - strikeout) .. versionchanged:: 3.10 Two fonts now compare equal (``==``) only when both are :class:`Font` instances with the same name belonging to the same Tcl interpreter. - arguments: - - | *font* - font specifier tuple (family, size, options) - | *name* - unique font name - | *exists* - self points to existing named font if true - - additional keyword options (ignored if *font* is specified): - - | *family* - font family, for example, Courier, Times - | *size* - font size - | If *size* is positive it is interpreted as size in points. - | If *size* is a negative number its absolute value is treated - | as size in pixels. - | *weight* - font emphasis (NORMAL, BOLD) - | *slant* - ROMAN, ITALIC - | *underline* - font underlining (0 - none, 1 - underline) - | *overstrike* - font strikeout (0 - none, 1 - strikeout) + .. versionchanged:: next + A font description can now be wrapped without creating a new named font, + and keyword options now override the attributes of the specified *font*. .. method:: actual(option=None, displayof=None) @@ -59,6 +79,12 @@ The different font weights and slants are: Retrieve an attribute of the font. + .. note:: + + :meth:`!cget` and :meth:`configure` operate on a named font and raise + :exc:`TclError` for a wrapped font description. + Use :meth:`actual` to query the attributes of the latter. + .. method:: config(**options) :no-typesetting: diff --git a/Lib/test/test_tkinter/test_font.py b/Lib/test/test_tkinter/test_font.py index 8adc9e1151db4ed..e6f331332eb20a9 100644 --- a/Lib/test/test_tkinter/test_font.py +++ b/Lib/test/test_tkinter/test_font.py @@ -44,6 +44,70 @@ def test_configure(self): self.assertRaises(TypeError, self.font.cget) self.assertRaises(TypeError, self.font.cget, 'size', 'weight') + def test_create(self): + sizetype = int if self.wantobjects else str + + # A new named font is created from the font description... + f = font.Font(root=self.root, font=('Times', 20, 'bold')) + self.assertIn(f.name, font.names(self.root)) + self.assertEqual(f.actual('weight'), 'bold') + self.assertEqual(f.cget('size'), sizetype(20)) + + # ... or from the keyword options. + f = font.Font(root=self.root, family='Times', size=20, weight='bold') + self.assertIn(f.name, font.names(self.root)) + self.assertEqual(f.actual('weight'), 'bold') + self.assertEqual(f.cget('size'), sizetype(20)) + + # Explicit options override the corresponding settings of *font*. + f = font.Font(root=self.root, font=('Times', 20, 'bold'), weight='normal') + self.assertEqual(f.actual('weight'), 'normal') + self.assertEqual(f.cget('size'), sizetype(20)) + + # The new font can be given an explicit name. + f = font.Font(root=self.root, name='testfont', font=('Times', 20)) + self.assertEqual(f.name, 'testfont') + self.assertIn('testfont', font.names(self.root)) + self.assertEqual(f.cget('size'), sizetype(20)) + # Reusing the name of an existing font fails. + self.assertRaises(tkinter.TclError, font.Font, root=self.root, + name='testfont', font=('Times', 10)) + + def test_existing(self): + sizetype = int if self.wantobjects else str + + # With a name, refer to the existing named font. + named = font.Font(root=self.root, name='existingfont', family='Times', size=20) + f = font.Font(root=self.root, name='existingfont', exists=True) + self.assertEqual(f.name, 'existingfont') + self.assertEqual(f.cget('size'), sizetype(20)) + # Referring to a non-existent named font fails. + self.assertRaises(tkinter.TclError, font.Font, root=self.root, + name='nosuchfont', exists=True) + # A name and options reconfigure the existing font. + font.Font(root=self.root, name='existingfont', exists=True, size=8) + self.assertEqual(f.cget('size'), sizetype(8)) + + # With a description and no name, the description is wrapped without + # creating a new named font (gh-143990), so that it is used without + # loss of precision by actual(), measure() and metrics(). + f = font.Font(root=self.root, font=('Times', 20, 'bold'), exists=True) + self.assertEqual(f.name, ('Times', 20, 'bold')) + 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'), sizetype(20)) + # It can be used as a widget option, with the same effect as the + # description itself (gh-143990). + self.assertEqual(tkinter.Label(self.root, font=f).cget('font'), + tkinter.Label(self.root, font=f.name).cget('font')) + + # Options cannot be combined with a wrapped description. + self.assertRaises(TypeError, font.Font, root=self.root, + font=('Times', 20), exists=True, weight='bold') + # A name or a description is required. + self.assertRaises(TypeError, font.Font, root=self.root, exists=True) + def test_copy(self): f = font.Font(root=self.root, family='Times', size=10, weight='bold') copied = f.copy() @@ -60,10 +124,7 @@ def test_copy(self): def test_unicode_family(self): family = 'MS \u30b4\u30b7\u30c3\u30af' - try: - f = font.Font(root=self.root, family=family, exists=True) - except tkinter.TclError: - f = font.Font(root=self.root, family=family, exists=False) + f = font.Font(root=self.root, family=family) self.assertEqual(f.cget('family'), family) del f gc_collect() @@ -96,6 +157,19 @@ def test_equality(self): self.assertEqual(font1, font2) self.assertNotEqual(font1, font1.copy()) + # Wrapped descriptions (gh-143990) compare by the description. + w1 = font.Font(root=self.root, font=('Times', 20, 'bold'), exists=True) + w2 = font.Font(root=self.root, font=('Times', 20, 'bold'), exists=True) + self.assertIsNot(w1, w2) + self.assertEqual(w1, w2) + w3 = font.Font(root=self.root, font=('Times', 12), exists=True) + self.assertNotEqual(w1, w3) + # A wrapped description never equals a named font, even one whose name + # is the string form of the description. + named = font.Font(root=self.root, name=str(w1), family='Courier') + self.assertNotEqual(w1, named) + self.assertNotEqual(named, w1) + self.assertNotEqual(font1, 0) self.assertEqual(font1, ALWAYS_EQ) diff --git a/Lib/tkinter/font.py b/Lib/tkinter/font.py index 896e910d69f6f33..d59b04d36638e79 100644 --- a/Lib/tkinter/font.py +++ b/Lib/tkinter/font.py @@ -70,34 +70,55 @@ def __init__(self, root=None, font=None, name=None, exists=False, if root is None: root = tkinter._get_default_root('use font') tk = getattr(root, 'tk', root) - if font: - # get actual settings corresponding to the given font - font = tk.splitlist(tk.call("font", "actual", font)) + self.delete_font = False + if exists and not name: + if not font: + raise TypeError("font name or description is required if exists=True") + if options: + raise TypeError("cannot specify font options when wrapping an " + "existing font description") + # Wrap the description without creating a new named font, so that + # it is used as is by actual(), measure() and metrics(). 'name' is + # then the description rather than a string. + self.name = font else: - font = self._set(options) - if not name: - name = "font" + str(next(self.counter)) - self.name = name - - if exists: - self.delete_font = False - # confirm font exists - if self.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 if font: - tk.call("font", "configure", self.name, *font) - else: - # create new font (raises TclError if the font exists) - tk.call("font", "create", self.name, *font) - self.delete_font = True + # start from the actual settings of the given font + font = tk.splitlist(tk.call("font", "actual", font)) + if options: + # explicit options override the corresponding settings + settings = self._mkdict(font) + settings.update(options) + font = self._set(settings) + else: + font = self._set(options) + if exists: + self.name = name + # confirm font exists + if self.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 + if font: + tk.call("font", "configure", self.name, *font) + else: + if name: + self.name = name + else: + self.name = "font" + str(next(self.counter)) + # create new font (raises TclError if the font exists) + tk.call("font", "create", self.name, *font) + self.delete_font = True # set after creation self._tk = tk self._split = tk.splitlist self._call = tk.call def __str__(self): - return self.name + # 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) def __repr__(self): return f"<{self.__class__.__module__}.{self.__class__.__qualname__}" \ diff --git a/Misc/NEWS.d/next/Library/2026-06-23-18-42-37.gh-issue-143990.o65v7O.rst b/Misc/NEWS.d/next/Library/2026-06-23-18-42-37.gh-issue-143990.o65v7O.rst new file mode 100644 index 000000000000000..c464f81a6bb2547 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-23-18-42-37.gh-issue-143990.o65v7O.rst @@ -0,0 +1,6 @@ +:class:`tkinter.font.Font` can now wrap a font description without creating a +new named font, by passing it as *font* with ``exists=True`` and no *name*. +This avoids a loss of precision in :meth:`~tkinter.font.Font.actual`, +:meth:`~tkinter.font.Font.measure` and :meth:`~tkinter.font.Font.metrics`. +Keyword options now override the corresponding settings of the given *font* +instead of being ignored. From bdac908e5842dfc18e69e1c9d6d096a09b14c6ca Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 23 Jun 2026 22:18:21 +0300 Subject: [PATCH 2/3] gh-143990: Add What's New entry for tkinter.font.Font wrapping Co-Authored-By: Claude Opus 4.8 --- Doc/whatsnew/3.16.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 64a986f2487d5cb..d6890d42053dd70 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -236,6 +236,12 @@ tkinter ttk version, and accepts mappings of button options as *buttons* entries. (Contributed by Serhiy Storchaka in :gh:`59396`.) +* :class:`tkinter.font.Font` can now wrap a font description without creating a + new named font, by passing it as *font* with ``exists=True`` and no *name*. + This avoids a loss of precision in :meth:`~tkinter.font.Font.actual`, + :meth:`~tkinter.font.Font.measure` and :meth:`~tkinter.font.Font.metrics`. + (Contributed by Serhiy Storchaka in :gh:`143990`.) + xml --- From 92e8239fa03ce2599c91b7bddfc80f154f67312d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 7 Jul 2026 10:58:02 +0300 Subject: [PATCH 3/3] Fix the TclError reference in the tkinter.font documentation Co-Authored-By: Claude Opus 4.8 --- Doc/library/tkinter.font.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/tkinter.font.rst b/Doc/library/tkinter.font.rst index 7fff7f3ee1e3f76..1ac7ac8e98d1662 100644 --- a/Doc/library/tkinter.font.rst +++ b/Doc/library/tkinter.font.rst @@ -82,7 +82,7 @@ The different font weights and slants are: .. note:: :meth:`!cget` and :meth:`configure` operate on a named font and raise - :exc:`TclError` for a wrapped font description. + :exc:`~tkinter.TclError` for a wrapped font description. Use :meth:`actual` to query the attributes of the latter. .. method:: config(**options)