-
-
Notifications
You must be signed in to change notification settings - Fork 671
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
[QUESTION] How to use with async? #88
Comments
Just found pallets/click#85 (comment) which may help me |
It'd be nice if we could use Typer with the async-click fork. |
Seeing as how FastAPI is an async framework, having an async CLI seems logical. The main reason being sharing code from the CLI and Web entry points. You can of course use the asgiref.sync.async_to_sync converter helpers to call existing async methods from the CLI but there are complications here and it makes your cli code clunky. I replaced typer with smurfix/trio-click (which is asyncclick on Pypi) and it works great, but of course this is just async click, not the cool typer implementation. Forking typer and replaceing all |
I will update asyncclick to the latest |
@mreschke I've updated @jessekrubin's PR to remove the conflicts with master, in case you find it useful. |
Thanks guys. Ill need some time to pull it all in and prototype this instead of asyncclick. If this all works out what is the probability of merging this request and making it a part of this official typer repo. Optional async would be perfect. I really hate to fork permanently. |
We all need this |
I agree with @mreschke, we tightly couple all of our code and actually use Type CLI to call our uvicorn/guinicorn using various "management" commands. Ran into this once we wanted to use some of the async calls we have. |
Hi :) |
@neimad1985 I don't think async is PR-ed in yet, but I use async with typer all the time by just running the async processes from within my sync functions once the parsing is done. It works for most basic things. |
Thanks for the quick answer @jessekrubin |
from asyncio import run as aiorun
import typer
async def _main(name: str):
typer.echo(f"Hello {name}")
def main(name: str = typer.Argument("Wade Wilson")):
aiorun(_main(name=name))
if __name__ == "__main__":
typer.run(main) |
Ok thanks, that's exactly what I was thinking. |
@neimad1985 A decorator might help you: from functools import wraps
import anyio
def run_async(func):
@wraps(func)
def wrapper(*args, **kwargs):
async def coro_wrapper():
return await func(*args, **kwargs)
return anyio.run(coro_wrapper)
return wrapper
@run_async
async def main(name: str = typer.Argument("Wade Wilson")):
typer.echo(f"Hello {name}") You can even have async completions: import click
def async_completion(func):
func = run_async(func)
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except (click.exceptions.Abort, click.exceptions.Exit):
return []
return wrapper
async def list_users() -> List[str]:
...
@run_async
async def main(
name: str = typer.Argument("Wade Wilson", autocompletion=async_completion(list_users))
):
typer.echo(f"Hello {name}") |
@cauebs |
@neimad1985 Easier, but less fancy than the decorator solution is to just nest your async func: from asyncio import run as aiorun
import typer
def main(name: str = typer.Argument("Wade Wilson")):
async def _main():
typer.echo(f"Hello {name}")
aiorun(_main())
if __name__ == "__main__":
typer.run(main) |
@jessekrubin |
As that issue was closed a few years ago and is now locked I decided to open a new one containing a bit more information and addressing some comments in the previous issue. You can look at it yourself, leave some feedback (preferably typer agnostic) and upvote it to show interest in this feature: pallets/click#2033 |
I think just adding those decorators to the library and having @app.command() auto detect if the function it's decorating is async or not and just pick the appropriate decoration. Not hard at all to implement. Thanks everyone for the suggestions |
Actually the decorator @cauebs wrote doesn't make sense to me (maybe I just misunderstand click and anyio). The point is to support running the asynchronous function in two modes:
If you just decorate the function with a function that makes it synchronous, you've ruined it. But also we need argument information preserved. So I propose the following: # file: root/__init__.py
from functools import wraps
from asyncio import sleep, run
import typer
# This is a standard decorator that takes arguments
# the same way app.command does but with
# app as the first parameter
def async_command(app, *args, **kwargs):
def decorator(async_func):
# Now we make a function that turns the async
# function into a synchronous function.
# By wrapping async_func we preserve the
# meta characteristics typer needs to create
# a good interface, such as the description and
# argument type hints
@wraps(async_func)
def sync_func(*_args, **_kwargs):
return run(async_func(*_args, **_kwargs))
# Now use app.command as normal to register the
# synchronous function
app.command(*args, **kwargs)(sync_func)
# We return the async function unmodifed,
# so its library functionality is preserved
return async_func
return decorator
# as a method injection, app will be replaced as self
# making the syntax exactly the same as it used to be.
# put this all in __init__.py and it will be injected into
# the library project wide
typer.Typer.async_command = async_command # file: root/some/code.py
import typer
from asyncio import sleep
app=typer.Typer()
# The command we want to be accessible by both
# the async library and the CLI
@app.async_command()
async def foo(bar: str = typer.Argument(..., help="foo bar")):
"""Foo bar"""
return await sleep(5)
if __name__=="__main__":
app() This is written in such a way it could be literally written as a PR and put as a method into typer.main.Typer. Thoughts? |
Tested the code I posted above and it works. You could probably just add it as a method to typer.Typer. I'll make a pr. |
Unsure why @aogier downvoted. It runs and its integrating well into my repo. |
i'm not able to be enthusiast about your attitude in this PR, this is where my emoji stem from. Given the irrelevant value your little boilerplate adds upstream this will neither add nor remove value to this library in my (irrelevant) opinion. |
Noted but I don't think I've been impolite in this thread... And I believe I've added a relevant feature (the ability to wrap async functions as cli commands). Correct me if I'm wrong. |
Okay, we're all trying to help here. Let's not take anything personally. @ryanpeach Your solution is in essence very similar to mine, but yours is one step ahead. One thing you missed and that I will insist on is that we should tackle not only commands but also things such as autocompletion functions (and others I might be missing). And another matter we should discuss before jumping to a PR (and here I kind of understand the discomfort you might have caused to @aogier) is supporting different async runtimes other than My proposal: add an "extra" on the package called On a final note, I usually wait for a maintainer to weigh in, so as not to waste any time on an unwelcome solution. I salute your initiative, but give people time to evaluate your proposal! 😄 Cheers. |
@cauebs I suppose jumping to a PR is a bit of a jump, I wasn't really aware the project was big enough to have a lot of maintainers or QA. We are planning on using the code I just wrote on a rather big company cli, so the feature is required for us, and is complete for our purposes. I just demo'd how to add basic anyio support, and I'll help out as best I can. I'm sure the PR can provide good scaffolding for a future solution. |
The class UTyper(typer.Typer):
# https://github.com/tiangolo/typer/issues/88
@override
def command(self, *args, **kwargs):
decorator = super().command(*args, **kwargs)
def add_runner(f):
if inspect.iscoroutinefunction(f):
@wraps(f)
def runner(*args, **kwargs):
return asyncio.run(f(*args,**kwargs))
decorator(runner)
else:
decorator(f)
return f
return add_runner
app = UTyper()
@app.command()
async def inner():
await asyncio.sleep(1)
print("inner")
@app.command()
async def outer():
await inner()
if __name__ == "__main__":
app() |
I agree, in the pr i made i only wrap the callback registered to typer. Not the function itself. I'll see if there was any progress on getting it merged. I'm just stuck with the pr because the mypy configurations is to strict and I can't make it happy for all python versions 😞 |
I am working on a cli part of an async app with async DB connection, so needed the async cli as well, thanks for all contributors to the AsyncTyper / UTyper workaround, here is my working version: import asyncio
import inspect
from functools import wraps, partial
from typer import Typer
class AsyncTyper(Typer):
@staticmethod
def maybe_run_async(decorator, f):
if inspect.iscoroutinefunction(f):
@wraps(f)
def runner(*args, **kwargs):
return asyncio.run(f(*args, **kwargs))
decorator(runner)
else:
decorator(f)
return f
def callback(self, *args, **kwargs):
decorator = super().callback(*args, **kwargs)
return partial(self.maybe_run_async, decorator)
def command(self, *args, **kwargs):
decorator = super().command(*args, **kwargs)
return partial(self.maybe_run_async, decorator) I needed the callback to be async as well to handle database connection init instead of repeating it |
Is there any good reason that async commands are not part of the library? Or is it just a missing feature? |
Its a missing feature and i have a PR open but there is a funny collision
between mypy and the python versions.
#633
I also didn't get any response on it.
…On Sun, 3 Sept 2023, 10:31 Joscha Götzer, ***@***.***> wrote:
Is there any good reason that async commands are *not* part of the
library? Or is it just a missing feature?
—
Reply to this email directly, view it on GitHub
<#88 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABNXZFXA6D4L7DSO5IVYLZ3XYQ54RANCNFSM4MGSXYGA>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
I can't see any appeal for having it at the moment other than it being extra code to be maintained to be honest. What's the problem with just calling |
I really like typer that a function and command are the same thing. This
allows me to reuse a command to chain bigger commands together. With
argparse i have to manually write a mapping for each typed function i want
to expose. Now we have to manually wrap each async function to become sync
or subclass typer for each project we use typer in.
While it would be better that this is handled by the typer library.
Especially since asyncio is in the standard library.
For example i could have an install dependency command which then reused
the install dependency command for each dependency. With asyncio I could
make this concurrently.
|
Either change the project motto to "Another random CLI lib that works a little bit like FastAPI" or introduce async and keep the "little brother of FastAPI" headline. FastAPI without async? Huh? Jokes aside, is this really such a large maintenance effort? |
Why not use |
Of course none of those are fair comparisons. It's still FastAPI's sibling because it follows the same design philosophy with types and clarity. Not to mention that async support is much more needed on a web framework, which allows it to handle requests concurrently. For a CLI, it's not the case that you really need concurrency in most scenarios. So, in the end, the appeal to support async on Typer is purely from a compatibility perspective. For example, you might have a FastAPI project and your code will have many async parts, it can be handy to be able to call those directly from your Typer CLI. In any case, it was just a devil's advocate positioning, no need to get emotional over it :) I'm assuming these are probably the reasons why the support for async hasn't been done yet, and of course if it's actually denied everyone can just fork the project and add it too, then use as a dependency, so you don't have to repeat anything. |
Nobody would be complaining about missing async support if was just for compatibility (similarity actually) with FastAPI. There's a increasingly common case of calling async functions, IMO the main example would be remote requests with httpx. |
I didn't mean compatibility with FastAPI. I meant compatibility with async projects' codebase in general. If you're making multiple remote requests with HTTPX, you could organize your code to, for example, chain Again, this is just some basic questioning which I assume the package maintainers will think about and are probably the reason why thins hasn't been fixed yet. |
Just because you use async only for web doesn't mean everyone else is. There are many async-only libraries that have little to do with web. |
@rafalkrupinski, that person has nothing to do with this project he is just there to waste everyone's time, because he only uses async when using "web framework" and nothing else. |
Hmm you seem to lack context or interpretation. I wasn't even responding to just you, but also to someone else who was comparing Typer with FastAPI, hence my response was focused on the comparison between the two. But interesting that you are able to instantly infer my whole dev experience from this brief conversation. This issue has been open for 3 years with no traction forward so I'm just trying to figure out why that is the case :) since I don't think there's been any official positioning about it. I'm pretty sure my questions won't change the maintainers' mind if they are leaning towards incorporating this into the package, when they get time to come here it should actually help if it doesn't get derailed by this kind of comment. Also, I've even helped formulate a solution for it that could be incorporated in the official implementation here... In any case, just to address your "answer", it doesn't matter if there are other async-only libraries and/or if they have to do with web. The point is that adding async capabilities to Typer means there should be an appeal for it to handle concurrency. This means now Typer will have to deal with the async loop in some form. If this hasn't been added yet, maybe the maintainer's philosophy is that this isn't the case and that concurrency should be handled by the user. |
Hello all! Thanks for all the discussion and workarounds. I want to add support for async in Typer, without making it required or default, using AnyIO as an optional dependency. In FastAPI/Starlette, AnyIO is required, as everything is just async underneath. But in Typer I don't want to force people to use AnyIO when they don't really need it. That makes the whole thing a bit more complex as I need to add all the logic to conditionally use or not AnyIO and do async or not. This is one of the top priorities for Typer, along with a code reference (API reference). I'm finishing some things in FastAPI, I have some things in SQLModel too, and then I'll continue with Typer stuff. |
AsyncTyper for anyio (Still with typing problems) import inspect
from functools import partial, wraps
import anyio
import asyncer
import typer
from typer import Typer
class AsyncTyper(Typer):
@staticmethod
def maybe_run_async(decorator, f):
if inspect.iscoroutinefunction(f):
@wraps(f)
def runner(*args, **kwargs):
return asyncer.runnify(f)(*args, **kwargs)
decorator(runner)
else:
decorator(f)
return f
def callback(self, *args, **kwargs):
decorator = super().callback(*args, **kwargs)
return partial(self.maybe_run_async, decorator)
def command(self, *args, **kwargs):
decorator = super().command(*args, **kwargs)
return partial(self.maybe_run_async, decorator)
app = AsyncTyper()
@app.command()
async def async_hello(name: str, last_name: str = "") -> None:
await anyio.sleep(1)
typer.echo(f"Hello World {name} {last_name}")
@app.command()
def hello() -> None:
print("Hello World")
if __name__ == "__main__":
app() |
I ran into a related problem when running async unit tests with pytest-asyncio because by the time the test calls Would love your feedback on whether there is a better way to go about this. # cli/commands/generate.py
...
@app.command("profiles")
@embed_event_loop
async def generate_profiles():
"""
Generates profile data using Random User Generator API (https://randomuser.me/) and
adds them to the database.
"""
...
# test/integration/cli/test_generate.py
...
def test_generate_profiles_happy_path(runner: CliRunner):
result = runner.invoke(["generate", "profiles"])
assert result.exception is None
...
@embed_event_loop
async def verify():
# Verify that the profiles were saved correctly to the database.
async with db_service.session() as session:
...
verify()
... where |
typer officially does not support asyncio, there's a thread about it fastapi/typer#88 essentially we are using a decorator based workaround which has been adapted from a solution posted by @gilcu2 on his comment https://github.com/tiangolo/typer/issues/88\#issuecomment-1732469681 note that this requires the use of asyncer which uses a previous version of anyio, i am willing to live with this for now until an official solution is published by typer
Hello @tiangolo, At least for me, by putting AnyIO as a dependency is not a problem. Actually, an async variant of the typer with specialyzed algorythm to run async functions could be enough. In this case a check if AnyIO is importable could be put in the AsyncTyper constructor. Thank you for these libraries you develop. |
@borissmidt's PR is still open. Not sure about 'hostility' |
Any news on the async support? Any way we can help? |
with syncer:
pip install syncer usage:import os
import typer
from faststream.nats import NatsBroker
from loguru import logger
from syncer import sync
app = typer.Typer()
HOST = os.getenv("NATS_HOST", default="nats://localhost:4222")
async def aio_pub(host: str, msg: str, subject: str):
async with NatsBroker(host) as broker:
logger.debug(f"publish message: {msg}")
await broker.publish(msg, subject=subject)
@app.command("pub")
def pub_task(host: str = HOST):
#
# here! converter: async -> sync
#
f_pub = sync(aio_pub)
# sync call:
f_pub(host, "Hi async wrap!", "test")
logger.debug("pub task done")
if __name__ == "__main__":
app() |
any news on this? |
There is a new issue, #950, meant to introduce asyncio support to Typer. Besides that, I made a new type-annotated wrapper around |
First check
Description
I have existing methods/model functions that use async functions with encode/databases to load data but I'm unable to use these within commands without getting errors such as
RuntimeWarning: coroutine 'something' was never awaited
How can I make make my
@app.command()
functions async friendly?The text was updated successfully, but these errors were encountered: