diff --git a/Lib/asyncio/queues.py b/Lib/asyncio/queues.py index 30004f2bc9bacd2..8871cdc16c68b33 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) diff --git a/Lib/asyncio/streams.py b/Lib/asyncio/streams.py index a28c11e928f806a..d6076465f79366f 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}'] 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 000000000000000..b5a5ca272b9405a --- /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/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). 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 000000000000000..dc363e160074302 --- /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 7e1884edf5213e3..e999fe20287e2d6 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) {