From a50086daaab472972446fe52e606b4d4d6342514 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:59:51 -0700 Subject: [PATCH] gh-160326: Raise ValueError for a NaN timeout in Condition.wait() (GH-160327) A NaN timeout was treated as zero, which made the deadline loops built on Condition.wait(), such as Queue.get() and Semaphore.acquire(), spin without ever timing out. (cherry picked from commit 3387b193b1a1f58d9f6efc9ae8890db457f4cc54) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> --- Lib/test/lock_tests.py | 12 ++++++++++++ Lib/threading.py | 7 ++++--- .../2026-10-22-18-30-00.gh-issue-160326.nAn7Tq.rst | 5 +++++ 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-22-18-30-00.gh-issue-160326.nAn7Tq.rst diff --git a/Lib/test/lock_tests.py b/Lib/test/lock_tests.py index 691029a1a54f7fc..47025c041daf1e2 100644 --- a/Lib/test/lock_tests.py +++ b/Lib/test/lock_tests.py @@ -562,6 +562,10 @@ def f(): for r, dt in results2: self.assertTrue(r) + def test_nan_timeout(self): + evt = self.eventtype() + self.assertRaises(ValueError, evt.wait, float('nan')) + def test_set_and_clear(self): # gh-57711: check that wait() returns true even when the event is # cleared before the waiting thread is woken up. @@ -760,6 +764,14 @@ def f(): # In practice, this implementation has no spurious wakeups. self.assertFalse(result) + def test_nan_timeout(self): + cond = self.condtype() + nan = float('nan') + with cond: + self.assertRaises(ValueError, cond.wait, nan) + self.assertRaises(ValueError, cond.wait_for, lambda: False, nan) + self.assertEqual(len(cond._waiters), 0) + def test_waitfor(self): cond = self.condtype() state = 0 diff --git a/Lib/threading.py b/Lib/threading.py index abac31e25886fae..507d40a1baa275f 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -363,10 +363,11 @@ def wait(self, timeout=None): waiter.acquire() gotit = True else: - if timeout > 0: - gotit = waiter.acquire(True, timeout) - else: + if timeout <= 0: gotit = waiter.acquire(False) + else: + # A NaN timeout is rejected by acquire(). + gotit = waiter.acquire(True, timeout) return gotit finally: self._acquire_restore(saved_state) diff --git a/Misc/NEWS.d/next/Library/2026-10-22-18-30-00.gh-issue-160326.nAn7Tq.rst b/Misc/NEWS.d/next/Library/2026-10-22-18-30-00.gh-issue-160326.nAn7Tq.rst new file mode 100644 index 000000000000000..4349a7380c377d9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-22-18-30-00.gh-issue-160326.nAn7Tq.rst @@ -0,0 +1,5 @@ +Raise :exc:`ValueError` for a NaN *timeout* in +:meth:`threading.Condition.wait` and the blocking calls built on it, such as +:meth:`threading.Event.wait`, :meth:`threading.Semaphore.acquire` and +:meth:`queue.Queue.get`, instead of returning at once or looping without ever +timing out.