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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With