Skip to content
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

Added async_command to typer.Typer #332

Closed
wants to merge 7 commits into from

Conversation

ryanpeach
Copy link

From my comment in:

tiangolo#88

Allows command to be used with async.

Let me know if using *args, **kwargs is ok or if it needs to be typed. *args, **kwargs will be more maintainable. Further we could just make @app.command DETECT if it's attached to an async function, but that might be more challenging.

@cauebs
Copy link

cauebs commented Sep 24, 2021

But shouldn't we allow other async runtimes other than asyncio? Not sure how that would work, though. Maybe adding anyio as an optional dependency.

@ryanpeach
Copy link
Author

@cauebs Certainly, I'm somewhat new to async runtimes so I just assumed that asyncio was the bultin and thus "only" one. We can use anyio.

For some reason when I do the method injection as I do in the issue it works. But this PR when I test it with the following test function gives me the following:

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():
    """Foo bar"""
    return await sleep(5)

if __name__=="__main__":
    app()
Traceback (most recent call last):
  File "/Users/ryanpeach/Documents/Workspace/typer/test.py", line 14, in <module>
    app()
  File "/Users/ryanpeach/Documents/Workspace/typer/typer/main.py", line 241, in __call__
    return get_command(self)(*args, **kwargs)
  File "/Users/ryanpeach/Documents/Workspace/OnScale_Cloud_Manager_CLI/.venv/lib/python3.9/site-packages/click/core.py", line 829, in __call__
    return self.main(*args, **kwargs)
  File "/Users/ryanpeach/Documents/Workspace/OnScale_Cloud_Manager_CLI/.venv/lib/python3.9/site-packages/click/core.py", line 782, in main
    rv = self.invoke(ctx)
  File "/Users/ryanpeach/Documents/Workspace/OnScale_Cloud_Manager_CLI/.venv/lib/python3.9/site-packages/click/core.py", line 1066, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/Users/ryanpeach/Documents/Workspace/OnScale_Cloud_Manager_CLI/.venv/lib/python3.9/site-packages/click/core.py", line 610, in invoke
    return callback(*args, **kwargs)
  File "/Users/ryanpeach/Documents/Workspace/typer/typer/main.py", line 527, in wrapper
    return callback(**use_params)  # type: ignore
  File "/Users/ryanpeach/Documents/Workspace/typer/typer/main.py", line 183, in sync_func
    return run(async_func(*_args, **_kwargs))
  File "/Users/ryanpeach/Documents/Workspace/typer/typer/main.py", line 891, in run
    app()
  File "/Users/ryanpeach/Documents/Workspace/typer/typer/main.py", line 241, in __call__
    return get_command(self)(*args, **kwargs)
  File "/Users/ryanpeach/Documents/Workspace/typer/typer/main.py", line 266, in get_command
    click_command = get_command_from_info(typer_instance.registered_commands[0])
  File "/Users/ryanpeach/Documents/Workspace/typer/typer/main.py", line 452, in get_command_from_info
    ) = get_params_convertors_ctx_param_name_from_function(command_info.callback)
  File "/Users/ryanpeach/Documents/Workspace/typer/typer/main.py", line 428, in get_params_convertors_ctx_param_name_from_function
    parameters = get_params_from_function(callback)
  File "/Users/ryanpeach/Documents/Workspace/typer/typer/utils.py", line 10, in get_params_from_function
    signature = inspect.signature(func)
  File "/usr/local/Cellar/[email protected]/3.9.7/Frameworks/Python.framework/Versions/3.9/lib/python3.9/inspect.py", line 3111, in signature
    return Signature.from_callable(obj, follow_wrapped=follow_wrapped)
  File "/usr/local/Cellar/[email protected]/3.9.7/Frameworks/Python.framework/Versions/3.9/lib/python3.9/inspect.py", line 2860, in from_callable
    return _signature_from_callable(obj, sigcls=cls,
  File "/usr/local/Cellar/[email protected]/3.9.7/Frameworks/Python.framework/Versions/3.9/lib/python3.9/inspect.py", line 2259, in _signature_from_callable
    raise TypeError('{!r} is not a callable object'.format(obj))
TypeError: <coroutine object foo at 0x10e4e7240> is not a callable object
sys:1: RuntimeWarning: coroutine 'foo' was never awaited

@ryanpeach
Copy link
Author

Actually now that error isn't reproducing. My current version seems to work.

typer/main.py Outdated
Comment on lines 173 to 176
if backend is not None:
from anyio import run
else:
from asyncio import run
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use a try...except ImportError instead? That way you don't need the backend arg.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I thought about that but what if you need to use the backend arg? Is the backend arg automatic?

Copy link
Author

@ryanpeach ryanpeach Sep 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think actually about this solution:

Instead of adding anyio or any other tool to extras, what if we just had run_func as a kwarg and default it to asyncio.run (as that is builtin). So that anyone can provide literally any tool with any arguments they want. Ultimately they have to import it themselves which keeps it out of our dependency files, making installation easier.

Copy link

@cauebs cauebs Sep 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iirc, asyncio.run has a different signature to anyio.run and trio.run, e.g.:

async def foo(bar): ...
asyncio.run(foo(bar))
anyio.run(foo, bar)
trio.run(foo, bar)

But there might be a way to handle that elegantly.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asyncio: run_func = lambda f, *a, **k: asyncio.run(f(*a, **k))
anyio: run_func = lambda f, *a, **k: anyio.run(f, *a, **k)
trio: run_func = lambda f, *a, **k: trio.run(f, *a, **k)

And we just make our run_func take lambda f, *a, **k

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented a demo pending review and feedback.

typer/main.py Outdated
self,
name: Optional[str] = None,
*,
run_func: RunFunction = lambda f, *args, **kwargs: asyncio.run(f(*args, **kwargs)),
Copy link

@skeletorXVI skeletorXVI Oct 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest to add this argument to the constructor as well. From my point of view it is more likely that you only overwrite the run function once (depending on your async runtime) or in rare special cases.

Maybe do something like

def async_command(
    # ...
    run_func: Optional[RunFunction] = None
    # ...
):
    # ...
    run_func = run_func or self.run_func
    # ...

@jancespivo
Copy link

I don't think it is needed to extend the API with another function async_command. The original function command might be extended to detect whether a decorated function is async or not and change the strategy how to run it.

@github-actions
Copy link

📝 Docs preview for commit 2a255b2 at: https://639cea0e2c118d099d47fb93--typertiangolo.netlify.app

@ryanpeach ryanpeach closed this Feb 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants