Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A Pythonic way to change how an asyncio task should be cancelled from where cancel() is called

How to change the behavior of a task cancellation from where the task is being cancelled?

What I would dream of:

task = ensure_future(foo())

def foo_done(task)
    try:
        return task.get_result()
    except CancelError as e:
        when, why = e.args
        if when == "now"
            # do something...
        elif when == "asap":
            # do something else...
        else:
            # do default
        print(f"task cancelled because {why}")

task.add_done_callback(foo_done)

[...]

task.cancel("now", "This is an order!")

I could attach an object to the task before calling task.cancel(), and inspect it later.

task = ensure_future(foo())

def foo_done(task)
    try:
        return task.get_result()
    except CancelError as e:
        when = getattr(task, "_when", "")
        why = getattr(task, "_why", "")
        if when == "now"
            # do something...
        elif when == "asap":
            # do something else...
        else:
            # do default
        print(f"task cancelled because {why}")

task.add_done_callback(foo_done)

[...]
task._when = "now"
task._why = "This is an order!"
task.cancel()

But it looks clunky in some situation, when I want to capture the CancelError within the task being process for example:

async def foo():
   # some stuff
   try:
       # some other stuff
   except CancellError as e:
       # here I have easily access to the error, but not the task :(
   [...]

I'm looking for a more Pythonic way to do it.

like image 898
René paul Debroize Avatar asked Dec 06 '25 05:12

René paul Debroize


1 Answers

Your solution to decorate the Task with data relevant for your exception is in fact a good one. Within the task you can access the task being processed with asyncio.Task.current_task().

You could also achieve the syntax you dream of using the following decorator (untested):

def propagate_when(fn):
    async def wrapped(*args, **kwds):
        try:
            return await fn(*args, **kwds)
        except CancelledError as e:
            e.when = getattr(asyncio.Task.current_task(), '_when', None)
            raise
    return wrapped

Decorating a coroutine with @propagate_when allows the code in foo_done to access e.when when handling CancelledError. The downside is that e.when will not be available inside the task - there you'd still have to use current_task(). Because of that inconsistency I would recommend sticking to reading from the task object.

Several related recommendations:

  • Put the cancellation code in a utility function that stores the object you pass it and then calls task.cancel(). This thin layer of encapsulation should remove the "clunky" feel from the current code.

  • Use prefixed attribute names - short and generic ones like _when can cause a clash in a future release. (I understand it was just an example, but unprefixed names are always in danger of clashing.)

  • Decorate the task with a single object, putting actual data in its attributes. It makes the retrieval simpler and cleaner, and the gives you the option of implementing methods on the stored object.

like image 117
user4815162342 Avatar answered Dec 08 '25 18:12

user4815162342



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!