Skip to content

Commit 2fb1ec0

Browse files
author
8r1ef
committed
Restore timeout cancellation state
1 parent 4d233dd commit 2fb1ec0

2 files changed

Lines changed: 70 additions & 1 deletion

File tree

asgiref/timeout.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88

99
import asyncio
10+
import sys
1011
import warnings
1112
from types import TracebackType
1213
from typing import Any # noqa
@@ -43,6 +44,7 @@ def __init__(
4344
)
4445
self._loop = loop
4546
self._task = None # type: Optional[asyncio.Task[Any]]
47+
self._cancelling = 0
4648
self._cancelled = False
4749
self._cancel_handler = None # type: Optional[asyncio.Handle]
4850
self._cancel_at = None # type: Optional[float]
@@ -92,6 +94,8 @@ def _do_enter(self) -> "timeout":
9294
raise RuntimeError(
9395
"Timeout context manager should be used " "inside a task"
9496
)
97+
if sys.version_info >= (3, 11):
98+
self._cancelling = self._task.cancelling()
9599

96100
if self._timeout <= 0:
97101
self._loop.call_soon(self._cancel_task)
@@ -102,7 +106,11 @@ def _do_enter(self) -> "timeout":
102106
return self
103107

104108
def _do_exit(self, exc_type: Type[BaseException]) -> None:
105-
if exc_type is asyncio.CancelledError and self._cancelled:
109+
should_raise_timeout = self._cancelled
110+
if self._cancelled and sys.version_info >= (3, 11):
111+
assert self._task is not None
112+
should_raise_timeout = self._task.uncancel() <= self._cancelling
113+
if exc_type is asyncio.CancelledError and should_raise_timeout:
106114
self._cancel_handler = None
107115
self._task = None
108116
raise asyncio.TimeoutError

tests/test_timeout.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import asyncio
2+
import sys
3+
4+
import pytest
5+
6+
from asgiref.timeout import timeout
7+
8+
9+
@pytest.mark.asyncio
10+
async def test_timeout_restores_cancellation_count() -> None:
11+
if sys.version_info < (3, 11):
12+
pytest.skip("Task cancellation counts were added in Python 3.11")
13+
14+
task = asyncio.current_task()
15+
assert task is not None
16+
cancelling = task.cancelling()
17+
18+
with pytest.raises(asyncio.TimeoutError):
19+
async with timeout(0):
20+
await asyncio.sleep(0)
21+
22+
assert task.cancelling() == cancelling
23+
24+
25+
@pytest.mark.asyncio
26+
async def test_timeout_restores_count_when_cancellation_is_suppressed() -> None:
27+
if sys.version_info < (3, 11):
28+
pytest.skip("Task cancellation counts were added in Python 3.11")
29+
30+
task = asyncio.current_task()
31+
assert task is not None
32+
cancelling = task.cancelling()
33+
34+
async with timeout(0):
35+
try:
36+
await asyncio.sleep(0)
37+
except asyncio.CancelledError:
38+
pass
39+
40+
assert task.cancelling() == cancelling
41+
42+
43+
@pytest.mark.asyncio
44+
async def test_timeout_preserves_external_cancellation() -> None:
45+
if sys.version_info < (3, 11):
46+
pytest.skip("Task cancellation counts were added in Python 3.11")
47+
48+
task = asyncio.current_task()
49+
assert task is not None
50+
cancelling = task.cancelling()
51+
52+
try:
53+
with pytest.raises(asyncio.CancelledError):
54+
async with timeout(0):
55+
asyncio.get_running_loop().call_soon(task.cancel)
56+
await asyncio.sleep(0)
57+
58+
assert task.cancelling() == cancelling + 1
59+
finally:
60+
while task.cancelling() > cancelling:
61+
task.uncancel()

0 commit comments

Comments
 (0)