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
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ dev = [
"autoflake >=1.3.1,<2.0.0",
"flake8 >=3.8.3,<4.0.0",
]
# TODO: Add a version number for anyio
async = ["anyio"]
all = [
"anyio",
"colorama >=0.4.3,<0.5.0",
"shellingham >=1.3.0,<2.0.0"
]
Expand Down
35 changes: 34 additions & 1 deletion typer/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import inspect
from datetime import datetime
from enum import Enum
from functools import update_wrapper
from functools import update_wrapper, wraps
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
from uuid import UUID
Expand Down Expand Up @@ -165,7 +165,40 @@ def decorator(f: CommandFunctionType) -> CommandFunctionType:
return f

return decorator


def async_command(self, *args: Any, backend=None, **kwargs: Any) -> Callable[[CommandFunctionType], CommandFunctionType]:
"""Same arguments as command but works with async functions."""
# Dynamically import either anyio or asyncio
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.


def decorator(async_func: CommandFunctionType) -> CommandFunctionType:
# 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):
if backend is not None:
return run(async_func(*_args, **_kwargs), backend=backend)
return run(async_func(*_args, **_kwargs))

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

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

return decorator


def add_typer(
self,
typer_instance: "Typer",
Expand Down