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() pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy