tiangolo / typer

Typer, build great CLIs. Easy to code. Based on Python type hints.
https://typer.tiangolo.com/
MIT License
15k stars 640 forks source link

[QUESTION] How to use with async? #88

Open csheppard opened 4 years ago

csheppard commented 4 years ago

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?

csheppard commented 4 years ago

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

thedrow commented 4 years ago

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

mreschke commented 3 years ago

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.

smurfix commented 3 years ago

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

mreschke commented 3 years ago

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.

jessekrubin commented 3 years ago

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

amfarrell commented 3 years ago

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

mreschke commented 3 years ago

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.

elpapi42 commented 3 years ago

We all need this

killswitch-GUI commented 3 years ago

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.

neimad1985 commented 3 years ago

Hi :) Anything new on this ?

jessekrubin commented 3 years ago

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.

neimad1985 commented 3 years ago

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

jessekrubin commented 3 years ago

@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)
neimad1985 commented 3 years ago

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

cauebs commented 3 years ago

@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}")
neimad1985 commented 3 years ago

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

jessekrubin commented 3 years ago

@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)
neimad1985 commented 2 years ago

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

septatrix commented 2 years ago

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

ryanpeach commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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

aogier commented 2 years ago

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.

ryanpeach commented 2 years ago

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.

cauebs commented 2 years ago

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.

ryanpeach commented 2 years ago

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

Butch78 commented 2 years ago

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

wizard-28 commented 2 years ago

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

hozn commented 2 years ago

Also interested in a solution to this.

oramirite commented 2 years ago

Typer mentions being able to stack the FastAPI and Typer decorators on top of each other because they're "meant to work together". Without async support this actually isn't possible though. I hope this PR will be accepted to provide a path towards a solution, even if imperfect, since the stated design goals of Typer are to work with FastAPI which is async in nature.

gghez commented 1 year ago

is this project deprecated? There is no answer to the merge request for 1 year.

pythonwood commented 1 year ago

pr #340 test ok. it is better.

from https://github.com/skeletorXVI/typer/tree/feature/anyio-support

not merge yet

good tutorial

import asyncio

import typer

app = typer.Typer()

@app.command()
async def wait(seconds: int):
    await asyncio.sleep(seconds)
    typer.echo(f"Waited for {seconds} seconds")

if __name__ == "__main__":
    app()
AsgrimS commented 1 year ago

To the people scrolling down to find a solution. In my opinion, what @neimad1985 did is really the simplest and works fine.

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

If you don't want to nest functions you can also just:


import asyncio

import typer

def main(arg_1: str): asyncio.run(my_async_func(arg_1))

async def my_async_func(arg_1: str): await asyncio.sleep(5)

if name == "main": typer.run(main)

gpkc commented 1 year ago

I like @ryanpeach solution. I've made an alternative that doesn't require patching the Typer class directly. You can just inherit from it normally:

from asyncio import run
from functools import wraps

import typer

class AsyncTyper(typer.Typer):
    def async_command(self, *args, **kwargs):
        def decorator(async_func):
            @wraps(async_func)
            def sync_func(*_args, **_kwargs):
                return run(async_func(*_args, **_kwargs))

            self.command(*args, **kwargs)(sync_func)
            return async_func

        return decorator

app = AsyncTyper()

@app.async_command()
async def my_async_command():
    ...

@app.command()
async def my_normal_command():
    ...

if __name__ == "__main__":
    app()
paketb0te commented 1 year ago

@gpkc This is f*cking genius!

@tiangolo What do you think?

rafalkrupinski commented 1 year ago
class AsyncTyper(typer.Typer):
    def async_command(self, _func: Callable = None, *args, **kwargs):
        def decorator(async_func):
            @wraps(async_func)
            def sync_func(*_args, **_kwargs):
                return run(async_func(*_args, **_kwargs))

            self.command(*args, **kwargs)(sync_func)
            return async_func

        if _func:
            return decorator(_func)
        return decorator

Then it also works without parenthesis

@app.async_command
async def my_async_command():
    pass

edit: I just realised that original typer doesn't work without them :D

macintacos commented 1 year ago

The workaround here are very clever! In my codebases I'm trying to use type annotations wherever possible, so I annotated the above to the point where I think everything works fine. If there are any type annotation aficionados here, feel free to rip it apart:

import asyncio
from collections.abc import Callable, Coroutine
from functools import wraps
from typing import Any, ParamSpec, TypeVar

import typer

P = ParamSpec("P")
R = TypeVar("R")

