typer icon indicating copy to clipboard operation
typer copied to clipboard

[QUESTION] How to use with async?

Open csheppard opened this issue 4 years ago • 75 comments

First check

  • [x] I used the GitHub search to find a similar issue and didn't find it.
  • [x] I searched the Typer documentation, with the integrated search.
  • [x] I already searched in Google "How to X in Typer" and didn't find any information.
  • [x] I already searched in Google "How to X in Click" and didn't find any information.

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?

csheppard avatar Apr 12 '20 22:04 csheppard

Just found https://github.com/pallets/click/issues/85#issuecomment-503464628 which may help me

csheppard avatar Apr 12 '20 22:04 csheppard

It'd be nice if we could use Typer with the async-click fork.

thedrow avatar Jul 05 '20 15:07 thedrow

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 import click with import asyncclick as click works like a charm but it means maintenance of the fork yourself. If @smurfix could keep his async fork maintained and up to date with click upstream, and if typer was based on asyncclick then we would really have something great here.

mreschke avatar Sep 02 '20 17:09 mreschke

I will update asyncclick to the latest click release as soon as anyio 2.0 is ready.

smurfix avatar Sep 03 '20 07:09 smurfix

Thanks @smurfix. Once you have asyncclick updated, if @tiangolo doesn't have a nice async typer by then, perhaps ill make a good typer-async fork of typer and use asyncclick and add to pypi for us all to use.

mreschke avatar Sep 03 '20 15:09 mreschke

Thanks @smurfix. Once you have asyncclick updated, if @tiangolo doesn't have a nice async typer by then, perhaps ill make a good typer-async fork of typer and use asyncclick and add to pypi for us all to use.

@mreschke I made a pull request to this repo that gets you most of the way to async. https://github.com/tiangolo/typer/pull/128

jessekrubin avatar Sep 08 '20 19:09 jessekrubin

@mreschke I've updated @jessekrubin's PR to remove the conflicts with master, in case you find it useful.

amfarrell avatar Dec 27 '20 20:12 amfarrell

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.

mreschke avatar Jan 05 '21 20:01 mreschke

We all need this

elpapi42 avatar Mar 26 '21 04:03 elpapi42

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.

killswitch-GUI avatar May 04 '21 23:05 killswitch-GUI

Hi :) Anything new on this ?

neimad1985 avatar Jul 27 '21 23:07 neimad1985

Hi :) Anything new on this ?

@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.

jessekrubin avatar Jul 28 '21 20:07 jessekrubin

Thanks for the quick answer @jessekrubin Would it be possible that you share a simple code example on how you do this please ?

neimad1985 avatar Jul 29 '21 13:07 neimad1985

@neimad1985

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)

jessekrubin avatar Jul 29 '21 19:07 jessekrubin

@jessekrubin

Ok thanks, that's exactly what I was thinking. The problem is the duplication of functions main and _main and their arguments. If you have multiple subcommands, which I have, for your program you have more and more duplication. Anyway thanks for answering me.

neimad1985 avatar Jul 29 '21 21:07 neimad1985

@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 avatar Jul 29 '21 21:07 cauebs

@cauebs Thanks, that is actually a nice idea, I will try it !

neimad1985 avatar Jul 29 '21 22:07 neimad1985

@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 avatar Jul 29 '21 23:07 jessekrubin

@jessekrubin Very nice trick. I should have thought about this. Thank you.

neimad1985 avatar Jul 30 '21 22:07 neimad1985

Just found pallets/click#85 (comment) which may help me

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: https://github.com/pallets/click/issues/2033

septatrix avatar Aug 04 '21 18:08 septatrix

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

ryanpeach avatar Sep 24 '21 00:09 ryanpeach

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:

  1. Called directly from the cli
  2. Called as an asynchronous function from another function as a library function

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?

ryanpeach avatar Sep 24 '21 02:09 ryanpeach

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.

ryanpeach avatar Sep 24 '21 18:09 ryanpeach

Unsure why @aogier downvoted. It runs and its integrating well into my repo.

ryanpeach avatar Sep 24 '21 18:09 ryanpeach

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.

aogier avatar Sep 24 '21 18:09 aogier

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.

ryanpeach avatar Sep 24 '21 18:09 ryanpeach

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 asyncio. I couldn't use the feature as it stands in your code, because I use trio instead of asyncio.

My proposal: add an "extra" on the package called anyio, alt-async or something else, that toggles an optional dependency on anyio=^3. Then, in this decorators impl, we check if anyio is available, and if it's not we fallback into asyncio (your current impl). Otherwise it's just a matter of time until someone opens another issue here requesting support for trio or something else.

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! :smile: Cheers.

cauebs avatar Sep 24 '21 19:09 cauebs

@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.

ryanpeach avatar Sep 24 '21 19:09 ryanpeach

You could possibly implement Tiangolo's new little reop Asyncer, it has Aynio support for making functions async. It is very similar to @ryanpeach's implementation of anyio :)

https://github.com/tiangolo/asyncer

Butch78 avatar Jan 10 '22 15:01 Butch78

What is the final solution for this? I'm a bit confused.

wizard-28 avatar Mar 23 '22 12:03 wizard-28