From d1cbbd3bf0164344532e1c62bff6d76cc5171f8a Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Tue, 6 Jun 2023 11:28:08 +0200
Subject: [PATCH] timeout functions now raise ValueError on NaN inputs
---
newsfragments/2493.breaking.rst | 1 +
trio/_tests/test_timeouts.py | 36 ++++++++++++++++++++++++---------
trio/_timeouts.py | 23 +++++++++++++++++----
3 files changed, 46 insertions(+), 14 deletions(-)
create mode 100644 newsfragments/2493.breaking.rst
diff --git a/newsfragments/2493.breaking.rst b/newsfragments/2493.breaking.rst
new file mode 100644
index 0000000000..51c2985d1a
--- /dev/null
+++ b/newsfragments/2493.breaking.rst
@@ -0,0 +1 @@
+Timeout functions now raise `ValueError` if passed `math.nan`. This includes `trio.sleep`, `trio.sleep_until`, `trio.move_on_at`, `trio.move_on_after`, `trio.fail_at` and `trio.fail_after`.
diff --git a/trio/_tests/test_timeouts.py b/trio/_tests/test_timeouts.py
index c817c49588..f55e697d6f 100644
--- a/trio/_tests/test_timeouts.py
+++ b/trio/_tests/test_timeouts.py
@@ -53,9 +53,6 @@ async def sleep_2():
await check_takes_about(sleep_2, TARGET)
- with pytest.raises(ValueError):
- await sleep(-1)
-
with assert_checkpoints():
await sleep(0)
# This also serves as a test of the trivial move_on_at
@@ -66,10 +63,6 @@ async def sleep_2():
@slow
async def test_move_on_after():
- with pytest.raises(ValueError):
- with move_on_after(-1):
- pass # pragma: no cover
-
async def sleep_3():
with move_on_after(TARGET):
await sleep(100)
@@ -99,6 +92,29 @@ async def sleep_5():
with fail_after(100):
await sleep(0)
- with pytest.raises(ValueError):
- with fail_after(-1):
- pass # pragma: no cover
+
+async def test_timeouts_raise_value_error():
+ # deadlines are allowed to be negative, but not delays.
+ # neither delays nor deadlines are allowed to be NaN
+
+ nan = float("nan")
+
+ for fun, val in (
+ (sleep, -1),
+ (sleep, nan),
+ (sleep_until, nan),
+ ):
+ with pytest.raises(ValueError):
+ await fun(val)
+
+ for cm, val in (
+ (fail_after, -1),
+ (fail_after, nan),
+ (fail_at, nan),
+ (move_on_after, -1),
+ (move_on_after, nan),
+ (move_on_at, nan),
+ ):
+ with pytest.raises(ValueError):
+ with cm(val):
+ pass # pragma: no cover
diff --git a/trio/_timeouts.py b/trio/_timeouts.py
index 1f7878f89e..ad31e78404 100644
--- a/trio/_timeouts.py
+++ b/trio/_timeouts.py
@@ -1,3 +1,4 @@
+import math
from contextlib import contextmanager
import trio
@@ -10,7 +11,12 @@ def move_on_at(deadline):
Args:
deadline (float): The deadline.
+ Raises:
+ ValueError: if deadline is NaN.
+
"""
+ if math.isnan(deadline):
+ raise ValueError("deadline must not be NaN")
return trio.CancelScope(deadline=deadline)
@@ -22,10 +28,9 @@ def move_on_after(seconds):
seconds (float): The timeout.
Raises:
- ValueError: if timeout is less than zero.
+ ValueError: if timeout is less than zero or NaN.
"""
-
if seconds < 0:
raise ValueError("timeout must be non-negative")
return move_on_at(trio.current_time() + seconds)
@@ -52,6 +57,9 @@ async def sleep_until(deadline):
the past, in which case this function executes a checkpoint but
does not block.
+ Raises:
+ ValueError: if deadline is NaN.
+
"""
with move_on_at(deadline):
await sleep_forever()
@@ -65,7 +73,7 @@ async def sleep(seconds):
insert a checkpoint without actually blocking.
Raises:
- ValueError: if *seconds* is negative.
+ ValueError: if *seconds* is negative or NaN.
"""
if seconds < 0:
@@ -96,9 +104,13 @@ def fail_at(deadline):
:func:`fail_at`, then it's caught and :exc:`TooSlowError` is raised in its
place.
+ Args:
+ deadline (float): The deadline.
+
Raises:
TooSlowError: if a :exc:`Cancelled` exception is raised in this scope
and caught by the context manager.
+ ValueError: if deadline is NaN.
"""
@@ -119,10 +131,13 @@ def fail_after(seconds):
it's caught and discarded. When it reaches :func:`fail_after`, then it's
caught and :exc:`TooSlowError` is raised in its place.
+ Args:
+ seconds (float): The timeout.
+
Raises:
TooSlowError: if a :exc:`Cancelled` exception is raised in this scope
and caught by the context manager.
- ValueError: if *seconds* is less than zero.
+ ValueError: if *seconds* is less than zero or NaN.
"""
if seconds < 0: