Repository: agronholm/anyio

Issue:
add sleep_until()
Often it's less code to use `sleep_until()` rather than `sleep()`, since it handles times in the past.

example:
```python3
elapsed = trio.current_time() - t_start
await trio.sleep(max(0, period - elapsed))
```
vs.
```python3
await trio.sleep_until(t_start + period)
```

Comments:

Patch:
diff --git a/docs/api.rst b/docs/api.rst
index 8260bd7..d921f1b 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -8,6 +8,8 @@ Event loop
 .. autofunction:: anyio.get_all_backends
 .. autofunction:: anyio.get_cancelled_exc_class
 .. autofunction:: anyio.sleep
+.. autofunction:: anyio.sleep_forever
+.. autofunction:: anyio.sleep_until
 .. autofunction:: anyio.current_time
 
 Asynchronous resources
diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst
index 4764d91..b640178 100644
--- a/docs/versionhistory.rst
+++ b/docs/versionhistory.rst
@@ -6,6 +6,7 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
 **UNRELEASED**
 
 - Added ``env`` and ``cwd`` keyword arguments to ``run_process()`` and ``open_process``.
+- Added the ``sleep_forever()`` and ``sleep_until()`` functions
 - Changed asyncio task groups so that if the host and child tasks have only raised
   ``CancelledErrors``, just one ``CancelledError`` will now be raised instead of an
   ``ExceptionGroup``, allowing asyncio to ignore it when it propagates out of the task
diff --git a/setup.cfg b/setup.cfg
index fe575f5..353182a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -41,12 +41,14 @@ anyio = py.typed
 
 [options.extras_require]
 test =
+    mock >= 4; python_version < '3.8'
     coverage[toml] >= 4.5
     hypothesis >= 4.0
     pytest >= 6.0
+    pytest-mock >= 3.6.1
     trustme
-    uvloop < 0.15; python_version < '3.7' and platform_python_implementation == 'CPython' and platform_system != 'Windows'
-    uvloop >= 0.15; python_version >= '3.7' and platform_python_implementation == 'CPython' and platform_system != 'Windows'
+    uvloop < 0.15; python_version < '3.7' and (platform_python_implementation == 'CPython' and platform_system != 'Windows')
+    uvloop >= 0.15; python_version >= '3.7' and (platform_python_implementation == 'CPython' and platform_system != 'Windows')
 trio = trio >= 0.16
 doc =
     sphinx_rtd_theme
diff --git a/src/anyio/__init__.py b/src/anyio/__init__.py
index da6124c..74caae8 100644
--- a/src/anyio/__init__.py
+++ b/src/anyio/__init__.py
@@ -3,6 +3,8 @@ __all__ = (
     'maybe_async_cm',
     'run',
     'sleep',
+    'sleep_forever',
+    'sleep_until',
     'current_time',
     'get_all_backends',
     'get_cancelled_exc_class',
@@ -71,7 +73,9 @@ __all__ = (
 )
 
 from ._core._compat import maybe_async, maybe_async_cm
-from ._core._eventloop import current_time, get_all_backends, get_cancelled_exc_class, run, sleep
+from ._core._eventloop import (
+    current_time, get_all_backends, get_cancelled_exc_class, run, sleep, sleep_forever,
+    sleep_until)
 from ._core._exceptions import (
     BrokenResourceError, BrokenWorkerProcess, BusyResourceError, ClosedResourceError,
     DelimiterNotFound, EndOfStream, ExceptionGroup, IncompleteRead, TypedAttributeLookupError,
diff --git a/src/anyio/_core/_eventloop.py b/src/anyio/_core/_eventloop.py
index ba14385..6021ab9 100644
--- a/src/anyio/_core/_eventloop.py
+++ b/src/anyio/_core/_eventloop.py
@@ -1,3 +1,4 @@
+import math
 import sys
 import threading
 from contextlib import contextmanager
@@ -68,6 +69,32 @@ async def sleep(delay: float) -> None:
     return await get_asynclib().sleep(delay)
 
 
+async def sleep_forever() -> None:
+    """
+    Pause the current task until it's cancelled.
+
+    This is a shortcut for ``sleep(math.inf)``.
+
+    .. versionadded:: 3.1
+
+    """
+    await sleep(math.inf)
+
+
+async def sleep_until(deadline: float) -> None:
+    """
+    Pause the current task until the given time.
+
+    :param deadline: the absolute time to wake up at (according to the internal monotonic clock of
+        the event loop)
+
+    .. versionadded:: 3.1
+
+    """
+    now = current_time()
+    await sleep(max(deadline - now, 0))
+
+
 def current_time() -> DeprecatedAwaitableFloat:
     """
     Return the current value of the event loop's internal clock.


Test patch:
diff --git a/tests/test_eventloop.py b/tests/test_eventloop.py
new file mode 100644
index 0000000..1fefde7
--- /dev/null
+++ b/tests/test_eventloop.py
@@ -0,0 +1,37 @@
+import math
+import sys
+
+import pytest
+
+from anyio import sleep_forever, sleep_until
+
+if sys.version_info < (3, 8):
+    from mock import AsyncMock
+else:
+    from unittest.mock import AsyncMock
+
+pytestmark = pytest.mark.anyio
+fake_current_time = 1620581544.0
+
+
+@pytest.fixture
+def fake_sleep(mocker):
+    mocker.patch('anyio._core._eventloop.current_time', return_value=fake_current_time)
+    return mocker.patch('anyio._core._eventloop.sleep', AsyncMock())
+
+
+async def test_sleep_until(fake_sleep):
+    deadline = fake_current_time + 500.102352
+    await sleep_until(deadline)
+    fake_sleep.assert_called_once_with(deadline - fake_current_time)
+
+
+async def test_sleep_until_in_past(fake_sleep):
+    deadline = fake_current_time - 500.102352
+    await sleep_until(deadline)
+    fake_sleep.assert_called_once_with(0)
+
+
+async def test_sleep_forever(fake_sleep):
+    await sleep_forever()
+    fake_sleep.assert_called_once_with(math.inf)