Chris Jerdonek <[email protected]> added the comment:
Here's a simplification of Marco's snippet to focus the discussion.
import asyncio
async def job():
# raise RuntimeError('error!')
await asyncio.sleep(5)
async def main():
task = asyncio.create_task(job())
await asyncio.sleep(1)
task.cancel('cancel job')
await task
if __name__=="__main__":
asyncio.run(main())
----
Running this pre-Python 3.9 gives something like this--
Traceback (most recent call last):
File "test.py", line 15, in <module>
asyncio.run(main())
File "/.../python3.7/asyncio/runners.py", line 43, in run
return loop.run_until_complete(main)
File "/.../python3.7/asyncio/base_events.py", line 579, in run_until_complete
return future.result()
concurrent.futures._base.CancelledError
----
Running this with Python 3.9+ gives something like the following. The
difference is that the traceback now starts at the sleep() call:
Traceback (most recent call last):
File "/.../test.py", line 6, in job
await asyncio.sleep(5)
File "/.../python3.9/asyncio/tasks.py", line 654, in sleep
return await future
asyncio.exceptions.CancelledError: cancel job
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/.../test.py", line 12, in main
await task
asyncio.exceptions.CancelledError
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/.../test.py", line 15, in <module>
asyncio.run(main())
File "/.../python3.9/asyncio/runners.py", line 44, in run
return loop.run_until_complete(main)
File "/.../python3.9/asyncio/base_events.py", line 642, in run_until_complete
return future.result()
asyncio.exceptions.CancelledError
----
Uncommenting the RuntimeError turns it into this--
Traceback (most recent call last):
File "/.../test.py", line 15, in <module>
asyncio.run(main())
File "/.../python3.9/asyncio/runners.py", line 44, in run
return loop.run_until_complete(main)
File "/.../python3.9/asyncio/base_events.py", line 642, in run_until_complete
return future.result()
File "/.../test.py", line 12, in main
await task
File "/.../test.py", line 5, in job
raise RuntimeError('error!')
RuntimeError: error!
----
I agree it would be a lot nicer if the original CancelledError('cancel job')
could bubble up just like the RuntimeError does, instead of creating a new
CancelledError at each await and chaining it to the previous CancelledError.
asyncio's creation of a new CancelledError at each stage predates the PR that
added the chaining, so this could be viewed as an evolution of the change that
added the chaining.
I haven't checked to be sure, but the difference in behavior between
CancelledError and other exceptions might be explained by the following lines:
https://github.com/python/cpython/blob/3d1ca867ed0e3ae343166806f8ddd9739e568ab4/Lib/asyncio/tasks.py#L242-L250
You can see that for exceptions other than CancelledError, the exception is
propagated by calling super().set_exception(exc), whereas with CancelledError,
it is propagated by calling super().cancel() again.
Maybe this would even be an easy change to make. Instead of asyncio creating a
new CancelledError and chaining it to the previous, asyncio can just raise the
existing one. For the pure Python implementation at least, it may be as simple
as making a change here, inside _make_cancelled_error():
https://github.com/python/cpython/blob/3d1ca867ed0e3ae343166806f8ddd9739e568ab4/Lib/asyncio/futures.py#L135-L142
----------
_______________________________________
Python tracker <[email protected]>
<https://bugs.python.org/issue45390>
_______________________________________
_______________________________________________
Python-bugs-list mailing list
Unsubscribe:
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com