class AsyncTyper(typer.Typer):
    """Asyncronous Typer that derives from Typer.

    Use this when you have an asynchronous command you want to build, otherwise, just use Typer.
    """

    def async_command(  # type: ignore # Because we're being generic in this decorator, 'Any' is fine for the args.
        self,
        *args: Any,
        **kwargs: Any,
    ) -> Callable[
        [Callable[P, Coroutine[Any, Any, R]]],
        Callable[P, Coroutine[Any, Any, R]],
    ]:
        """An async decorator for Typer commands that are asynchronous."""

        def decorator(  # type: ignore # Because we're being generic in this decorator, 'Any' is fine for the args.
            async_func: Callable[P, Coroutine[Any, Any, R]],
        ) -> Callable[P, Coroutine[Any, Any, R]]:
            @wraps(async_func)
            def sync_func(*_args: P.args, **_kwargs: P.kwargs) -> R:
                return asyncio.run(async_func(*_args, **_kwargs))

            # Now use app.command as normal to register the synchronous function
            self.command(*args, **kwargs)(sync_func)

            # We return the async function unmodified, so its library functionality is preserved.
            return async_func

        return decorator
Kludex commented 1 year ago

I don't think it makes sense for typer to have an async_command built in.

Calling asyncio.run in the first line of the command does the job.

rafalkrupinski commented 1 year ago

Calling asyncio.run in the first line of the command does the job.

Of course you actually need to pass another function to asyncio.run, so for every command you need two functions, one of which is boilerplate. So maybe you write a decorator for it... but it's such a common case, maybe someone wrote it already... Wait, why it isn't a part of the library?

NikosAlexandris commented 1 year ago

The workaround here are very clever! In my codebases I'm trying to use type annotations wherever possible, so I annotated the above to the point where I think everything works fine. If there are any type annotation aficionados here, feel free to rip it apart:

import asyncio
from collections.abc import Callable, Coroutine
from functools import wraps
from typing import Any, ParamSpec, TypeVar

import typer

P = ParamSpec("P")
R = TypeVar("R")

class AsyncTyper(typer.Typer):
    """Asyncronous Typer that derives from Typer.

    Use this when you have an asynchronous command you want to build, otherwise, just use Typer.
    """

    def async_command(  # type: ignore # Because we're being generic in this decorator, 'Any' is fine for the args.
        self,
        *args: Any,
        **kwargs: Any,
    ) -> Callable[
        [Callable[P, Coroutine[Any, Any, R]]],
        Callable[P, Coroutine[Any, Any, R]],
    ]:
        """An async decorator for Typer commands that are asynchronous."""

        def decorator(  # type: ignore # Because we're being generic in this decorator, 'Any' is fine for the args.
            async_func: Callable[P, Coroutine[Any, Any, R]],
        ) -> Callable[P, Coroutine[Any, Any, R]]:
            @wraps(async_func)
            def sync_func(*_args: P.args, **_kwargs: P.kwargs) -> R:
                return asyncio.run(async_func(*_args, **_kwargs))

            # Now use app.command as normal to register the synchronous function
            self.command(*args, **kwargs)(sync_func)

            # We return the async function unmodified, so its library functionality is preserved.
            return async_func

        return decorator

Would you have any examples to share?

NikosAlexandris commented 1 year ago

I don't think it makes sense for typer to have an async_command built in.

Calling asyncio.run in the first line of the command does the job.

Can you please point to any examples?

gpkc commented 1 year ago

I don't think it makes sense for typer to have an async_command built in. Calling asyncio.run in the first line of the command does the job.

Can you please point to any examples?

Just call asyncio.run inside the command if you want to do it like that.

from asyncio import run as aiorun

@app.command()
def my_command(email: str) -> None:
    async def do_my_command():
        from <some_module> import awaitable
        await awaitable()

    aiorun(do_my_command())
toppk commented 1 year ago

I think the solutions above by @macintacos @rafalkrupinski @gpkc are close, but they miss the magic of typer, which is no instructions needed. using inspect.iscoroutinefunction() (in python since 3.5) we can just do the code automatically so users can just do @app.command() as normal.

This implementation also allows to switch event loop (example show for uvloop) using the asyncio.Runner available in python >= 3.11.

I think something like this would be good to have in typer as it fits the simple design of typer.

#!/usr/bin/python

import typer
import asyncio
import inspect
import uvloop
import sys
from functools import wraps

