From dac58742d134a945388179641886f1a58bd38811 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 2 Mar 2022 14:57:41 +0200 Subject: [PATCH 01/31] bpo-46771: Implement asyncio context managers for handling timeouts --- Lib/asyncio/__init__.py | 2 + Lib/asyncio/timeouts.py | 141 +++++++++++++ Lib/test/test_asyncio/test_timeouts.py | 198 ++++++++++++++++++ .../2022-02-21-11-41-23.bpo-464471.fL06TV.rst | 2 + 4 files changed, 343 insertions(+) create mode 100644 Lib/asyncio/timeouts.py create mode 100644 Lib/test/test_asyncio/test_timeouts.py create mode 100644 Misc/NEWS.d/next/Library/2022-02-21-11-41-23.bpo-464471.fL06TV.rst diff --git a/Lib/asyncio/__init__.py b/Lib/asyncio/__init__.py index db1124cc9bd1ee..fed16ec7c67fac 100644 --- a/Lib/asyncio/__init__.py +++ b/Lib/asyncio/__init__.py @@ -18,6 +18,7 @@ from .subprocess import * from .tasks import * from .taskgroups import * +from .timeouts import * from .threads import * from .transports import * @@ -34,6 +35,7 @@ subprocess.__all__ + tasks.__all__ + threads.__all__ + + timeouts.__all__ + transports.__all__) if sys.platform == 'win32': # pragma: no cover diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py new file mode 100644 index 00000000000000..9648a06d61b317 --- /dev/null +++ b/Lib/asyncio/timeouts.py @@ -0,0 +1,141 @@ +import enum + +from types import TracebackType +from typing import final, Any, Dict, Optional, Type + +from . import events +from . import tasks + + +__all__ = ( + "Timeout", + "timeout", + "timeout_at", +) + + +class _State(enum.Enum): + CREATED = "created" + ENTERED = "active" + EXPIRING = "expiring" + EXPIRED = "expired" + EXITED = "exited" + + +@final +class Timeout: + + def __init__(self, deadline: Optional[float]) -> None: + self._state = _State.CREATED + + self._timeout_handler: Optional[events.TimerHandle] = None + self._task: Optional[tasks.Task[Any]] = None + self._deadline = deadline + + def when(self) -> Optional[float]: + return self._deadline + + def reschedule(self, deadline: Optional[float]) -> None: + assert self._state != _State.CREATED + if self._state != _State.ENTERED: + raise RuntimeError( + f"Cannot change state of {self._state} CancelScope", + ) + + self._deadline = deadline + + if self._timeout_handler is not None: + self._timeout_handler.cancel() + + if deadline is None: + self._timeout_handler = None + else: + loop = events.get_running_loop() + self._timeout_handler = loop.call_at( + deadline, + self._on_timeout, + ) + + def expired(self) -> bool: + """Is timeout expired during execution?""" + return self._state in (_State.EXPIRING, _State.EXPIRED) + + def __repr__(self) -> str: + info = [str(self._state)] + if self._state is _State.ENTERED and self._deadline is not None: + info.append(f"deadline={self._deadline}") + cls_name = self.__class__.__name__ + return f"<{cls_name} at {id(self):#x}, {' '.join(info)}>" + + async def __aenter__(self) -> "Timeout": + self._state = _State.ENTERED + self._task = tasks.current_task() + if self._task is None: + raise RuntimeError("Timeout should be used inside a task") + self.reschedule(self._deadline) + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> Optional[bool]: + assert self._state in (_State.ENTERED, _State.EXPIRING) + + if self._timeout_handler is not None: + self._timeout_handler.cancel() + self._timeout_handler = None + + if self._state is _State.EXPIRING: + self._state = _State.EXPIRED + + if self._task.uncancel() == 0: + # Since there are no outstanding cancel requests, we're + # handling this. + raise TimeoutError + elif self._state is _State.ENTERED: + self._state = _State.EXITED + + return None + + def _on_timeout(self) -> None: + assert self._state is _State.ENTERED + self._task.cancel() + self._state = _State.EXPIRING + # drop the reference early + self._timeout_handler = None + + +def timeout(delay: Optional[float]) -> Timeout: + """timeout context manager. + + Useful in cases when you want to apply timeout logic around block + of code or in cases when asyncio.wait_for is not suitable. For example: + + >>> with timeout(10): # 10 seconds timeout + ... await long_running_task() + + + delay - value in seconds or None to disable timeout logic + """ + loop = events.get_running_loop() + return Timeout(loop.time() + delay if delay is not None else None) + + +def timeout_at(deadline: Optional[float]) -> Timeout: + """Schedule the timeout at absolute time. + + deadline argument points on the time in the same clock system + as loop.time(). + + Please note: it is not POSIX time but a time with + undefined starting base, e.g. the time of the system power on. + + >>> async with timeout_at(loop.time() + 10): + ... async with aiohttp.get('https://github.com') as r: + ... await r.text() + + + """ + return Timeout(deadline) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py new file mode 100644 index 00000000000000..d3f7d8f9721276 --- /dev/null +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -0,0 +1,198 @@ +"""Tests for asyncio/timeouts.py""" + +import unittest +import time + +import asyncio +from asyncio import tasks + + +def tearDownModule(): + asyncio.set_event_loop_policy(None) + + +class BaseTimeoutTests: + Task = None + + def new_task(self, loop, coro, name='TestTask'): + return self.__class__.Task(coro, loop=loop, name=name) + + def _setupAsyncioLoop(self): + assert self._asyncioTestLoop is None, 'asyncio test loop already initialized' + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.set_debug(True) + self._asyncioTestLoop = loop + loop.set_task_factory(self.new_task) + fut = loop.create_future() + self._asyncioCallsTask = loop.create_task(self._asyncioLoopRunner(fut)) + loop.run_until_complete(fut) + + async def test_timeout_basic(self): + with self.assertRaises(TimeoutError): + async with asyncio.timeout(0.01) as cm: + await asyncio.sleep(10) + self.assertTrue(cm.expired()) + + async def test_timeout_at_basic(self): + loop = asyncio.get_running_loop() + + with self.assertRaises(TimeoutError): + deadline = loop.time() + 0.01 + async with asyncio.timeout_at(deadline) as cm: + await asyncio.sleep(10) + self.assertTrue(cm.expired()) + self.assertEqual(deadline, cm.when()) + + async def test_nested_timeouts(self): + cancel = False + with self.assertRaises(TimeoutError): + async with asyncio.timeout(0.01) as cm1: + try: + async with asyncio.timeout(0.01) as cm2: + await asyncio.sleep(10) + except asyncio.CancelledError: + cancel = True + raise + except TimeoutError: + self.fail( + "The only topmost timed out context manager " + "raises TimeoutError" + ) + self.assertTrue(cancel) + self.assertTrue(cm1.expired()) + self.assertTrue(cm2.expired()) + + async def test_waiter_cancelled(self): + with self.assertRaises(TimeoutError): + async with asyncio.timeout(0.01): + with self.assertRaises(asyncio.CancelledError): + await asyncio.sleep(10) + + async def test_timeout_not_called(self): + loop = asyncio.get_running_loop() + t0 = loop.time() + async with asyncio.timeout(10) as cm: + await asyncio.sleep(0.01) + t1 = loop.time() + + self.assertFalse(cm.expired()) + # finised fast. Very busy CI box requires high enough limit, + # that's why 0.01 cannot be used + self.assertLess(t1-t0, 2) + + async def test_timeout_disabled(self): + loop = asyncio.get_running_loop() + t0 = loop.time() + async with asyncio.timeout(None) as cm: + await asyncio.sleep(0.01) + t1 = loop.time() + + self.assertFalse(cm.expired()) + self.assertIsNone(cm.when()) + # finised fast. Very busy CI box requires high enough limit, + # that's why 0.01 cannot be used + self.assertLess(t1-t0, 2) + + async def test_timeout_at_disabled(self): + loop = asyncio.get_running_loop() + t0 = loop.time() + async with asyncio.timeout(None) as cm: + await asyncio.sleep(0.01) + t1 = loop.time() + + self.assertFalse(cm.expired()) + self.assertIsNone(cm.when()) + # finised fast. Very busy CI box requires high enough limit, + # that's why 0.01 cannot be used + self.assertLess(t1-t0, 2) + + async def test_timeout_zero(self): + loop = asyncio.get_running_loop() + t0 = loop.time() + with self.assertRaises(TimeoutError): + async with asyncio.timeout(0) as cm: + await asyncio.sleep(10) + t1 = loop.time() + self.assertTrue(cm.expired()) + # finised fast. Very busy CI box requires high enough limit, + # that's why 0.01 cannot be used + self.assertLess(t1-t0, 2) + + async def test_foreign_exception_passed(self): + with self.assertRaises(KeyError): + async with asyncio.timeout(0.01) as cm: + raise KeyError + self.assertFalse(cm.expired()) + + async def test_foreign_cancel_doesnt_timeout_if_not_expired(self): + with self.assertRaises(asyncio.CancelledError): + async with asyncio.timeout(10) as cm: + raise asyncio.CancelledError + self.assertFalse(cm.expired()) + + async def test_outer_task_is_not_cancelled(self): + + has_timeout = False + + async def outer() -> None: + nonlocal has_timeout + try: + async with asyncio.timeout(0.001): + await asyncio.sleep(1) + except asyncio.TimeoutError: + has_timeout = True + + task = asyncio.create_task(outer()) + await task + assert has_timeout + assert not task.cancelled() + assert task.done() + + async def test_nested_timeouts_concurrent(self): + with self.assertRaises(TimeoutError): + async with asyncio.timeout(0.002): + try: + async with asyncio.timeout(0.003): + # Pretend we crunch some numbers. + time.sleep(0.005) + await asyncio.sleep(1) + except asyncio.TimeoutError: + pass + + async def test_nested_timeouts_loop_busy(self): + """ + After the inner timeout is an expensive operation which should + be stopped by the outer timeout. + + Note: this fails for now. + """ + start = time.perf_counter() + try: + async with asyncio.timeout(0.002): + try: + async with asyncio.timeout(0.001): + # Pretend the loop is busy for a while. + time.sleep(0.010) + await asyncio.sleep(0.001) + except asyncio.TimeoutError: + # This sleep should be interrupted. + await asyncio.sleep(10) + except asyncio.TimeoutError: + pass + took = time.perf_counter() - start + self.assertTrue(took <= 1) + + +@unittest.skipUnless(hasattr(tasks, '_CTask'), + 'requires the C _asyncio module') +class Timeout_CTask_Tests(BaseTimeoutTests, unittest.IsolatedAsyncioTestCase): + Task = getattr(tasks, '_CTask', None) + + +class Timeout_PyTask_Tests(BaseTimeoutTests, unittest.IsolatedAsyncioTestCase): + Task = tasks._PyTask + + +if __name__ == '__main__': + unittest.main() diff --git a/Misc/NEWS.d/next/Library/2022-02-21-11-41-23.bpo-464471.fL06TV.rst b/Misc/NEWS.d/next/Library/2022-02-21-11-41-23.bpo-464471.fL06TV.rst new file mode 100644 index 00000000000000..b8a48d658250f9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-02-21-11-41-23.bpo-464471.fL06TV.rst @@ -0,0 +1,2 @@ +:func:`asyncio.timeout` and :func:`asyncio.timeout_at` context managers +added. Patch by Tin Tvrtković and Andrew Svetlov. From 374ff2ad0cae59e888dc83fbdd38c94e46c736d9 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 2 Mar 2022 15:30:16 +0200 Subject: [PATCH 02/31] Add reschedule tests --- Lib/test/test_asyncio/test_timeouts.py | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index d3f7d8f9721276..a48da20e3e102b 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -80,6 +80,7 @@ async def test_timeout_not_called(self): # finised fast. Very busy CI box requires high enough limit, # that's why 0.01 cannot be used self.assertLess(t1-t0, 2) + self.assertGreater(cm.when(), t1) async def test_timeout_disabled(self): loop = asyncio.get_running_loop() @@ -118,6 +119,7 @@ async def test_timeout_zero(self): # finised fast. Very busy CI box requires high enough limit, # that's why 0.01 cannot be used self.assertLess(t1-t0, 2) + self.assertTrue(t0 <= cm.when() <= t1) async def test_foreign_exception_passed(self): with self.assertRaises(KeyError): @@ -183,6 +185,32 @@ async def test_nested_timeouts_loop_busy(self): took = time.perf_counter() - start self.assertTrue(took <= 1) + async def test_reschedule(self): + loop = asyncio.get_running_loop() + fut = loop.create_future() + deadline1 = loop.time() + 10 + deadline2 = deadline1 + 20 + + async def f(): + async with asyncio.timeout_at(deadline1) as cm: + fut.set_result(cm) + await asyncio.sleep(50) + + task = asyncio.create_task(f()) + cm = await fut + + self.assertEqual(cm.when(), deadline1) + cm.reschedule(deadline2) + self.assertEqual(cm.when(), deadline2) + cm.reschedule(None) + self.assertIsNone(cm.when()) + + task.cancel() + + with self.assertRaises(asyncio.CancelledError): + await task + self.assertFalse(cm.expired()) + @unittest.skipUnless(hasattr(tasks, '_CTask'), 'requires the C _asyncio module') From 70fa59be99ef0c69acec9e6fc16c448e1090cb34 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 2 Mar 2022 15:31:12 +0200 Subject: [PATCH 03/31] Add reschedule tests --- Lib/asyncio/timeouts.py | 2 +- Lib/test/test_asyncio/test_timeouts.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py index 9648a06d61b317..2c38fd97aed245 100644 --- a/Lib/asyncio/timeouts.py +++ b/Lib/asyncio/timeouts.py @@ -14,7 +14,7 @@ ) -class _State(enum.Enum): +class _State(str, enum.Enum): CREATED = "created" ENTERED = "active" EXPIRING = "expiring" diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index a48da20e3e102b..bcd48a489f35c6 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -200,6 +200,7 @@ async def f(): cm = await fut self.assertEqual(cm.when(), deadline1) + breakpoint() cm.reschedule(deadline2) self.assertEqual(cm.when(), deadline2) cm.reschedule(None) From 3ae2af69b8c7acf4c06db29ef5e7a857ed65168f Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 2 Mar 2022 15:31:58 +0200 Subject: [PATCH 04/31] Dro breakpoint --- Lib/test/test_asyncio/test_timeouts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index bcd48a489f35c6..a48da20e3e102b 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -200,7 +200,6 @@ async def f(): cm = await fut self.assertEqual(cm.when(), deadline1) - breakpoint() cm.reschedule(deadline2) self.assertEqual(cm.when(), deadline2) cm.reschedule(None) From 1654ec43f4283db95497e691dcc45c834288e6f0 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 2 Mar 2022 16:02:44 +0200 Subject: [PATCH 05/31] Tune repr --- Lib/asyncio/timeouts.py | 14 +++++++------- Lib/test/test_asyncio/test_timeouts.py | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py index 2c38fd97aed245..d5a3c19ad04a61 100644 --- a/Lib/asyncio/timeouts.py +++ b/Lib/asyncio/timeouts.py @@ -14,12 +14,12 @@ ) -class _State(str, enum.Enum): +class _State(enum.Enum): CREATED = "created" ENTERED = "active" EXPIRING = "expiring" EXPIRED = "expired" - EXITED = "exited" + EXITED = "finished" @final @@ -61,11 +61,11 @@ def expired(self) -> bool: return self._state in (_State.EXPIRING, _State.EXPIRED) def __repr__(self) -> str: - info = [str(self._state)] - if self._state is _State.ENTERED and self._deadline is not None: - info.append(f"deadline={self._deadline}") - cls_name = self.__class__.__name__ - return f"<{cls_name} at {id(self):#x}, {' '.join(info)}>" + info = [''] + if self._state is _State.ENTERED: + info.append(f"deadline={self._deadline:.3f}") + info_str = ' '.join(info) + return f"" async def __aenter__(self) -> "Timeout": self._state = _State.ENTERED diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index a48da20e3e102b..01cfd2e036517b 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -200,6 +200,8 @@ async def f(): cm = await fut self.assertEqual(cm.when(), deadline1) + breakpoint() + repr(cm) cm.reschedule(deadline2) self.assertEqual(cm.when(), deadline2) cm.reschedule(None) From 2c9dbf8d4950f8407dcbd4bc9ebd999e37dd81d4 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 2 Mar 2022 16:54:51 +0200 Subject: [PATCH 06/31] Add tests --- Lib/test/test_asyncio/test_timeouts.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index 01cfd2e036517b..8e557871f436db 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -200,8 +200,6 @@ async def f(): cm = await fut self.assertEqual(cm.when(), deadline1) - breakpoint() - repr(cm) cm.reschedule(deadline2) self.assertEqual(cm.when(), deadline2) cm.reschedule(None) @@ -213,6 +211,16 @@ async def f(): await task self.assertFalse(cm.expired()) + async def test_repr_active(self): + async with asyncio.timeout(10) as cm: + self.assertRegex(repr(cm), r"") + + async def test_repr_expired(self): + with self.assertRaises(TimeoutError): + async with asyncio.timeout(0.01) as cm: + await asyncio.sleep(10) + self.assertEqual(repr(cm), "") + @unittest.skipUnless(hasattr(tasks, '_CTask'), 'requires the C _asyncio module') From baa7400f1b8e0288c66fec756d9d7626cf3cc385 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 2 Mar 2022 17:00:30 +0200 Subject: [PATCH 07/31] More tests --- Lib/test/test_asyncio/test_timeouts.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index 8e557871f436db..61ff2f969304f5 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -221,6 +221,12 @@ async def test_repr_expired(self): await asyncio.sleep(10) self.assertEqual(repr(cm), "") + async def test_repr_finished(self): + async with asyncio.timeout(10) as cm: + await asyncio.sleep(0) + + self.assertEqual(repr(cm), "") + @unittest.skipUnless(hasattr(tasks, '_CTask'), 'requires the C _asyncio module') From 1ca0fb854ba6cf3bae0cd7b9571a42b65c447963 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 2 Mar 2022 20:14:06 +0200 Subject: [PATCH 08/31] Rename --- Lib/asyncio/timeouts.py | 30 +++++++++++++------------- Lib/test/test_asyncio/test_timeouts.py | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py index d5a3c19ad04a61..0bfd4afa9c1a9f 100644 --- a/Lib/asyncio/timeouts.py +++ b/Lib/asyncio/timeouts.py @@ -25,34 +25,34 @@ class _State(enum.Enum): @final class Timeout: - def __init__(self, deadline: Optional[float]) -> None: + def __init__(self, when: Optional[float]) -> None: self._state = _State.CREATED self._timeout_handler: Optional[events.TimerHandle] = None self._task: Optional[tasks.Task[Any]] = None - self._deadline = deadline + self._when = when def when(self) -> Optional[float]: - return self._deadline + return self._when - def reschedule(self, deadline: Optional[float]) -> None: - assert self._state != _State.CREATED - if self._state != _State.ENTERED: + def reschedule(self, when: Optional[float]) -> None: + assert self._state is not _State.CREATED + if self._state is not _State.ENTERED: raise RuntimeError( - f"Cannot change state of {self._state} CancelScope", + f"Cannot change state of {self._state.value} Timeout", ) - self._deadline = deadline + self._when = when if self._timeout_handler is not None: self._timeout_handler.cancel() - if deadline is None: + if when is None: self._timeout_handler = None else: loop = events.get_running_loop() self._timeout_handler = loop.call_at( - deadline, + when, self._on_timeout, ) @@ -63,7 +63,7 @@ def expired(self) -> bool: def __repr__(self) -> str: info = [''] if self._state is _State.ENTERED: - info.append(f"deadline={self._deadline:.3f}") + info.append(f"when={self._when:.3f}") info_str = ' '.join(info) return f"" @@ -72,7 +72,7 @@ async def __aenter__(self) -> "Timeout": self._task = tasks.current_task() if self._task is None: raise RuntimeError("Timeout should be used inside a task") - self.reschedule(self._deadline) + self.reschedule(self._when) return self async def __aexit__( @@ -123,10 +123,10 @@ def timeout(delay: Optional[float]) -> Timeout: return Timeout(loop.time() + delay if delay is not None else None) -def timeout_at(deadline: Optional[float]) -> Timeout: +def timeout_at(when: Optional[float]) -> Timeout: """Schedule the timeout at absolute time. - deadline argument points on the time in the same clock system + when argument points on the time in the same clock system as loop.time(). Please note: it is not POSIX time but a time with @@ -138,4 +138,4 @@ def timeout_at(deadline: Optional[float]) -> Timeout: """ - return Timeout(deadline) + return Timeout(when) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index 61ff2f969304f5..6021cfdee7261b 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -213,7 +213,7 @@ async def f(): async def test_repr_active(self): async with asyncio.timeout(10) as cm: - self.assertRegex(repr(cm), r"") + self.assertRegex(repr(cm), r"") async def test_repr_expired(self): with self.assertRaises(TimeoutError): From 8a81dd1f8225a822a07e3375d8bf21675f883686 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 8 Mar 2022 16:23:46 +0200 Subject: [PATCH 09/31] Update Lib/asyncio/timeouts.py Co-authored-by: Guido van Rossum --- Lib/asyncio/timeouts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py index 0bfd4afa9c1a9f..ec784aa7e51e1a 100644 --- a/Lib/asyncio/timeouts.py +++ b/Lib/asyncio/timeouts.py @@ -29,7 +29,7 @@ def __init__(self, when: Optional[float]) -> None: self._state = _State.CREATED self._timeout_handler: Optional[events.TimerHandle] = None - self._task: Optional[tasks.Task[Any]] = None + self._task: Optional[tasks.Task] = None self._when = when def when(self) -> Optional[float]: From fae235dbdc99852f3b4fc3804728170d59d664bf Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 8 Mar 2022 16:23:58 +0200 Subject: [PATCH 10/31] Update Lib/asyncio/timeouts.py Co-authored-by: Guido van Rossum --- Lib/asyncio/timeouts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py index ec784aa7e51e1a..bf686ef98da039 100644 --- a/Lib/asyncio/timeouts.py +++ b/Lib/asyncio/timeouts.py @@ -1,7 +1,7 @@ import enum from types import TracebackType -from typing import final, Any, Dict, Optional, Type +from typing import final, Optional, Type from . import events from . import tasks From 663b82f64978f67b9d257c6f63c9bd392a1ea3a6 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 8 Mar 2022 16:24:13 +0200 Subject: [PATCH 11/31] Update Lib/asyncio/timeouts.py Co-authored-by: Guido van Rossum --- Lib/asyncio/timeouts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py index bf686ef98da039..4622dd43b1d56c 100644 --- a/Lib/asyncio/timeouts.py +++ b/Lib/asyncio/timeouts.py @@ -126,7 +126,7 @@ def timeout(delay: Optional[float]) -> Timeout: def timeout_at(when: Optional[float]) -> Timeout: """Schedule the timeout at absolute time. - when argument points on the time in the same clock system + Like `timeout() but argument gives absolute time in the same clock system as loop.time(). Please note: it is not POSIX time but a time with From 24d62d11d455fa7f6e05ea32a387633297b03466 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 8 Mar 2022 16:24:22 +0200 Subject: [PATCH 12/31] Update Lib/asyncio/timeouts.py Co-authored-by: Guido van Rossum --- Lib/asyncio/timeouts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py index 4622dd43b1d56c..9a6125526180e4 100644 --- a/Lib/asyncio/timeouts.py +++ b/Lib/asyncio/timeouts.py @@ -113,7 +113,7 @@ def timeout(delay: Optional[float]) -> Timeout: Useful in cases when you want to apply timeout logic around block of code or in cases when asyncio.wait_for is not suitable. For example: - >>> with timeout(10): # 10 seconds timeout + >>> async with timeout(10): # 10 seconds timeout ... await long_running_task() From 6a26d1be43525b0f04d21a8c4068e9e6d56b15a9 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 8 Mar 2022 16:28:58 +0200 Subject: [PATCH 13/31] Add a test --- Lib/asyncio/timeouts.py | 3 ++- Lib/test/test_asyncio/test_timeouts.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py index 9a6125526180e4..9e56579609037f 100644 --- a/Lib/asyncio/timeouts.py +++ b/Lib/asyncio/timeouts.py @@ -63,7 +63,8 @@ def expired(self) -> bool: def __repr__(self) -> str: info = [''] if self._state is _State.ENTERED: - info.append(f"when={self._when:.3f}") + when = round(self._when, 3) if self._when is not None else None + info.append(f"when={when}") info_str = ' '.join(info) return f"" diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index 6021cfdee7261b..ba4502de8eff1e 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -227,6 +227,11 @@ async def test_repr_finished(self): self.assertEqual(repr(cm), "") + async def test_repr_disabled(self): + async with asyncio.timeout(None) as cm: + self.assertEqual(repr(cm), r"") + + @unittest.skipUnless(hasattr(tasks, '_CTask'), 'requires the C _asyncio module') From 930b92b55d492d12857647eb4e89a329a367aeaa Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 8 Mar 2022 16:33:03 +0200 Subject: [PATCH 14/31] Polish docstrings --- Lib/asyncio/timeouts.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py index 9e56579609037f..2bc84f0da933be 100644 --- a/Lib/asyncio/timeouts.py +++ b/Lib/asyncio/timeouts.py @@ -109,12 +109,12 @@ def _on_timeout(self) -> None: def timeout(delay: Optional[float]) -> Timeout: - """timeout context manager. + """Timeout async context manager. Useful in cases when you want to apply timeout logic around block of code or in cases when asyncio.wait_for is not suitable. For example: - >>> async with timeout(10): # 10 seconds timeout + >>> async with asyncio.timeout(10): # 10 seconds timeout ... await long_running_task() @@ -133,9 +133,8 @@ def timeout_at(when: Optional[float]) -> Timeout: Please note: it is not POSIX time but a time with undefined starting base, e.g. the time of the system power on. - >>> async with timeout_at(loop.time() + 10): - ... async with aiohttp.get('https://github.com') as r: - ... await r.text() + >>> async with asyncio.timeout_at(loop.time() + 10): + ... await long_running_task() """ From ac5c53d64c6d5510b6dc22bc751e67fc39747d50 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 8 Mar 2022 16:33:39 +0200 Subject: [PATCH 15/31] Format --- Lib/asyncio/timeouts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py index 2bc84f0da933be..46e5bdf6259411 100644 --- a/Lib/asyncio/timeouts.py +++ b/Lib/asyncio/timeouts.py @@ -127,7 +127,7 @@ def timeout(delay: Optional[float]) -> Timeout: def timeout_at(when: Optional[float]) -> Timeout: """Schedule the timeout at absolute time. - Like `timeout() but argument gives absolute time in the same clock system + Like timeout() but argument gives absolute time in the same clock system as loop.time(). Please note: it is not POSIX time but a time with From f96ad1cd4eed9b987c8bb73377550eb07de54141 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 8 Mar 2022 18:29:52 +0200 Subject: [PATCH 16/31] Tune tests --- Lib/test/test_asyncio/test_timeouts.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index ba4502de8eff1e..ebc61810827dd3 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -155,9 +155,9 @@ async def test_nested_timeouts_concurrent(self): with self.assertRaises(TimeoutError): async with asyncio.timeout(0.002): try: - async with asyncio.timeout(0.003): + async with asyncio.timeout(0.1): # Pretend we crunch some numbers. - time.sleep(0.005) + time.sleep(0.01) await asyncio.sleep(1) except asyncio.TimeoutError: pass @@ -169,21 +169,20 @@ async def test_nested_timeouts_loop_busy(self): Note: this fails for now. """ - start = time.perf_counter() - try: + loop = asyncio.get_running_loop() + t0 = loop.time() + with self.assertRaises(TimeoutError): async with asyncio.timeout(0.002): try: - async with asyncio.timeout(0.001): + async with asyncio.timeout(0.01): # Pretend the loop is busy for a while. - time.sleep(0.010) - await asyncio.sleep(0.001) + time.sleep(0.1) + await asyncio.sleep(0.01) except asyncio.TimeoutError: # This sleep should be interrupted. await asyncio.sleep(10) - except asyncio.TimeoutError: - pass - took = time.perf_counter() - start - self.assertTrue(took <= 1) + t1 = loop.time() + self.assertTrue(t0 <= t1 <= t0 + 1) async def test_reschedule(self): loop = asyncio.get_running_loop() From 94b4b4ceee3e6a8a70de1ee31028de3f83ffd16a Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 8 Mar 2022 23:48:55 +0200 Subject: [PATCH 17/31] Fix comment --- Lib/test/test_asyncio/test_timeouts.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index ebc61810827dd3..0feade411c741f 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -163,12 +163,8 @@ async def test_nested_timeouts_concurrent(self): pass async def test_nested_timeouts_loop_busy(self): - """ - After the inner timeout is an expensive operation which should - be stopped by the outer timeout. - - Note: this fails for now. - """ + # After the inner timeout is an expensive operation which should + # be stopped by the outer timeout. loop = asyncio.get_running_loop() t0 = loop.time() with self.assertRaises(TimeoutError): From 388c6da3147fbee31ad1eb1a2855632d604b4ad3 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 8 Mar 2022 23:50:28 +0200 Subject: [PATCH 18/31] Tune comment --- Lib/test/test_asyncio/test_timeouts.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index 0feade411c741f..8eaa1d58c6ef73 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -77,8 +77,7 @@ async def test_timeout_not_called(self): t1 = loop.time() self.assertFalse(cm.expired()) - # finised fast. Very busy CI box requires high enough limit, - # that's why 0.01 cannot be used + # 2 sec for slow CI boxes self.assertLess(t1-t0, 2) self.assertGreater(cm.when(), t1) @@ -91,8 +90,7 @@ async def test_timeout_disabled(self): self.assertFalse(cm.expired()) self.assertIsNone(cm.when()) - # finised fast. Very busy CI box requires high enough limit, - # that's why 0.01 cannot be used + # 2 sec for slow CI boxes self.assertLess(t1-t0, 2) async def test_timeout_at_disabled(self): @@ -116,8 +114,7 @@ async def test_timeout_zero(self): await asyncio.sleep(10) t1 = loop.time() self.assertTrue(cm.expired()) - # finised fast. Very busy CI box requires high enough limit, - # that's why 0.01 cannot be used + # 2 sec for slow CI boxes self.assertLess(t1-t0, 2) self.assertTrue(t0 <= cm.when() <= t1) From cdc7f881578735b9b7c901dcc9d711fac02a3305 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 8 Mar 2022 23:53:48 +0200 Subject: [PATCH 19/31] Tune docstrings --- Lib/asyncio/timeouts.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py index 46e5bdf6259411..a7218970aa7b35 100644 --- a/Lib/asyncio/timeouts.py +++ b/Lib/asyncio/timeouts.py @@ -119,6 +119,10 @@ def timeout(delay: Optional[float]) -> Timeout: delay - value in seconds or None to disable timeout logic + + long_running_task() is interrupted by raising asyncio.CancelledError, + the top-most affected timeout() context manager converts CancelledError + into TimeoutError. """ loop = events.get_running_loop() return Timeout(loop.time() + delay if delay is not None else None) @@ -137,5 +141,10 @@ def timeout_at(when: Optional[float]) -> Timeout: ... await long_running_task() + when - a deadline when timeout occurs or None to disable timeout logic + + long_running_task() is interrupted by raising asyncio.CancelledError, + the top-most affected timeout() context manager converts CancelledError + into TimeoutError. """ return Timeout(when) From 9949fe4914c136b65a557779ffdd3364ecc7fef6 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 9 Mar 2022 00:12:22 +0200 Subject: [PATCH 20/31] Tune --- Lib/test/test_asyncio/test_timeouts.py | 42 ++++++++------------------ 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index 8eaa1d58c6ef73..548401f459a9ab 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -48,18 +48,10 @@ async def test_nested_timeouts(self): cancel = False with self.assertRaises(TimeoutError): async with asyncio.timeout(0.01) as cm1: - try: + # The only topmost timed out context manager raises TimeoutError + with self.assertRaises(asyncio.CancelledError): async with asyncio.timeout(0.01) as cm2: await asyncio.sleep(10) - except asyncio.CancelledError: - cancel = True - raise - except TimeoutError: - self.fail( - "The only topmost timed out context manager " - "raises TimeoutError" - ) - self.assertTrue(cancel) self.assertTrue(cm1.expired()) self.assertTrue(cm2.expired()) @@ -131,33 +123,24 @@ async def test_foreign_cancel_doesnt_timeout_if_not_expired(self): self.assertFalse(cm.expired()) async def test_outer_task_is_not_cancelled(self): - - has_timeout = False - async def outer() -> None: - nonlocal has_timeout - try: + with self.assertRaises(TimeoutError): async with asyncio.timeout(0.001): await asyncio.sleep(1) - except asyncio.TimeoutError: - has_timeout = True task = asyncio.create_task(outer()) await task - assert has_timeout - assert not task.cancelled() - assert task.done() + self.assertFalse(task.cancelled()) + self.assertTrue(task.done()) async def test_nested_timeouts_concurrent(self): with self.assertRaises(TimeoutError): async with asyncio.timeout(0.002): - try: + with self.assertRaises(TimeoutError): async with asyncio.timeout(0.1): # Pretend we crunch some numbers. time.sleep(0.01) await asyncio.sleep(1) - except asyncio.TimeoutError: - pass async def test_nested_timeouts_loop_busy(self): # After the inner timeout is an expensive operation which should @@ -165,15 +148,14 @@ async def test_nested_timeouts_loop_busy(self): loop = asyncio.get_running_loop() t0 = loop.time() with self.assertRaises(TimeoutError): - async with asyncio.timeout(0.002): - try: - async with asyncio.timeout(0.01): + async with asyncio.timeout(0.1): # (1) + with self.assertRaises(TimeoutError): + async with asyncio.timeout(0.01): # (2) # Pretend the loop is busy for a while. time.sleep(0.1) - await asyncio.sleep(0.01) - except asyncio.TimeoutError: - # This sleep should be interrupted. - await asyncio.sleep(10) + await asyncio.sleep(1) + # TimeoutError was cought by (2) + await asyncio.sleep(10) # This sleep should be interrupted by (1) t1 = loop.time() self.assertTrue(t0 <= t1 <= t0 + 1) From c716856464f3e6bd8b2578333c6d834128806b3d Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 9 Mar 2022 00:14:35 +0200 Subject: [PATCH 21/31] Tune tests --- Lib/test/test_asyncio/test_timeouts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index 548401f459a9ab..9f138c126ea07a 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -119,7 +119,8 @@ async def test_foreign_exception_passed(self): async def test_foreign_cancel_doesnt_timeout_if_not_expired(self): with self.assertRaises(asyncio.CancelledError): async with asyncio.timeout(10) as cm: - raise asyncio.CancelledError + asyncio.current_task().cancel() + await asyncio.sleep(10) self.assertFalse(cm.expired()) async def test_outer_task_is_not_cancelled(self): From b4889a0566a4fc2fa82882513a617c1713bd457f Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 9 Mar 2022 00:15:18 +0200 Subject: [PATCH 22/31] Update Lib/test/test_asyncio/test_timeouts.py Co-authored-by: Guido van Rossum --- Lib/test/test_asyncio/test_timeouts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index 9f138c126ea07a..756948a3fbc322 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -88,7 +88,7 @@ async def test_timeout_disabled(self): async def test_timeout_at_disabled(self): loop = asyncio.get_running_loop() t0 = loop.time() - async with asyncio.timeout(None) as cm: + async with asyncio.timeout_at(None) as cm: await asyncio.sleep(0.01) t1 = loop.time() From b6504e6f3bd08950a50a6440ff2423583ec21877 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 9 Mar 2022 00:16:52 +0200 Subject: [PATCH 23/31] Tune tests --- Lib/test/test_asyncio/test_timeouts.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index 756948a3fbc322..7d3c8aac50aad4 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -48,7 +48,7 @@ async def test_nested_timeouts(self): cancel = False with self.assertRaises(TimeoutError): async with asyncio.timeout(0.01) as cm1: - # The only topmost timed out context manager raises TimeoutError + # Only the topmost context manager should raise TimeoutError with self.assertRaises(asyncio.CancelledError): async with asyncio.timeout(0.01) as cm2: await asyncio.sleep(10) @@ -94,8 +94,7 @@ async def test_timeout_at_disabled(self): self.assertFalse(cm.expired()) self.assertIsNone(cm.when()) - # finised fast. Very busy CI box requires high enough limit, - # that's why 0.01 cannot be used + # 2 sec for slow CI boxes self.assertLess(t1-t0, 2) async def test_timeout_zero(self): From fd2688da3c7817659b76bb7c1c0fa44ee99ef973 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Tue, 8 Mar 2022 20:54:19 -0800 Subject: [PATCH 24/31] Don't clobber foreign exceptions even if timeout is expiring (Includes test.) --- Lib/asyncio/timeouts.py | 3 ++- Lib/test/test_asyncio/test_timeouts.py | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py index a7218970aa7b35..d26eb1c50b06c8 100644 --- a/Lib/asyncio/timeouts.py +++ b/Lib/asyncio/timeouts.py @@ -4,6 +4,7 @@ from typing import final, Optional, Type from . import events +from . import exceptions from . import tasks @@ -91,7 +92,7 @@ async def __aexit__( if self._state is _State.EXPIRING: self._state = _State.EXPIRED - if self._task.uncancel() == 0: + if self._task.uncancel() == 0 and exc_type in (None, exceptions.CancelledError): # Since there are no outstanding cancel requests, we're # handling this. raise TimeoutError diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index 7d3c8aac50aad4..48ed0755f252a7 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -115,6 +115,15 @@ async def test_foreign_exception_passed(self): raise KeyError self.assertFalse(cm.expired()) + async def test_foreign_exception_on_timeout(self): + async def crash(): + try: + await asyncio.sleep(1) + finally: + 1/0 + with self.assertRaises(ZeroDivisionError): + async with asyncio.timeout(0.01): + await crash() async def test_foreign_cancel_doesnt_timeout_if_not_expired(self): with self.assertRaises(asyncio.CancelledError): async with asyncio.timeout(10) as cm: From 2ddda69c18d82cd3b57d974e2e260caaf66d4d15 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Tue, 8 Mar 2022 20:56:52 -0800 Subject: [PATCH 25/31] Add test from discussion Ensures that a finally clause after a cancelled await can still use a timeout. --- Lib/test/test_asyncio/test_timeouts.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index 48ed0755f252a7..b2205575cc27fb 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -214,6 +214,15 @@ async def test_repr_disabled(self): async with asyncio.timeout(None) as cm: self.assertEqual(repr(cm), r"") + async def test_nested_timeout_in_finally(self): + with self.assertRaises(TimeoutError): + async with asyncio.timeout(0.01): + try: + await asyncio.sleep(1) + finally: + with self.assertRaises(TimeoutError): + async with asyncio.timeout(0.01): + await asyncio.sleep(10) @unittest.skipUnless(hasattr(tasks, '_CTask'), From 493545ab0e955275cb031a71e6d3dfe1f66c46dc Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Tue, 8 Mar 2022 21:21:15 -0800 Subject: [PATCH 26/31] Fix indent of added test And add blank line. --- Lib/test/test_asyncio/test_timeouts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index b2205575cc27fb..2cda3e058c9b94 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -115,7 +115,7 @@ async def test_foreign_exception_passed(self): raise KeyError self.assertFalse(cm.expired()) - async def test_foreign_exception_on_timeout(self): + async def test_foreign_exception_on_timeout(self): async def crash(): try: await asyncio.sleep(1) @@ -124,6 +124,7 @@ async def crash(): with self.assertRaises(ZeroDivisionError): async with asyncio.timeout(0.01): await crash() + async def test_foreign_cancel_doesnt_timeout_if_not_expired(self): with self.assertRaises(asyncio.CancelledError): async with asyncio.timeout(10) as cm: From ff36f2ae16db1e17931a2ad83ff8c54099071760 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 9 Mar 2022 13:21:12 +0200 Subject: [PATCH 27/31] Disable slow callback warning --- Lib/test/test_asyncio/test_timeouts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index 2cda3e058c9b94..f148240536943a 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -156,6 +156,8 @@ async def test_nested_timeouts_loop_busy(self): # After the inner timeout is an expensive operation which should # be stopped by the outer timeout. loop = asyncio.get_running_loop() + # Disable a message about long running task + loop.slow_callback_duration = 10 t0 = loop.time() with self.assertRaises(TimeoutError): async with asyncio.timeout(0.1): # (1) From 8790e4950a58f4eb4c70a63a140e65afcb85a8de Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 9 Mar 2022 13:38:03 +0200 Subject: [PATCH 28/31] Reformat --- Lib/asyncio/timeouts.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py index d26eb1c50b06c8..2a5b54b561c213 100644 --- a/Lib/asyncio/timeouts.py +++ b/Lib/asyncio/timeouts.py @@ -92,7 +92,9 @@ async def __aexit__( if self._state is _State.EXPIRING: self._state = _State.EXPIRED - if self._task.uncancel() == 0 and exc_type in (None, exceptions.CancelledError): + if (self._task.uncancel() == 0 + and exc_type in (None, exceptions.CancelledError) + ): # Since there are no outstanding cancel requests, we're # handling this. raise TimeoutError From ac6f8c805d377ff05543cccd666f9a6e7535ef93 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 9 Mar 2022 23:08:47 +0200 Subject: [PATCH 29/31] Increase delay --- Lib/test/test_asyncio/test_timeouts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index f148240536943a..d28ad18cb2cd60 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -136,7 +136,7 @@ async def test_outer_task_is_not_cancelled(self): async def outer() -> None: with self.assertRaises(TimeoutError): async with asyncio.timeout(0.001): - await asyncio.sleep(1) + await asyncio.sleep(10) task = asyncio.create_task(outer()) await task From e8c67cef3741d171ea72742a69fd08ef7c732222 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 10 Mar 2022 01:48:35 +0200 Subject: [PATCH 30/31] Don't raise TimeoutError if the CancelledError was swallowed by inner code --- Lib/asyncio/timeouts.py | 4 +--- Lib/test/test_asyncio/test_timeouts.py | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py index 2a5b54b561c213..a89205348ff24c 100644 --- a/Lib/asyncio/timeouts.py +++ b/Lib/asyncio/timeouts.py @@ -92,9 +92,7 @@ async def __aexit__( if self._state is _State.EXPIRING: self._state = _State.EXPIRED - if (self._task.uncancel() == 0 - and exc_type in (None, exceptions.CancelledError) - ): + if self._task.uncancel() == 0 and exc_type is exceptions.CancelledError: # Since there are no outstanding cancel requests, we're # handling this. raise TimeoutError diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index d28ad18cb2cd60..c7d96f1ef866d5 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -45,21 +45,33 @@ async def test_timeout_at_basic(self): self.assertEqual(deadline, cm.when()) async def test_nested_timeouts(self): - cancel = False + loop = asyncio.get_running_loop() + cancelled = False with self.assertRaises(TimeoutError): - async with asyncio.timeout(0.01) as cm1: + deadline = loop.time() + 0.01 + async with asyncio.timeout_at(deadline) as cm1: # Only the topmost context manager should raise TimeoutError - with self.assertRaises(asyncio.CancelledError): - async with asyncio.timeout(0.01) as cm2: + try: + async with asyncio.timeout_at(deadline) as cm2: await asyncio.sleep(10) + except asyncio.CancelledError: + cancelled = True + raise + self.assertTrue(cancelled) self.assertTrue(cm1.expired()) self.assertTrue(cm2.expired()) async def test_waiter_cancelled(self): + loop = asyncio.get_running_loop() + cancelled = False with self.assertRaises(TimeoutError): async with asyncio.timeout(0.01): - with self.assertRaises(asyncio.CancelledError): + try: await asyncio.sleep(10) + except asyncio.CancelledError: + cancelled = True + raise + self.assertTrue(cancelled) async def test_timeout_not_called(self): loop = asyncio.get_running_loop() From e65d766b6c8d5e3f0295faea4871c7b95986a429 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 10 Mar 2022 05:29:09 +0200 Subject: [PATCH 31/31] Don't duplicate py/c tests, timeout has no C accelerators --- Lib/test/test_asyncio/test_timeouts.py | 27 +------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index c7d96f1ef866d5..ef1ab0acb390d2 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -11,22 +11,7 @@ def tearDownModule(): asyncio.set_event_loop_policy(None) -class BaseTimeoutTests: - Task = None - - def new_task(self, loop, coro, name='TestTask'): - return self.__class__.Task(coro, loop=loop, name=name) - - def _setupAsyncioLoop(self): - assert self._asyncioTestLoop is None, 'asyncio test loop already initialized' - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop.set_debug(True) - self._asyncioTestLoop = loop - loop.set_task_factory(self.new_task) - fut = loop.create_future() - self._asyncioCallsTask = loop.create_task(self._asyncioLoopRunner(fut)) - loop.run_until_complete(fut) +class TimeoutTests(unittest.IsolatedAsyncioTestCase): async def test_timeout_basic(self): with self.assertRaises(TimeoutError): @@ -240,15 +225,5 @@ async def test_nested_timeout_in_finally(self): await asyncio.sleep(10) -@unittest.skipUnless(hasattr(tasks, '_CTask'), - 'requires the C _asyncio module') -class Timeout_CTask_Tests(BaseTimeoutTests, unittest.IsolatedAsyncioTestCase): - Task = getattr(tasks, '_CTask', None) - - -class Timeout_PyTask_Tests(BaseTimeoutTests, unittest.IsolatedAsyncioTestCase): - Task = tasks._PyTask - - if __name__ == '__main__': unittest.main()