From 45010f441d392558f1be8f3cea7d86954ed2217d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 8 Jul 2026 11:30:12 +0300 Subject: [PATCH 1/4] 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 --- 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). From 0bf2f073194520645abd768f7debc234011e248e Mon Sep 17 00:00:00 2001 From: Timofei <128279579+deadlovelll@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:24:24 +0300 Subject: [PATCH 2/4] gh-150358: Remove unused _complete_fut attribute from asyncio.StreamWriter (#150359) --- Lib/asyncio/streams.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/asyncio/streams.py b/Lib/asyncio/streams.py index a28c11e928f806..d6076465f79366 100644 --- a/Lib/asyncio/streams.py +++ b/Lib/asyncio/streams.py @@ -321,8 +321,6 @@ def __init__(self, transport, protocol, reader, loop): assert reader is None or isinstance(reader, StreamReader) self._reader = reader self._loop = loop - self._complete_fut = self._loop.create_future() - self._complete_fut.set_result(None) def __repr__(self): info = [self.__class__.__name__, f'transport={self._transport!r}'] From 1051384fcdfa88dd88d66dfc93b60aec9ca1ad2e Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 8 Jul 2026 12:41:38 +0300 Subject: [PATCH 3/4] gh-153292: Fix data race in `threading.RLock.__repr__` in FT builds (#153299) --- .../test_free_threading/test_threading.py | 26 +++++++++++++++++++ ...-07-08-01-03-17.gh-issue-153292.oHDt3l.rst | 1 + Modules/_threadmodule.c | 2 +- 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 Lib/test/test_free_threading/test_threading.py create mode 100644 Misc/NEWS.d/next/Library/2026-07-08-01-03-17.gh-issue-153292.oHDt3l.rst diff --git a/Lib/test/test_free_threading/test_threading.py b/Lib/test/test_free_threading/test_threading.py new file mode 100644 index 00000000000000..b5a5ca272b9405 --- /dev/null +++ b/Lib/test/test_free_threading/test_threading.py @@ -0,0 +1,26 @@ +import unittest +from test.support import threading_helper + +threading_helper.requires_working_threading(module=True) + + +class TestRlock(unittest.TestCase): + def test_repr_race(self): + # gh-153292 + import _thread + r = _thread.RLock() + + def repr_thread(): + for _ in range(2000): + repr(r) + + def mutate_thread(): + for _ in range(2000): + r.acquire() + r.release() + + threading_helper.run_concurrently([repr_thread, mutate_thread]) + + +if __name__ == "__main__": + unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-07-08-01-03-17.gh-issue-153292.oHDt3l.rst b/Misc/NEWS.d/next/Library/2026-07-08-01-03-17.gh-issue-153292.oHDt3l.rst new file mode 100644 index 00000000000000..dc363e16007430 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-08-01-03-17.gh-issue-153292.oHDt3l.rst @@ -0,0 +1 @@ +Fix data race in repr of :class:`threading.RLock` in free-threading build. diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 7e1884edf5213e..e999fe20287e2d 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -1288,7 +1288,7 @@ static PyObject * rlock_repr(PyObject *op) { rlockobject *self = rlockobject_CAST(op); - PyThread_ident_t owner = self->lock.thread; + PyThread_ident_t owner = FT_ATOMIC_LOAD_ULLONG_RELAXED(self->lock.thread); int locked = rlock_locked_impl(self); size_t count; if (locked) { From 754b9f28b31e659691e1f3954c9606d0ef064d5d Mon Sep 17 00:00:00 2001 From: Timofei <128279579+deadlovelll@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:15:18 +0300 Subject: [PATCH 4/4] gh-149740: Remove redundant self.empty() check in asyncio.Queue.get() (#149741) Co-authored-by: Kumar Aditya --- Lib/asyncio/queues.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/queues.py b/Lib/asyncio/queues.py index 30004f2bc9bacd..8871cdc16c68b3 100644 --- a/Lib/asyncio/queues.py +++ b/Lib/asyncio/queues.py @@ -178,7 +178,7 @@ async def get(self): or if the queue has been shut down immediately. """ while self.empty(): - if self._is_shutdown and self.empty(): + if self._is_shutdown: raise QueueShutDown getter = self._get_loop().create_future() self._getters.append(getter)