class UTyper(typer.Typer):
    def __init__(self, *args, loop_factory=None, **kwargs):
        super().__init__(*args, **kwargs)
        self.loop_factory = loop_factory

    def command(self, *args, **kwargs):
        decorator = super().command(*args, **kwargs)
        def add_runner(f):

            @wraps(f)
            def runner(*args, **kwargs):
                if sys.version_info >= (3, 11) and self.loop_factory:
                    with asyncio.Runner(loop_factory=self.loop_factory) as runner:
                        runner.run(f(*args,**kwargs))
                else:
                    asyncio.run(f(*args,**kwargs))

            if inspect.iscoroutinefunction(f):
                return decorator(runner)
            return decorator(f)
        return add_runner

app = UTyper(loop_factory=uvloop.new_event_loop)

@app.command()
def hello(name: str):
    print(f"Hello {name}")

@app.command()
async def goodbye(name: str, formal: bool = False):
    if formal:
        print(f"Goodbye Ms. {name}. Have a good day.")
    else:
        print(f"Bye {name}!")
    await asyncio.sleep(1)

if __name__ == "__main__":
    app()

prints:

$ ./test.py goodbye asdf --formal
Goodbye Ms. asdf. Have a good day.
$ ./test.py hello asdf 
Hello asdf

and the 'goodbye' test pauses for a second, as expected.

takeda commented 1 year ago

This looks good, maybe this should transform into a PR?

toppk commented 1 year ago

Looks like someone is working on a PR, maybe.

One issue I'll leave here for @borissmidt is that if one were to use


subapp=Typer(loop_factory=...)

app=Typer(loop_factory=...)
app.add_typer(subapp,"mysub"))

then it would be nice that the "subapp" app would use the loop_factory from the root Typer() object. I hadn't written that up yet, and currently it doesn't do that in my child class or your native implementation AFAICT

borissmidt commented 1 year ago

I'm a bit junior in python asyncio, so if you have a sample that would be great. I expected that it is automatically used from the task context? I.e. you can call get_current_loop or something like that.

I coded it in a way that if the async function is called directly that the loop factory isn't used.

On Fri, 7 Jul 2023, 21:03 toppk, @.***> wrote:

Looks like someone is working on a PR, maybe.

One issue I'll leave here for @borissmidt https://github.com/borissmidt is that if you use

subapp=Typer(loop_factory=...) app=Typer(loop_factory=...)app.add_typer(subapp,"mysub"))

then it would be nice that the "subapp" app users the loop_factory from the root Typer() object. I hadn't written that up yet, and currently it doesn't do that in my child class or your native implementation AFAICT

— Reply to this email directly, view it on GitHub https://github.com/tiangolo/typer/issues/88#issuecomment-1625911781, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABNXZFR4C5OG2X2VYY33EQDXPBMRNANCNFSM4MGSXYGA . You are receiving this because you were mentioned.Message ID: @.***>

toppk commented 1 year ago

Here's my attempt at some asyncio background: there is only one call to run during an event loop lifecycle. you can create tasks while an event loop is running, but if there isn't an event loop you need to start it up, and the run interface is the only game in town. The whole get_running_loop() interfaces is when you know there is a running loop and you want to do something with it. That's not applicable for us (well it is in the fact that I use it to demonstrate different event loops in use).

Of course you can spin down the event loop and start up another one, but most of the time it's just one event loop per run.

What typer does is route to a method, and we're putting in a asyncio.run (well specifically the 3.11 version of that interface) into the decorator. Since typer will never call two methods, then we're only starting one event loop. Getting the loop factory option shared between Typer instances linked via Typer.add_typer is probably a more obscure use case, because it's not clear how many people replace the event loop, and certainly how many people chain together Typer instances. But imagine if you have Typer instances started in a dozen modules then you have to go to each one to set the loop_factory, that then the one place where it all joins together.

what I'm saying that it would be nice if you don't have to set the loop factory for every typer instance, just the root. This example below (that needs my UTyper class above, as well as uvloop package installed) shows that the loop_factory in use has to be set for each typer instance, and I think that's sub optimal, and unexpected behavior.

