diff options
-rw-r--r-- | Doc/library/asyncio-task.rst | 3 | ||||
-rw-r--r-- | Lib/asyncio/tasks.py | 10 | ||||
-rw-r--r-- | Lib/test/test_asyncio/test_tasks.py | 55 | ||||
-rw-r--r-- | Misc/ACKS | 1 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2020-05-13-15-32-13.bpo-40607.uSPFCi.rst | 3 |
5 files changed, 66 insertions, 6 deletions
diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index 42e2b4e..bc8a272 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -453,7 +453,8 @@ Timeouts wrap it in :func:`shield`. The function will wait until the future is actually cancelled, - so the total wait time may exceed the *timeout*. + so the total wait time may exceed the *timeout*. If an exception + happens during cancellation, it is propagated. If the wait is cancelled, the future *aw* is also cancelled. diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 717837d..f5de1a2 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -496,7 +496,15 @@ async def wait_for(fut, timeout, *, loop=None): # after wait_for() returns. # See https://bugs.python.org/issue32751 await _cancel_and_wait(fut, loop=loop) - raise exceptions.TimeoutError() + # In case task cancellation failed with some + # exception, we should re-raise it + # See https://bugs.python.org/issue40607 + try: + fut.result() + except exceptions.CancelledError as exc: + raise exceptions.TimeoutError() from exc + else: + raise exceptions.TimeoutError() finally: timeout_handle.cancel() diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 6eb6b46..0f8d921 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -80,6 +80,12 @@ class CoroLikeObject: return self +# The following value can be used as a very small timeout: +# it passes check "timeout > 0", but has almost +# no effect on the test performance +_EPSILON = 0.0001 + + class BaseTaskTests: Task = None @@ -904,12 +910,53 @@ class BaseTaskTests: inner_task = self.new_task(loop, inner()) - with self.assertRaises(asyncio.TimeoutError): - await asyncio.wait_for(inner_task, timeout=0.1) + await asyncio.wait_for(inner_task, timeout=_EPSILON) - self.assertTrue(task_done) + with self.assertRaises(asyncio.TimeoutError) as cm: + loop.run_until_complete(foo()) - loop.run_until_complete(foo()) + self.assertTrue(task_done) + chained = cm.exception.__context__ + self.assertEqual(type(chained), asyncio.CancelledError) + + def test_wait_for_reraises_exception_during_cancellation(self): + loop = asyncio.new_event_loop() + self.addCleanup(loop.close) + + class FooException(Exception): + pass + + async def foo(): + async def inner(): + try: + await asyncio.sleep(0.2) + finally: + raise FooException + + inner_task = self.new_task(loop, inner()) + + await asyncio.wait_for(inner_task, timeout=_EPSILON) + + with self.assertRaises(FooException): + loop.run_until_complete(foo()) + + def test_wait_for_raises_timeout_error_if_returned_during_cancellation(self): + loop = asyncio.new_event_loop() + self.addCleanup(loop.close) + + async def foo(): + async def inner(): + try: + await asyncio.sleep(0.2) + except asyncio.CancelledError: + return 42 + + inner_task = self.new_task(loop, inner()) + + await asyncio.wait_for(inner_task, timeout=_EPSILON) + + with self.assertRaises(asyncio.TimeoutError): + loop.run_until_complete(foo()) def test_wait_for_self_cancellation(self): loop = asyncio.new_event_loop() @@ -1593,6 +1593,7 @@ J. Sipprell Ngalim Siregar Kragen Sitaker Kaartic Sivaraam +Roman Skurikhin Ville Skyttä Michael Sloan Nick Sloan diff --git a/Misc/NEWS.d/next/Library/2020-05-13-15-32-13.bpo-40607.uSPFCi.rst b/Misc/NEWS.d/next/Library/2020-05-13-15-32-13.bpo-40607.uSPFCi.rst new file mode 100644 index 0000000..8060628 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-05-13-15-32-13.bpo-40607.uSPFCi.rst @@ -0,0 +1,3 @@ +When cancelling a task due to timeout, :meth:`asyncio.wait_for` will now +propagate the exception if an error happens during cancellation. +Patch by Roman Skurikhin. |