Skip to content

Commit 75c3c65

Browse files
authored
Merge branch 'main' into fix/dict_borrow_ref
2 parents 09e2148 + 2cd5b79 commit 75c3c65

10 files changed

Lines changed: 220 additions & 72 deletions

File tree

Doc/library/tkinter.font.rst

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,33 +20,53 @@ The different font weights and slants are:
2020

2121
.. class:: Font(root=None, font=None, name=None, exists=False, **options)
2222

23-
The :class:`Font` class represents a named font. *Font* instances are given
24-
unique names and can be specified by their family, size, and style
25-
configuration. Named fonts are Tk's method of creating and identifying
26-
fonts as a single object, rather than specifying a font by its attributes
27-
with each occurrence.
23+
The :class:`Font` class represents a font used by Tk widgets.
24+
It either creates a new *named font* or refers to an existing font.
25+
A named font is Tk's way of identifying a font as a single object that can
26+
be referred to by name and reconfigured in place,
27+
rather than respecifying its attributes at each use.
28+
29+
With *exists* false (the default), a new named font is created.
30+
Its attributes are taken from the font description *font* if it is given,
31+
overridden by any keyword *options*.
32+
The new font is named *name*, or a generated unique name if *name* is
33+
omitted.
34+
35+
With *exists* true, an existing font is referred to instead of being
36+
created.
37+
If *name* is given, it is the name of the font,
38+
which is reconfigured by *font* and *options* if either is given.
39+
If *name* is omitted, the font description *font* is wrapped as is,
40+
without creating a named font,
41+
so that it is used without loss of precision by :meth:`actual`,
42+
:meth:`measure` and :meth:`metrics`.
43+
In this case no keyword options are accepted,
44+
and the :attr:`!name` attribute is the description itself rather than a
45+
string.
46+
47+
The font description *font* is a tuple of the family name, the size and
48+
zero or more styles,
49+
or any other form accepted by Tk, such as the name of a named font.
50+
51+
The keyword *options* are:
52+
53+
| *family* - font family, for example, Courier, Times
54+
| *size* - font size
55+
| If *size* is positive it is interpreted as size in points.
56+
| If *size* is a negative number its absolute value is treated
57+
| as size in pixels.
58+
| *weight* - font emphasis (NORMAL, BOLD)
59+
| *slant* - ROMAN, ITALIC
60+
| *underline* - font underlining (0 - none, 1 - underline)
61+
| *overstrike* - font strikeout (0 - none, 1 - strikeout)
2862
2963
.. versionchanged:: 3.10
3064
Two fonts now compare equal (``==``) only when both are :class:`Font`
3165
instances with the same name belonging to the same Tcl interpreter.
3266

33-
arguments:
34-
35-
| *font* - font specifier tuple (family, size, options)
36-
| *name* - unique font name
37-
| *exists* - self points to existing named font if true
38-
39-
additional keyword options (ignored if *font* is specified):
40-
41-
| *family* - font family, for example, Courier, Times
42-
| *size* - font size
43-
| If *size* is positive it is interpreted as size in points.
44-
| If *size* is a negative number its absolute value is treated
45-
| as size in pixels.
46-
| *weight* - font emphasis (NORMAL, BOLD)
47-
| *slant* - ROMAN, ITALIC
48-
| *underline* - font underlining (0 - none, 1 - underline)
49-
| *overstrike* - font strikeout (0 - none, 1 - strikeout)
67+
.. versionchanged:: next
68+
A font description can now be wrapped without creating a new named font,
69+
and keyword options now override the attributes of the specified *font*.
5070

5171
.. method:: actual(option=None, displayof=None)
5272

@@ -59,6 +79,12 @@ The different font weights and slants are:
5979

6080
Retrieve an attribute of the font.
6181

82+
.. note::
83+
84+
:meth:`!cget` and :meth:`configure` operate on a named font and raise
85+
:exc:`~tkinter.TclError` for a wrapped font description.
86+
Use :meth:`actual` to query the attributes of the latter.
87+
6288
.. method:: config(**options)
6389
:no-typesetting:
6490

Doc/pylock.toml

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# uv pip compile Doc/requirements.txt --exclude-newer P14D --exclude-newer-package linklint=PT0S --exclude-newer-package python-docs-theme=PT0S --no-cache --output-file Doc/pylock.toml --python-version 3.12 --universal
33
lock-version = "1.0"
44
created-by = "uv"
5+
requires-python = ">=3.12"
56