Since this the add_typer call is after all the decorations done, such a solution will require some minor surgery that occurs at the time of the add_typer call. Hopefully not too bad. But if this work is too much, I think we can get it into a second PR after this one goes it (which is what I hope you're looking to do soon). It might be better to defer this, as it will certainly complicate the implementation.

Demonstrating that the loop factor needs to be set to each Typer instance instead of just the root.

$ python test-utyper.py goodbye foo
Bye foo!
asyncio.get_running_loop()=<uvloop.Loop running=True closed=False debug=False>

$ python test-utyper.py sub hello foo
Hello to subtest foo
asyncio.get_running_loop()=<_UnixSelectorEventLoop running=True closed=False debug=False>

app = UTyper(loop_factory=uvloop.new_event_loop)
app2 = UTyper()

@app2.command()
async def hello(name: str):
    print(f"Hello to subtest {name}")
    print(f"{asyncio.get_running_loop()=}")

app.add_typer(app2, name="sub")

@app.command()
def hello(name: str):
    print(f"Hello {name}")

@app.command()
async def goodbye(name: str, formal: bool = False):
    if formal:
        print(f"Goodbye Ms. {name}. Have a good day.")
    else:
        print(f"Bye {name}!")
    await asyncio.sleep(1)
    print(f"{asyncio.get_running_loop()=}")

if __name__ == "__main__":
    app()
borissmidt commented 1 year ago

1 Looking at the python async docs the loop factory might be unnecessary if you set the eventloop policy. The disadvantage is that you have to set it before any app is ran.

2 An alternate is to make typer uvloop aware and set it when the package is installed.

3 The disadvantage of doing it on an typer.add is that it is a bit magic that the parents setting is ported to the child setting bur only for the loop. It would also be tricky to get it right with multiple levels of apps. Also when you then run the subapp separately you still don't get the uv eventloop.

4 maybe an classvar to set the eventloop can be used.

I think option 2 needs buy in from the project owner. Option 1 seems the most correct solution so set the policy and i might need to update the pr to use asyncio.get_event_loop. Then the factory stays for app specific overrides.


asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

loop = asyncio.get_event_loop()

On Sat, 8 Jul 2023, 07:11 toppk, @.***> wrote:

Here's my attempt at some asyncio background: there is only one call to run during an event loop lifecycle. you can create tasks while an event loop is running, but if there isn't an event loop you need to start it up, and the run interface is the only game in town. The whole get_running_loop() interfaces is when you know there is a running loop and you want to do something with it. That's not applicable for us (well it is in the fact that I use it to demonstrate different event loops in use).

Of course you can spin down the event loop and start up another one, but most of the time it's just one event loop per run.

What typer does is route to a method, and we're putting in a asyncio.run (well specifically the 3.11 version of that interface) into the decorator. Since typer will never call two methods, then we're only starting one event loop. Getting the loop factory option shared between Typer instances linked via Typer.add_typer is probably a more obscure use case, because it's not clear how many people replace the event loop, and certainly how many people chain together Typer instances. But imagine if you have Typer instances started in a dozen modules then you have to go to each one to set the loop_factory, that then the one place where it all joins together.

what I'm saying that it would be nice if you don't have to set the loop factory for every typer instance, just the root. This example below (that needs my UTyper class above, as well as uvloop package installed) shows that the loop_factory in use has to be set for each typer instance, and I think that's sub optimal, and unexpected behavior.

Since this the add_typer call is after all the decorations done, such a solution will require some minor surgery that occurs at the time of the add_typer call. Hopefully not too bad. But if this work is too much, I think we can get it into a second PR after this one goes it (which is what I hope you're looking to do soon). It might be better to defer this, as it will certainly complicate the implementation.

Demonstrating that the loop factor needs to be set to each Typer instance instead of just the root.

$ python test-utyper.py goodbye foo Bye foo!asyncio.get_running_loop()=

$ python test-utyper.py sub hello foo Hello to subtest fooasyncio.get_running_loop()=<_UnixSelectorEventLoop running=True closed=False debug=False>

app = UTyper(loop_factory=uvloop.new_event_loop)app2 = UTyper() @app2.command()async def hello(name: str): print(f"Hello to subtest {name}") print(f"{asyncio.get_running_loop()=}") app.add_typer(app2, name="sub") @app.command()def hello(name: str): print(f"Hello {name}")

@app.command()async def goodbye(name: str, formal: bool = False): if formal: print(f"Goodbye Ms. {name}. Have a good day.") else: print(f"Bye {name}!") await asyncio.sleep(1) print(f"{asyncio.get_running_loop()=}") if name == "main": app()

— Reply to this email directly, view it on GitHub https://github.com/tiangolo/typer/issues/88#issuecomment-1626860346, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABNXZFTB4GASOBGDK2W7W4TXPDTXFANCNFSM4MGSXYGA . You are receiving this because you were mentioned.Message ID: @.***>