Skip to content

AttributeError: 'Task' object has no attribute 'uncancel'. Did you mean: 'cancel'? when the asyncio loop.set_task_factory factory returns a asyncio.Future-compatible object #95097

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
graingert opened this issue Jul 21, 2022 · 14 comments
Labels
3.11 only security fixes 3.12 only security fixes release-blocker type-bug An unexpected behavior, bug, or error

Comments

@graingert
Copy link
Contributor

graingert commented Jul 21, 2022

Bug report

https://docs.python.org/3.11/library/asyncio-eventloop.html#asyncio.loop.set_task_factory is documented as: "The callable must return a asyncio.Future-compatible object." however a number of apis now assume tasks have an uncancel() method:

import asyncio
import signal
import os


class Task:
    def __init__(self, loop, coro, **kwargs):
        self._task = asyncio.Task(coro, loop=loop, **kwargs)

    def cancel(self, *args, **kwargs):
        return self._task.cancel(*args, **kwargs)

    def add_done_callback(self, *args, **kwargs):
        return self._task.add_done_callback(*args, **kwargs)

    def remove_done_callback(self, *args, **kwargs):
        return self._task.remove_done_callback(*args, **kwargs)

    @property
    def _asyncio_future_blocking(self):
        return self._task._asyncio_future_blocking

    def result(self, *args, **kwargs):
        return self._task.result(*args, **kwargs)

    def done(self, *args, **kwargs):
        return self._task.done(*args, **kwargs)

    def cancelled(self, *args, **kwargs):
        return self._task.cancelled(*args, **kwargs)

    def exception(self, *args, **kwargs):
        return self._task.exception(*args, **kwargs)

    def get_loop(self, *args, **kwargs):
        return self._task.get_loop(*args, **kwargs)


async def amain():
    os.kill(os.getpid(), signal.SIGINT)
    await asyncio.Event().wait()


class MyPolicy(asyncio.DefaultEventLoopPolicy):
    def new_event_loop(self, *args, **kwargs):
        loop = super().new_event_loop(*args, **kwargs)
        loop.set_task_factory(Task)
        return loop

asyncio.set_event_loop_policy(MyPolicy())
try:
    asyncio.run(amain())
except KeyboardInterrupt:
    print("ok!")

results in:

