From 62f59ea4c077f6b3acda14b05ca78ff5d8299019 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 8 Jul 2026 11:30:12 +0300 Subject: [PATCH 1/3] gh-143990: Preserve the size when creating a Font from a named font (GH-153267) 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 (cherry picked from commit 45010f441d392558f1be8f3cea7d86954ed2217d) --- Lib/test/test_tkinter/test_font.py | 41 +++++++++++++++++-- Lib/tkinter/font.py | 13 ++++-- ...-07-07-17-50-54.gh-issue-143990.FoNtCf.rst | 4 ++ 3 files changed, 52 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 8adc9e1151db4ed..4e3dba923afa14c 100644 --- a/Lib/test/test_tkinter/test_font.py +++ b/Lib/test/test_tkinter/test_font.py @@ -44,17 +44,52 @@ def test_configure(self): self.assertRaises(TypeError, self.font.cget) self.assertRaises(TypeError, self.font.cget, 'size', 'weight') + 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 + ] + for desc in descriptions: + with self.subTest(font=desc): + f = font.Font(root=self.root, font=desc) + self.assertGreater(int(f.cget('size')), 0) # pixels -> points + 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 896e910d69f6f33..1face04279af7ff 100644 --- a/Lib/tkinter/font.py +++ b/Lib/tkinter/font.py @@ -71,8 +71,15 @@ def __init__(self, root=None, font=None, name=None, exists=False, 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)) + # 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)) else: font = self._set(options) if not name: @@ -125,7 +132,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 000000000000000..a78dcccf482ab59 --- /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). From b15245aace468fd7bae0e6c5133e11e7a18e6cee Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 8 Jul 2026 15:23:05 +0300 Subject: [PATCH 2/3] gh-143990: Fix the 3.15 backport of GH-153267 Drop the assertion in test_create_from_named_font that explicit options override the copied font settings: that behavior was added by GH-152025 and is not in 3.15, where options are ignored when a font is specified. Co-Authored-By: Claude Opus 4.8 --- Lib/test/test_tkinter/test_font.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/test/test_tkinter/test_font.py b/Lib/test/test_tkinter/test_font.py index 4e3dba923afa14c..8f7a473a9e957ac 100644 --- a/Lib/test/test_tkinter/test_font.py +++ b/Lib/test/test_tkinter/test_font.py @@ -57,9 +57,7 @@ def test_create_from_named_font(self): 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)) + # (Options overriding the given font are only supported since 3.16.) def test_create_from_description(self): # gh-143990: a font created from a font description is resolved via From d92fea4a26fee21056f9e752a3ee1c85869916af Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 8 Jul 2026 15:25:00 +0300 Subject: [PATCH 3/3] gh-143990: Drop a redundant comment in the 3.15 backport Co-Authored-By: Claude Opus 4.8 --- Lib/test/test_tkinter/test_font.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_tkinter/test_font.py b/Lib/test/test_tkinter/test_font.py index 8f7a473a9e957ac..6423a4e70af01ca 100644 --- a/Lib/test/test_tkinter/test_font.py +++ b/Lib/test/test_tkinter/test_font.py @@ -57,7 +57,6 @@ def test_create_from_named_font(self): self.assertEqual(f.cget('size'), sizetype(-20)) self.assertEqual(f.actual('family'), named.actual('family')) self.assertEqual(f.actual('weight'), 'bold') - # (Options overriding the given font are only supported since 3.16.) def test_create_from_description(self): # gh-143990: a font created from a font description is resolved via