67
[[packages]]
78
name = "alabaster"
@@ -23,9 +24,9 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/b4/03/374bd9e31b58e8a
2324

2425
[[packages]]
2526
name = "certifi"
26-
version = "2026.4.22"
27-
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", upload-time = 2026-04-22T11:26:11Z, size = 137077, hashes = { sha256 = "8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580" } }
28-
wheels = [{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", upload-time = 2026-04-22T11:26:09Z, size = 135707, hashes = { sha256 = "3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a" } }]
27+
version = "2026.6.17"
28+
sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", upload-time = 2026-06-17T10:31:07Z, size = 134594, hashes = { sha256 = "024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432" } }
29+
wheels = [{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", upload-time = 2026-06-17T10:31:06Z, size = 133289, hashes = { sha256 = "2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db" } }]
2930

3031
[[packages]]
3132
name = "charset-normalizer"
@@ -114,9 +115,9 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929
114115

115116
[[packages]]
116117
name = "idna"
117-
version = "3.13"
118-
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", upload-time = 2026-04-22T16:42:42Z, size = 194210, hashes = { sha256 = "585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242" } }
119-
wheels = [{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", upload-time = 2026-04-22T16:42:40Z, size = 68629, hashes = { sha256 = "892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3" } }]
118+
version = "3.18"
119+
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", upload-time = 2026-06-02T14:34:07Z, size = 196711, hashes = { sha256 = "ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848" } }
120+
wheels = [{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", upload-time = 2026-06-02T14:34:06Z, size = 65455, hashes = { sha256 = "7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2" } }]
120121

121122
[[packages]]
122123
name = "imagesize"
@@ -132,9 +133,9 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f
132133

133134
[[packages]]
134135
name = "linklint"
135-
version = "1.0.0"
136-
sdist = { url = "https://files.pythonhosted.org/packages/f3/59/a2bb261b3fc0e3bfdc9d3e6f8a37bdf1fb6eca8d992f23f55a83bf19acb1/linklint-1.0.0.tar.gz", upload-time = 2026-05-02T11:39:39Z, size = 21310, hashes = { sha256 = "52dc292f27b7eb4f3825d23ec1222d4a17bd116945b77c486406d97bb936e6c2" } }
137-
wheels = [{ url = "https://files.pythonhosted.org/packages/7b/78/98e52e4262416060a0eedc2e5514035cfcfc2fd962d09b84e27848c1aef3/linklint-1.0.0-py3-none-any.whl", upload-time = 2026-05-02T11:39:37Z, size = 12521, hashes = { sha256 = "bbb3f589ab65709cf23655ef6097bdc25180f80c0d42a4d9a1714366d2d3edab" } }]
136+
version = "1.0.1"
137+
sdist = { url = "https://files.pythonhosted.org/packages/62/22/c65a97b2192c3c317c472058bf52d7ca88e421998fa8d5c183b2280ab5d4/linklint-1.0.1.tar.gz", upload-time = 2026-06-09T12:30:47Z, size = 22412, hashes = { sha256 = "10201b4366edecfbeb3281883aff857dbe77d40bb56f16f212d372f597342d5f" } }
138+
wheels = [{ url = "https://files.pythonhosted.org/packages/7b/d8/b2f1708f55f4dd5358b48d28be07ff8e0dadd6d23e41ead2ed1424e90256/linklint-1.0.1-py3-none-any.whl", upload-time = 2026-06-09T12:30:46Z, size = 12828, hashes = { sha256 = "c931f110482f1d17808aa2df78b116adc44d7a14e9e79922c57bf9a7cba40a2d" } }]
138139

139140
[[packages]]
140141
name = "markupsafe"
@@ -162,8 +163,7 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c
162163
[[packages]]
163164
name = "pygments"
164165
version = "2.20.0"
165-
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", upload-time = 2026-03-29T13:29:33Z, size = 4955991, hashes = { sha256 = "6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f" } }
166-
wheels = [{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", upload-time = 2026-03-29T13:29:30Z, size = 1231151, hashes = { sha256 = "81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176" } }]
166+
archive = { url = "https://github.com/pygments/pygments/archive/2cad2642058441b59782a6a18f03c98c42d081f1.tar.gz", hashes = { sha256 = "e6ae46831285e86355eabb969bf2d0520655b001dd882147637f4bc500e56bfb" } }
167167

168168
[[packages]]
169169
name = "python-docs-theme"
@@ -173,9 +173,9 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/db/05/b9298eb9330c70a
173173

174174
[[packages]]
175175
name = "requests"
176-
version = "2.33.1"
177-
sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", upload-time = 2026-03-30T16:09:15Z, size = 134120, hashes = { sha256 = "18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517" } }
178-
wheels = [{ url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", upload-time = 2026-03-30T16:09:13Z, size = 64947, hashes = { sha256 = "4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a" } }]
176+
version = "2.34.2"
177+
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", upload-time = 2026-05-14T19:25:27Z, size = 142856, hashes = { sha256 = "f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed" } }
178+
wheels = [{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", upload-time = 2026-05-14T19:25:26Z, size = 73075, hashes = { sha256 = "2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0" } }]
179179

180180
[[packages]]
181181
name = "roman-numerals"
@@ -251,6 +251,6 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/bf/a4/66c1fd4f8fab88f
251251

252252
[[packages]]
253253
name = "urllib3"
254-
version = "2.6.3"
255-
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", upload-time = 2026-01-07T16:24:43Z, size = 435556, hashes = { sha256 = "1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed" } }
256-
wheels = [{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", upload-time = 2026-01-07T16:24:42Z, size = 131584, hashes = { sha256 = "bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4" } }]
254+
version = "2.7.0"
255+
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", upload-time = 2026-05-07T16:13:18Z, size = 433602, hashes = { sha256 = "231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c" } }
256+
wheels = [{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", upload-time = 2026-05-07T16:13:17Z, size = 131087, hashes = { sha256 = "9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897" } }]

Doc/requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
# Keep this version in sync with ``Doc/conf.py``.
1010
sphinx<9.0.0
1111

12+
# Temporary direct requirement, pending release of Pygments > 2.20.0
13+
# https://github.com/pygments/pygments/discussions/3145
14+
pygments @ https://github.com/pygments/pygments/archive/2cad2642058441b59782a6a18f03c98c42d081f1.tar.gz
15+
1216
blurb
1317

1418
sphinxext-opengraph~=0.13.0

Doc/whatsnew/3.16.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,12 @@ tkinter
408408
them.
409409
(Contributed by Serhiy Storchaka in :gh:`59396`.)
410410

411+
* :class:`tkinter.font.Font` can now wrap a font description without creating a
412+
new named font, by passing it as *font* with ``exists=True`` and no *name*.
413+
This avoids a loss of precision in :meth:`~tkinter.font.Font.actual`,
414+
:meth:`~tkinter.font.Font.measure` and :meth:`~tkinter.font.Font.metrics`.
415+
(Contributed by Serhiy Storchaka in :gh:`143990`.)
416+
411417
xml
412418
---
413419

Lib/test/test_array.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import collections.abc
66
import unittest
77
from test import support
8-
from test.support import import_helper
8+
from test.support import import_helper, script_helper
99
from test.support import os_helper
1010
from test.support import _2G
1111
from test.support import subTests
@@ -42,6 +42,23 @@ def test_bad_constructor(self):
4242
self.assertRaises(ValueError, array.array, 'x')
4343
self.assertRaises(ValueError, array.array, 'Z')
4444

45+
@support.cpython_only
46+
def test_does_not_crash_on_broken_imports(self):
47+
# gh-153210
48+
code = """if 1:
49+
import collections.abc
50+
51+
del collections.abc.MutableSequence
52+
53+
try:
54+
import array # it used to crash before
55+
except AttributeError:
56+
pass
57+
else:
58+
raise AssertionError('AttributeError was not raised')
59+
"""
60+
script_helper.assert_python_ok('-c', code)
61+
4562
@support.cpython_only
4663
def test_disallow_instantiation(self):
4764
my_array = array.array("I")

Lib/test/test_tkinter/test_font.py

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,70 @@ def test_configure(self):
4444
self.assertRaises(TypeError, self.font.cget)
4545
self.assertRaises(TypeError, self.font.cget, 'size', 'weight')
4646

47+
def test_create(self):
48+
sizetype = int if self.wantobjects else str
49+
50+
# A new named font is created from the font description...
51+
f = font.Font(root=self.root, font=('Times', 20, 'bold'))
52+
self.assertIn(f.name, font.names(self.root))
53+
self.assertEqual(f.actual('weight'), 'bold')
54+
self.assertEqual(f.cget('size'), sizetype(20))
55+
56+
# ... or from the keyword options.
57+
f = font.Font(root=self.root, family='Times', size=20, weight='bold')
58+
self.assertIn(f.name, font.names(self.root))
59+
self.assertEqual(f.actual('weight'), 'bold')
60+
self.assertEqual(f.cget('size'), sizetype(20))
61+
62+
# Explicit options override the corresponding settings of *font*.
63+
f = font.Font(root=self.root, font=('Times', 20, 'bold'), weight='normal')
64+
self.assertEqual(f.actual('weight'), 'normal')
65+
self.assertEqual(f.cget('size'), sizetype(20))
66+
67+
# The new font can be given an explicit name.
68+
f = font.Font(root=self.root, name='testfont', font=('Times', 20))
69+
self.assertEqual(f.name, 'testfont')
70+
self.assertIn('testfont', font.names(self.root))
71+
self.assertEqual(f.cget('size'), sizetype(20))
72+
# Reusing the name of an existing font fails.
73+
self.assertRaises(tkinter.TclError, font.Font, root=self.root,
74+
name='testfont', font=('Times', 10))
75+
76+
def test_existing(self):
77+
sizetype = int if self.wantobjects else str
78+
79+
# With a name, refer to the existing named font.
80+
named = font.Font(root=self.root, name='existingfont', family='Times', size=20)
81+
f = font.Font(root=self.root, name='existingfont', exists=True)
82+
self.assertEqual(f.name, 'existingfont')
83+
self.assertEqual(f.cget('size'), sizetype(20))
84+
# Referring to a non-existent named font fails.
85+
self.assertRaises(tkinter.TclError, font.Font, root=self.root,
86+
name='nosuchfont', exists=True)
87+
# A name and options reconfigure the existing font.
88+
font.Font(root=self.root, name='existingfont', exists=True, size=8)
89+
self.assertEqual(f.cget('size'), sizetype(8))
90+
91+
# With a description and no name, the description is wrapped without
92+
# creating a new named font (gh-143990), so that it is used without
93+
# loss of precision by actual(), measure() and metrics().
94+
f = font.Font(root=self.root, font=('Times', 20, 'bold'), exists=True)
95+
self.assertEqual(f.name, ('Times', 20, 'bold'))
96+
self.assertEqual(str(f), 'Times 20 bold')
97+
self.assertNotIn(f.name, font.names(self.root))
98+
self.assertEqual(f.actual('weight'), 'bold')
99+
self.assertEqual(f.actual('size'), sizetype(20))
100+
# It can be used as a widget option, with the same effect as the
101+
# description itself (gh-143990).
102+
self.assertEqual(tkinter.Label(self.root, font=f).cget('font'),
103+
tkinter.Label(self.root, font=f.name).cget('font'))
104+
105+
# Options cannot be combined with a wrapped description.
106+
self.assertRaises(TypeError, font.Font, root=self.root,
107+
font=('Times', 20), exists=True, weight='bold')
108+
# A name or a description is required.
109+
self.assertRaises(TypeError, font.Font, root=self.root, exists=True)
110+
47111
def test_copy(self):
48112
f = font.Font(root=self.root, family='Times', size=10, weight='bold')
49113
copied = f.copy()
@@ -60,10 +124,7 @@ def test_copy(self):
60124

61125
def test_unicode_family(self):
62126
family = 'MS \u30b4\u30b7\u30c3\u30af'
63-
try:
64-
f = font.Font(root=self.root, family=family, exists=True)
65-
except tkinter.TclError:
66-
f = font.Font(root=self.root, family=family, exists=False)
127+
f = font.Font(root=self.root, family=family)
67128
self.assertEqual(f.cget('family'), family)
68129
del f
69130
gc_collect()
@@ -96,6 +157,19 @@ def test_equality(self):
96157
self.assertEqual(font1, font2)
97158
self.assertNotEqual(font1, font1.copy())
98159

160+
# Wrapped descriptions (gh-143990) compare by the description.
161+
w1 = font.Font(root=self.root, font=('Times', 20, 'bold'), exists=True)
162+
w2 = font.Font(root=self.root, font=('Times', 20, 'bold'), exists=True)
163+
self.assertIsNot(w1, w2)
164+
self.assertEqual(w1, w2)
165+
w3 = font.Font(root=self.root, font=('Times', 12), exists=True)
166+
self.assertNotEqual(w1, w3)
167+
# A wrapped description never equals a named font, even one whose name
168+
# is the string form of the description.
169+
named = font.Font(root=self.root, name=str(w1), family='Courier')
170+
self.assertNotEqual(w1, named)
171+
self.assertNotEqual(named, w1)
172+
99173
self.assertNotEqual(font1, 0)
100174
self.assertEqual(font1, ALWAYS_EQ)
101175

0 commit comments

Comments
 (0)