$ python3.10 demo.py
ok!
$ python3.11 demo.py
Traceback (most recent call last):
  File "/usr/lib/python3.11/asyncio/runners.py", line 120, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/asyncio/base_events.py", line 650, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/home/graingert/demo.py", line 24, in result
    return self._task.result(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/graingert/demo.py", line 41, in amain
    await asyncio.Event().wait()
  File "/usr/lib/python3.11/asyncio/locks.py", line 213, in wait
    await fut
asyncio.exceptions.CancelledError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/graingert/demo.py", line 52, in <module>
    asyncio.run(amain())
  File "/usr/lib/python3.11/asyncio/runners.py", line 187, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/asyncio/runners.py", line 122, in run
    if self._interrupt_count > 0 and task.uncancel() == 0:
                                     ^^^^^^^^^^^^^
AttributeError: 'Task' object has no attribute 'uncancel'. Did you mean: 'cancel'?

Your environment

  • CPython versions tested on: 3.10, 3.11.0b4, a6daaf2
  • Operating system and architecture:
@graingert graingert added the type-bug An unexpected behavior, bug, or error label Jul 21, 2022
@graingert graingert changed the title AttributeError: 'Task' object has no attribute 'uncancel'. Did you mean: 'cancel'? when the asyncio loop.set_task_factory factory returns a asyncio.Future-compatible object AttributeError: 'Task' object has no attribute 'uncancel'. Did you mean: 'cancel'? when the asyncio loop.set_task_factory factory returns a asyncio.Future-compatible object Jul 21, 2022
graingert added a commit to graingert/cpython that referenced this issue Jul 21, 2022
@graingert graingert added 3.11 only security fixes 3.12 only security fixes labels Jul 24, 2022
@graingert
Copy link
Contributor Author

@pablogsal @Fidget-Spinner tagging this is a release blocker for 3.11

@Fidget-Spinner
Copy link
Member

Fidget-Spinner commented Jul 24, 2022

A short summary of the issue and why it's a release blocker:

asyncio in 3.11 now calls uncancel on tasks. uncancel is a completely undocumented method which didn't exist in 3.10. This causes any custom task classes which don't define uncancel to be rejected by asyncio in 3.11. This is backwards incompatible behavior.

@kumaraditya303
Copy link
Contributor

This is backwards incompatible behavior.

Strictly, it is not a backwards incompatible change, The docs mentions that "The callable must return a asyncio.Future-compatible object." so if the Task interface changes then you have to adapt your code to allow duck typing to work. See Yury's opinion on a similar issue #84979 (comment). IMO a whatsnew entry would be sufficient here on porting to 3.11.

@pablogsal
Copy link
Member

pablogsal commented Jul 24, 2022

so if the Task interface changes then you have to adapt your code to allow duck typing to work.

Unfortunately, that's not what backwards compatibility is defined. The interface itself changing is backwards incompatible if existing APIs now fail due to extra parameters added. This would be fine if the interface changes to add a new parameter that's defaulted to something and existing interfaces keep doing what they were doing but new ones don't.

If I understand correctly, this makes existing subclasses fail with existing asyncio APIs, which is itself backwards incompatible.

@graingert
Copy link
Contributor Author

graingert commented Jul 24, 2022

Here's the original opinion that influenced asyncio.tasks._set_name:

#8547 (comment) (the link might not work on mobile you have to click "load more" to expand 42 hidden items)

@pablogsal
Copy link
Member

This should be fixed ASAP, because beta5 is tomorrow and we don't really want to delay this one.

@kumaraditya303
Copy link
Contributor

Unfortunately, that's not what backwards compatibility is defined. The interface itself changing is backwards incompatible if existing APIs now fail due to extra parameters added. This would be fine if the interface changes to add a new parameter that's defaulted to something and existing interfaces keep doing what they were doing but new ones don't.

Unfortunately, there are multiple new APIs like taskgroups and the timeout context manager which depend on the new uncancel method to work correctly and if the third party implementation does not provides it will not work.

@kumaraditya303
Copy link
Contributor

The PR #95098 will break taskgroups and timeout context manager on third party task factories as it skips the calling the uncancel method if it does not exists.

@pablogsal
Copy link
Member

Unfortunately, there are multiple new APIs like taskgroups and the timeout context manager which depend on the new uncancel method to work correctly and if the third party implementation does not provides it will not work.

New APIs that don't work without new stuff is ok. Old APIs that now don't work without the new stuff is not ok

@graingert
Copy link
Contributor Author

The PR #95098 will break taskgroups and timeout context manager on third party task factories as it skips the calling the uncancel method if it does not exists.

it doesn't break it - it just allows the CancelledError to propagate, which is the old api

@kumaraditya303
Copy link
Contributor

I'll work on a PR to unblock 3.11 release with minimal changes as Pablo advised #95097 (comment)

@kumaraditya303
Copy link
Contributor

See #95211

@ambv ambv added needs backport to 3.11 only security fixes and removed needs backport to 3.11 only security fixes labels Jul 28, 2022
miss-islington pushed a commit to miss-islington/cpython that referenced this issue Jul 28, 2022
…pythonGH-95211)

Co-authored-by: Thomas Grainger <tagrain@gmail.com>
(cherry picked from commit 54f4884)

Co-authored-by: Kumar Aditya <59607654+kumaraditya303@users.noreply.github.com>
gvanrossum pushed a commit that referenced this issue Jul 28, 2022
Co-authored-by: Thomas Grainger <tagrain@gmail.com>
ambv pushed a commit that referenced this issue Jul 29, 2022
…5211) (GH-95387)

(cherry picked from commit 54f4884)

Co-authored-by: Kumar Aditya <59607654+kumaraditya303@users.noreply.github.com>
Co-authored-by: Thomas Grainger <tagrain@gmail.com>
@ambv
Copy link
Contributor

ambv commented Jul 29, 2022

This is fixed in 3.11 and 3.12. Thanks, Thomas! ✨ 🍰 ✨

@ambv ambv closed this as completed Jul 29, 2022
Repository owner moved this from Todo to Done in Release and Deferred blockers 🚫 Jul 29, 2022
@gvanrossum
Copy link
Member

Thanks, Kumar, too!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.11 only security fixes 3.12 only security fixes release-blocker type-bug An unexpected behavior, bug, or error
Projects
Development

No branches or pull requests

6 participants