Skip to content

Commit 45010f4

Browse files
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 <noreply@anthropic.com>
1 parent 4572903 commit 45010f4

3 files changed

Lines changed: 58 additions & 6 deletions

File tree

Lib/test/test_tkinter/test_font.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,44 @@ def test_create(self):
7373
self.assertRaises(tkinter.TclError, font.Font, root=self.root,
7474
name='testfont', font=('Times', 10))
7575

76+
def test_create_from_named_font(self):
77+
# gh-143990: a font created from a named font copies its configured
78+
# options, preserving a size specified in pixels (a negative size).
79+
sizetype = int if self.wantobjects else str
80+
named = font.Font(root=self.root, name='my named font', # name with spaces
81+
family='Times', size=-20, weight='bold')
82+
# The source is the name of a named font or a Font representing one.
83+
for source in ['my named font', named]:
84+
with self.subTest(source=source):
85+
f = font.Font(root=self.root, font=source)
86+
self.assertEqual(f.cget('size'), sizetype(-20))
87+
self.assertEqual(f.actual('family'), named.actual('family'))
88+
self.assertEqual(f.actual('weight'), 'bold')
89+
# Explicit options still override the copied settings.
90+
f = font.Font(root=self.root, font=named, size=30)
91+
self.assertEqual(f.cget('size'), sizetype(30))
92+
93+
def test_create_from_description(self):
94+
# gh-143990: a font created from a font description is resolved via
95+
# "font actual", so a size in pixels (negative) becomes a size in points.
96+
descriptions = [
97+
('Times', -20), # tuple
98+
('Times', -20, 'bold'), # tuple with a style
99+
'Times -20', # string
100+
'Times -20 bold', # string with a style
101+
'{Times New Roman} -20', # string, family with spaces
102+
# a Font wrapping a description, as a tuple and as a string
103+
font.Font(root=self.root, font=('Times', -20), exists=True),
104+
font.Font(root=self.root, font='Times -20', exists=True),
105+
]
106+
for desc in descriptions:
107+
with self.subTest(font=desc):
108+
f = font.Font(root=self.root, font=desc)
109+
# resolved as if the description were wrapped by exists=True
110+
wrapped = font.Font(root=self.root, font=desc, exists=True)
111+
self.assertEqual(f.actual(), wrapped.actual())
112+
self.assertGreater(int(f.cget('size')), 0) # pixels -> points
113+
76114
def test_existing(self):
77115
sizetype = int if self.wantobjects else str
78116

@@ -109,16 +147,19 @@ def test_existing(self):
109147
self.assertRaises(TypeError, font.Font, root=self.root, exists=True)
110148

111149
def test_copy(self):
112-
f = font.Font(root=self.root, family='Times', size=10, weight='bold')
150+
# size=-20 (pixels): copy() copies the configured options, so the
151+
# size is preserved rather than resolved (gh-143990).
152+
f = font.Font(root=self.root, family='Times', size=-20, weight='bold')
113153
copied = f.copy()
114154
self.assertIsInstance(copied, font.Font)
115155
self.assertIsNot(copied, f)
116156
self.assertNotEqual(copied.name, f.name)
117157
self.assertEqual(copied.actual(), f.actual())
118-
# The copy is independent of the original.
119158
sizetype = int if self.wantobjects else str
159+
self.assertEqual(copied.cget('size'), sizetype(-20))
160+
# The copy is independent of the original.
120161
copied.configure(size=20)
121-
self.assertEqual(f.cget('size'), sizetype(10))
162+
self.assertEqual(f.cget('size'), sizetype(-20))
122163
self.assertEqual(copied.cget('size'), sizetype(20))
123164
self.assertRaises(TypeError, f.copy, 'x')
124165

Lib/tkinter/font.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,15 @@ def __init__(self, root=None, font=None, name=None, exists=False,
8383
self.name = font
8484
else:
8585
if font:
86-
# start from the actual settings of the given font
87-
font = tk.splitlist(tk.call("font", "actual", font))
86+
# start from the settings of the given font
87+
try:
88+
# a named font: copy its options, preserving the size,
89+
# which can be negative (specified in pixels)
90+
font = tk.splitlist(tk.call("font", "configure", font))
91+
except tkinter.TclError:
92+
# a font description: resolve it ("font configure" only
93+
# accepts a font name); this loses a size in pixels
94+
font = tk.splitlist(tk.call("font", "actual", font))
8895
if options:
8996
# explicit options override the corresponding settings
9097
settings = self._mkdict(font)
@@ -146,7 +153,7 @@ def __del__(self):
146153

147154
def copy(self):
148155
"Return a distinct copy of the current font"
149-
return Font(self._tk, **self.actual())
156+
return Font(self._tk, self.name)
150157

151158
def actual(self, option=None, displayof=None):
152159
"Return actual font attributes"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
A :class:`tkinter.font.Font` created from a named font,
2+
including by :meth:`~tkinter.font.Font.copy`,
3+
now copies its configured options rather than the options resolved by Tcl's ``font actual``,
4+
preserving a size specified in pixels (a negative size).

0 commit comments

Comments
 (0)