[Feature] Basic `async` support
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 17.7k
- Forks
- 2.3k
- Avg merge
- 1d 30m
- Merged PRs (30d)
- 18
Description
This is a resumption of #85 which was closed more than 7 years ago when asyncio was at a completely different stage in the python ecosystem. The goal of this issue is basically to enable users to do the following:
@click.command()
async def cli():
await asyncio.sleep(1)
click.echo("Hello World")
if __name__ == "__main__":
cli()
Currently a workaround is to wrap every such function with a decorator like the following:
def make_sync(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return asyncio.run(func(*args, **kwargs))
return wrapper
@click.command()
@make_sync
async def cli():
...
The unclean aspect of the decorator approach is that it requires additional code which could be redundant as click is able to simply infer via introspection that a function is a coroutine and handle accordingly.
This issue is only about the bare minimum to get such simple commands working. What this issue is not about is async-ifying most of the API like it is done in asyncclick (a fork which also makes e.g. ctx and autocompletion async).
Arguments which have been brought up in the past by @mitsuhiko are that (1) this does not belong in click, (2) one would need to get access to the loop anyhow for complex use cases and (3) that there would inevitably be some people demanding support for twisted, trio etc. I would like to address each of them:
- I see no reason why this does not belong in click.
async/awaitis a first-class language feature in python and should be supported where feasible. Many CLI tools perform I/O, be it networking, file access or spawning subprocesses. All of these things can benefit from coroutines. - This is not really an argument as one can still get the current loop using
asyncio.get_event_loop()/asyncio.get_running_loop()- previous cases where the loop itself may have been passed as an argument are long made redundant. - Those people can still use
asyncclickwhich supports different event loops. However depending on the implementation it could be made possible to allow subclassing and overwriting some methods for increased compatibility.
Another reason why one might not want this might be decreased maintainability. However as the simplest implementations is 3 lines long I am not sure if this counts. Python 3.6 support would add another 2 lines.
On the other hand there are confused users when they get exceptions like sys:1: RuntimeWarning: coroutine 'main' was never awaited due to click lacking async support and users who need to get creative when the above decorator approach does not work anymore (when using some more involved type hints).
Other pallets projects which turned down async support years ago now support it like Flask (since 2.0) and Jinja (since 2.9). They too only implement a minimum to work with it but that is sufficient for most people.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Begin by tracing the invocation path behind @click.command and compare it with the async behavior described in the issue and asyncclick. Confirm what is needed for the shown coroutine command to run and await successfully without the make_sync decorator, while leaving broader async APIs out of scope.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- cli
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100