https://github.com/python/cpython/commit/45010f441d392558f1be8f3cea7d86954ed2217d
commit: 45010f441d392558f1be8f3cea7d86954ed2217d
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-07-08T11:30:12+03:00
summary:

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 <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-07-07-17-50-54.gh-issue-143990.FoNtCf.rst
M Lib/test/test_tkinter/test_font.py
M Lib/tkinter/font.py

diff --git a/Lib/test/test_tkinter/test_font.py 
b/Lib/test/test_tkinter/test_font.py
index e6f331332eb20a9..8e278456f383a1c 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 d59b04d36638e79..7ce7047885fce5f 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 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).

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to