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: