-
-
Notifications
You must be signed in to change notification settings - Fork 369
Change fail_after
&move_on_after
to set deadline relative to entering. Add CancelScope.relative_deadline
#3010
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
dea2e79
d6e91cc
dc3bd3d
da25949
7a8262a
d028037
5447f45
1381560
742c013
0075b56
9d3d0b9
b29db0d
480681d
00ba1ca
510aaa1
7079d71
3474128
df5876d
a240948
0183b43
ca63354
7591297
75fc52b
5a32eb0
c2a3d8e
f8e9417
36786a6
8e863b8
6a7650d
02ac588
bf12a80
9961abc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
:func:`trio.move_on_after` and :func:`trio.fail_after` previously set the deadline relative to initialization time, instead of more intuitively upon entering the context manager. Any code where these two points in time do not line up (according to :func:`trio.current_time`) will now issue a `DeprecationWarning`. To silence the warning and adopt the new behaviour, pass ``timeout_from_enter = True``. If you want to retain current behaviour, you should instead use :func:`trio.move_on_at`/:func:`trio.fail_at`. For example: | ||
|
||
.. code-block:: python | ||
# this is equivalent to how trio.move_on_after(5) would function previously | ||
cs = trio.move_on_at(trio.current_time() + 5) | ||
... | ||
with cs: | ||
... | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
:func:`trio.CancelScope` now has an attribute ``relative_deadline`` that can be used together with, or instead of, ``deadline``. :func:`trio.move_on_after` and :func:`trio.fail_after` now use this functionality in order to resolve the absolute deadline upon entering the context manager. | ||
|
||
If setting both ``deadline`` and ``relative_deadline`` before entering the cm, the deadline will be set to ``min(deadline, trio.current_time() + relative_deadline)``. | ||
Accessing ``relative_deadline`` after entering will return remaining time until deadline (i.e. ``deadline - trio.current_time()``. Setting ``relative_deadline`` after entering will set ``deadline`` to ``trio.current_time() + relative_deadline`` | ||
|
||
Example: | ||
|
||
.. code-block:: python | ||
|
||
my_cs = trio.CancelScope(relative_deadline = 5) | ||
... | ||
with my_cs: # my_cs.deadline will now be 5 seconds into the future | ||
... |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -543,16 +543,60 @@ class CancelScope: | |
|
||
# Constructor arguments: | ||
_deadline: float = attrs.field(default=inf, kw_only=True, alias="deadline") | ||
# use float|None, or math.inf? | ||
# or take it as an opportunity to also introduce deadline=None? | ||
_relative_deadline: float | None = attrs.field( | ||
default=None, kw_only=True, alias="relative_deadline" | ||
) | ||
|
||
_shield: bool = attrs.field(default=False, kw_only=True, alias="shield") | ||
|
||
# introduced during deprecation period of handling relative timeouts from | ||
# initialization | ||
_timeout_from_enter: bool = attrs.field( | ||
default=True, kw_only=True, alias="_timeout_from_enter" | ||
) | ||
_creation_time: float | None = attrs.field(default=None, alias="_creation_time") | ||
|
||
@enable_ki_protection | ||
def __enter__(self) -> Self: | ||
task = _core.current_task() | ||
if self._has_been_entered: | ||
raise RuntimeError( | ||
"Each CancelScope may only be used for a single 'with' block" | ||
) | ||
|
||
if ( | ||
self._creation_time is not None | ||
and abs(self._creation_time - current_time()) > 0.01 | ||
and not self._timeout_from_enter | ||
): | ||
# not using warn_deprecated because the message template is a weird fit | ||
# TODO: mention versions in the message? | ||
Zac-HD marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
warnings.warn( | ||
DeprecationWarning( | ||
"`move_on_after` and `fail_after` will change behaviour to " | ||
"start the deadline relative to entering the cm, instead of " | ||
"at creation time. To silence this warning and opt into the " | ||
"new behaviour, pass `timeout_from_enter=True`. " | ||
"To keep old behaviour, use `move_on_at(trio.current_time() + x)` " | ||
"(or `fail_at`), where `x` is the previous timeout length. " | ||
"See https://github.com/python-trio/trio/issues/2512" | ||
), | ||
stacklevel=2, | ||
) | ||
assert ( | ||
self._relative_deadline is not None | ||
), "User is manually passing `_timeout_from_enter=False`. Don't do that." | ||
self._relative_deadline -= current_time() - self._creation_time | ||
|
||
self._has_been_entered = True | ||
|
||
if ( | ||
self._relative_deadline is not None | ||
and self._relative_deadline + current_time() < self._deadline | ||
): | ||
self._deadline = self._relative_deadline + current_time() | ||
|
||
if current_time() >= self._deadline: | ||
self.cancel() | ||
with self._might_change_registered_deadline(): | ||
|
@@ -740,6 +784,25 @@ def deadline(self, new_deadline: float) -> None: | |
with self._might_change_registered_deadline(): | ||
self._deadline = float(new_deadline) | ||
|
||
@property | ||
def relative_deadline(self) -> float | None: | ||
"""TODO: write docstring""" | ||
if self._has_been_entered: | ||
if self.deadline == inf: | ||
return None | ||
return self._deadline - current_time() | ||
return self._relative_deadline | ||
|
||
@relative_deadline.setter | ||
def relative_deadline(self, relative_deadline: float | None) -> None: | ||
self._relative_deadline = relative_deadline | ||
if self._has_been_entered: | ||
with self._might_change_registered_deadline(): | ||
if relative_deadline is None: | ||
self._deadline = inf | ||
jakkdl marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
else: | ||
self._deadline = current_time() + relative_deadline | ||
|
||
@property | ||
def shield(self) -> bool: | ||
"""Read-write, :class:`bool`, default :data:`False`. So long as | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,17 @@ | ||
from __future__ import annotations | ||
|
||
import math | ||
from contextlib import AbstractContextManager, contextmanager | ||
from contextlib import contextmanager | ||
from typing import TYPE_CHECKING | ||
|
||
import trio | ||
|
||
from ._util import final | ||
|
||
if TYPE_CHECKING: | ||
from collections.abc import Generator | ||
from types import TracebackType | ||
|
||
|
||
def move_on_at(deadline: float) -> trio.CancelScope: | ||
"""Use as a context manager to create a cancel scope with the given | ||
|
@@ -23,9 +29,14 @@ def move_on_at(deadline: float) -> trio.CancelScope: | |
return trio.CancelScope(deadline=deadline) | ||
|
||
|
||
def move_on_after(seconds: float) -> trio.CancelScope: | ||
def move_on_after( | ||
seconds: float, | ||
*, | ||
timeout_from_enter: bool = False, | ||
_creation_time: float | None = None, | ||
) -> trio.CancelScope: | ||
"""Use as a context manager to create a cancel scope whose deadline is | ||
set to now + *seconds*. | ||
set to creation time + *seconds*. | ||
|
||
Args: | ||
seconds (float): The timeout. | ||
|
@@ -36,7 +47,15 @@ def move_on_after(seconds: float) -> trio.CancelScope: | |
""" | ||
if seconds < 0: | ||
raise ValueError("timeout must be non-negative") | ||
return move_on_at(trio.current_time() + seconds) | ||
if math.isnan(seconds): | ||
raise ValueError("timeout must not be NaN") | ||
if _creation_time is None: | ||
_creation_time = trio.current_time() | ||
return trio.CancelScope( | ||
relative_deadline=seconds, | ||
_timeout_from_enter=timeout_from_enter, | ||
_creation_time=_creation_time, | ||
) | ||
|
||
|
||
async def sleep_forever() -> None: | ||
|
@@ -94,9 +113,8 @@ class TooSlowError(Exception): | |
""" | ||
|
||
|
||
# workaround for PyCharm not being able to infer return type from @contextmanager | ||
# see https://youtrack.jetbrains.com/issue/PY-36444/PyCharm-doesnt-infer-types-when-using-contextlib.contextmanager-decorator | ||
def fail_at(deadline: float) -> AbstractContextManager[trio.CancelScope]: # type: ignore[misc] | ||
@contextmanager | ||
def fail_at(deadline: float) -> Generator[trio.CancelScope, None, None]: | ||
"""Creates a cancel scope with the given deadline, and raises an error if it | ||
is actually cancelled. | ||
|
||
|
@@ -123,11 +141,14 @@ def fail_at(deadline: float) -> AbstractContextManager[trio.CancelScope]: # typ | |
raise TooSlowError | ||
|
||
|
||
if not TYPE_CHECKING: | ||
fail_at = contextmanager(fail_at) | ||
# This was previously a simple @contextmanager-based cm, but in order to save | ||
# the current time at initialization it needed to be class-based. | ||
# Once that change of behaviour has gone through and the old functionality is removed | ||
# it can be reverted. | ||
|
||
|
||
|
||
def fail_after(seconds: float) -> AbstractContextManager[trio.CancelScope]: | ||
@final | ||
class fail_after: | ||
"""Creates a cancel scope with the given timeout, and raises an error if | ||
it is actually cancelled. | ||
|
||
|
@@ -138,6 +159,9 @@ def fail_after(seconds: float) -> AbstractContextManager[trio.CancelScope]: | |
it's caught and discarded. When it reaches :func:`fail_after`, then it's | ||
caught and :exc:`TooSlowError` is raised in its place. | ||
|
||
The deadline of the cancel scope is calculated at creation time, not upon | ||
entering the context manager. | ||
|
||
Args: | ||
seconds (float): The timeout. | ||
|
||
|
@@ -147,6 +171,30 @@ def fail_after(seconds: float) -> AbstractContextManager[trio.CancelScope]: | |
ValueError: if *seconds* is less than zero or NaN. | ||
|
||
""" | ||
if seconds < 0: | ||
raise ValueError("timeout must be non-negative") | ||
return fail_at(trio.current_time() + seconds) | ||
|
||
def __init__(self, seconds: float, *, timeout_from_enter: bool = False): | ||
self._seconds = seconds | ||
self._timeout_from_enter = timeout_from_enter | ||
self._creation_time = trio.current_time() | ||
self._scope: trio.CancelScope | None = None | ||
|
||
def __enter__(self) -> trio.CancelScope: | ||
self._scope = trio.move_on_after( | ||
self._seconds, | ||
timeout_from_enter=self._timeout_from_enter, | ||
_creation_time=self._creation_time, | ||
) | ||
return self._scope.__enter__() | ||
|
||
def __exit__( | ||
self, | ||
exc_type: type[BaseException] | None, | ||
exc_value: BaseException | None, | ||
traceback: TracebackType | None, | ||
) -> bool | None: | ||
if self._scope is None: # pragma: no cover | ||
raise RuntimeError("__exit__ called before __enter__") | ||
result = self._scope.__exit__(exc_type, exc_value, traceback) | ||
if self._scope.cancelled_caught: | ||
raise TooSlowError | ||
return result |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can deal with that by appending an empty RST comment
\n..\n
to the newsfragment; it might even be worth upstreaming that into towncrier (for cases where the last nonempty line is indented).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
turns out an issue was recently opened for it: twisted/towncrier#614