From ae1f4a713a12cd907be4d5b9ef720eec1a781d27 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 7 Jul 2026 17:55:23 +0300 Subject: [PATCH] gh-143990: Preserve the size when creating a Font from a named font tkinter.font.Font now copies the options of a named font (via "font configure") instead of the options resolved by "font actual", which would resolve a size specified in pixels (a negative size) to points. A font description is still resolved, as it cannot be parsed otherwise. Font.copy(), which has always been equivalent to constructing a Font from the original font, is updated to match and now preserves the size too. Co-Authored-By: Claude Opus 4.8 --- Lib/test/test_tkinter/test_font.py | 47 +++++++++++++++++-- Lib/tkinter/font.py | 13 +++-- ...-07-07-17-50-54.gh-issue-143990.FoNtCf.rst | 4 ++ 3 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-07-17-50-54.gh-issue-143990.FoNtCf.rst diff --git a/Lib/test/test_tkinter/test_font.py b/Lib/test/test_tkinter/test_font.py index e6f331332eb20a..8e278456f383a1 100644 --- a/Lib/test/test_tkinter/test_font.py +++ b/Lib/test/test_tkinter/test_font.py @@ -73,6 +73,44 @@ def test_create(self): self.assertRaises(tkinter.TclError, font.Font, root=self.root, name='testfont', font=('Times', 10)) + def test_create_from_named_font(self): + # gh-143990: a font created from a named font copies its configured + # options, preserving a size specified in pixels (a negative size). + sizetype = int if self.wantobjects else str + named = font.Font(root=self.root, name='my named font', # name with spaces + family='Times', size=-20, weight='bold') + # The source is the name of a named font or a Font representing one. + for source in ['my named font', named]: + with self.subTest(source=source): + f = font.Font(root=self.root, font=source) + self.assertEqual(f.cget('size'), sizetype(-20)) + self.assertEqual(f.actual('family'), named.actual('family')) + self.assertEqual(f.actual('weight'), 'bold') + # Explicit options still override the copied settings. + f = font.Font(root=self.root, font=named, size=30) + self.assertEqual(f.cget('size'), sizetype(30)) + + def test_create_from_description(self): + # gh-143990: a font created from a font description is resolved via + # "font actual", so a size in pixels (negative) becomes a size in points. + descriptions = [ + ('Times', -20), # tuple + ('Times', -20, 'bold'), # tuple with a style + 'Times -20', # string + 'Times -20 bold', # string with a style + '{Times New Roman} -20', # string, family with spaces + # a Font wrapping a description, as a tuple and as a string + font.Font(root=self.root, font=('Times', -20), exists=True), + font.Font(root=self.root, font='Times -20', exists=True), + ] + for desc in descriptions: + with self.subTest(font=desc): + f = font.Font(root=self.root, font=desc) + # resolved as if the description were wrapped by exists=True + wrapped = font.Font(root=self.root, font=desc, exists=True) + self.assertEqual(f.actual(), wrapped.actual()) + self.assertGreater(int(f.cget('size')), 0) # pixels -> points + def test_existing(self): sizetype = int if self.wantobjects else str @@ -109,16 +147,19 @@ def test_existing(self): 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') + # size=-20 (pixels): copy() copies the configured options, so the + # size is preserved rather than resolved (gh-143990). + f = font.Font(root=self.root, family='Times', size=-20, weight='bold') copied = f.copy() self.assertIsInstance(copied, font.Font) self.assertIsNot(copied, f) self.assertNotEqual(copied.name, f.name) self.assertEqual(copied.actual(), f.actual()) - # The copy is independent of the original. sizetype = int if self.wantobjects else str + self.assertEqual(copied.cget('size'), sizetype(-20)) + # The copy is independent of the original. copied.configure(size=20) - self.assertEqual(f.cget('size'), sizetype(10)) + self.assertEqual(f.cget('size'), sizetype(-20)) self.assertEqual(copied.cget('size'), sizetype(20)) self.assertRaises(TypeError, f.copy, 'x') diff --git a/Lib/tkinter/font.py b/Lib/tkinter/font.py index d59b04d36638e7..7ce7047885fce5 100644 --- a/Lib/tkinter/font.py +++ b/Lib/tkinter/font.py @@ -83,8 +83,15 @@ def __init__(self, root=None, font=None, name=None, exists=False, self.name = font else: if font: - # start from the actual settings of the given font - font = tk.splitlist(tk.call("font", "actual", font)) + # start from the settings of the given font + try: + # a named font: copy its options, preserving the size, + # which can be negative (specified in pixels) + font = tk.splitlist(tk.call("font", "configure", font)) + except tkinter.TclError: + # a font description: resolve it ("font configure" only + # accepts a font name); this loses a size in pixels + font = tk.splitlist(tk.call("font", "actual", font)) if options: # explicit options override the corresponding settings settings = self._mkdict(font) @@ -146,7 +153,7 @@ def __del__(self): def copy(self): "Return a distinct copy of the current font" - return Font(self._tk, **self.actual()) + return Font(self._tk, self.name) def actual(self, option=None, displayof=None): "Return actual font attributes" diff --git a/Misc/NEWS.d/next/Library/2026-07-07-17-50-54.gh-issue-143990.FoNtCf.rst b/Misc/NEWS.d/next/Library/2026-07-07-17-50-54.gh-issue-143990.FoNtCf.rst new file mode 100644 index 00000000000000..a78dcccf482ab5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-07-17-50-54.gh-issue-143990.FoNtCf.rst @@ -0,0 +1,4 @@ +A :class:`tkinter.font.Font` created from a named font, +including by :meth:`~tkinter.font.Font.copy`, +now copies its configured options rather than the options resolved by Tcl's ``font actual``, +preserving a size specified in pixels (a negative size).