Chris Jerdonek <chris.jerdo...@gmail.com> 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 <rep...@bugs.python.org>
<https://bugs.python.org/issue45390>
_______________________________________
_______________________________________________
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com

Reply via email to