-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Basic async
support
#2033
Comments
I agree with this generally. The ecosystem is in a vastly different space now than it was back then. That said, native support is a bit tricker because click allows to invoke things recursively and it's unclear if the expectation is that async -> sync is happening at any level or toplevel only. The wrapping that is currently recommended effectively makes the async stop early and I'm not sure this is correct to do for a first class version. |
I agree that this is going to be tricky to get right, and would probably require a lot of thought. Personally, if this was implemented, I would expect async to be available at every stage throughout click. Even in the 'assemble sub-commands' stage, when calculating completions, when doing validation, etc. I was using asyncclick for a project before, and in general it worked very well (I esp. liked that it used anyio under the hood to support multiple event loops). The one problem that was tricky to get implemented was that I wanted to dynamically discover sub-commands according to some json files on the filesystem, and I wanted to do all of this async. I kinda got it working, but don't ask me to show you the code... If I did that again, I think I'd just use a thread-pool, 'normal' click, and be done with it. |
Supporting "everything" runs into the same problem as Flask: everything about Click's API is sync, so we'd have to essentially duplicate every object to have async versions of all their methods. (We can't just make everything For example, if type validation can be async, that must mean that a custom While I'm generally fine with the idea of supporting async, I really need to understand what the common use case we should solve for is. What specifically do people want to do that they can't do right now? Not "type conversion" but "I have to do this specific type conversion that absolutely requires async to be useful." |
I think the problem here is not as bad as in Flask. There is only a sync entrypoint to the call chain and a "make async code silently block" behavior is more than possible since nobody expects click to be involved in async call chains. One could then imagine a secondary entrypoint which does not "upgrade" a call to sync or even force a call to be async at all times. I'm kinda curious where the general opinion is in 2021 now about how async is working out in Python. Are we moving into a world where almost every single thing is turning into |
I have to admit, my first thought was "why do you need async in click" because in the end parsing command line args isn't something that feels very async... sure, you might have some edge cases where you do I/O while parsing arguments, but in the end that can be done sync as well... |
So the reason I stumbled upon this issues is that I had to invoke a completely unnecessary async call chain from a click function. But alas, apparently almost everything is turning into async now so I guess this war is lost :P Maybe a utility decorator to wrap a function so it starts an async function and blocks on it is a reasonable thing to ship at this point. |
I can only speak for myself, but if I have an async code-base that needs a command-line interface, being able to use call my existing async functions directly would be great (for quite a few other purposes than 'just' parsing args). Not for very simple cases, but as soon as I use the more advanced stuff that click allows for (like the dynamic sub-command creation, shell-completion hints...), it would certainly make life easier. I'm not arguing that click should support that use-case, because as others on this thread I suspect the effort (and possibly complexity) involved in supporting this comprehensively would be quite high, and therefor not a good trade-off to make. It might make more sense to create a new 'async-from-the-ground-up' cli toolkit, dunno. But, if click supported this, I would definitely find a few good use-cases for it, I'm 100% certain! As for only shipping a few utility methods, decorators: that'd probably be nice, but personally I don't think I'd use those. I'd either use asyncclick, or threads. |
I am not suggesting complete support for async throughout the whole codebase but only for the command functions themselves. This would make click <-> asyncclick similar to the relationship between Flask <-> Quart. Alternatively to make this a bit more clear we could add a new |
Well, currently a basic solution can be something like this: class MyContext(click.Context):
def invoke(self, *args, **kwargs):
r = super().invoke(*args, **kwargs)
if inspect.isawaitable(r):
loop = asyncio.get_event_loop()
return loop.run_until_complete(executor.submit(r))
# or just
# return loop.run_until_complete(r)
# (see later)
else:
return r
click.BaseCommand.context_class = MyContext ...and one can has I'm sure you recognize that this is not a solution when there is already an event loop running. If one uses click in a context in which it can spin the loop (be a loop owner/controller), then it's kind of fine. A little bit of novelty here is that executor thing (not shown in detail here). It has a crucial role of passing coro through a queue to an executor task which works like this:
The point of this dance is to make |
I think this discussion is kinda stranded but I would like to add that it would be great if the solution could make |
The most that we would be adding is having Click automatically start an event loop if a command callback is marked |
that's already what I'm doing but then it conflicts with pytest-asyncio because CLIRunner does weird stuff and the coroutine is never awaited |
I can't reproduce that issue. |
Hi everyone, I don't read all the conversation but if you want to add async support, don't forget that asyncio is not the only coroutine library, I'm thinking about trio. You can also take inspiration from the asyncclick project. |
@lewoudar asyncclick and trio are mentioned multiple times in this thread. |
@makkus yeah, sorry, but I was afraid when I look at his first example dealing with asyncio |
If something were to get in, it would presumably work similar to Flask, where there is a method that handles spawning the event loop for coroutines, and can be overridden to use something besides asyncio. |
Thanks for the clarification @davidism :) |
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:Currently a workaround is to wrap every such function with a decorator like the following:
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:
async
/await
is 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.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.asyncclick
which 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.
The text was updated successfully, but these errors were encountered: