diff --git a/docs/tutorial/async-cmd.md b/docs/tutorial/async-cmd.md
new file mode 100644
index 0000000000..e7d46b249c
--- /dev/null
+++ b/docs/tutorial/async-cmd.md
@@ -0,0 +1,40 @@
+Typer allows you to use [async](https://docs.python.org/3/library/asyncio.html) functions.
+
+```Python
+{!../docs_src/async_cmd/async001.py!}
+```
+
+
+
+```console
+$ python main.py
+
+Hello Async World
+```
+
+
+
+It also works with commands, and you can mix regular and async commands:
+
+```Python
+{!../docs_src/async_cmd/async002.py!}
+```
+
+
+
+```console
+$ python main.py sync
+
+Hello Sync World
+
+$ python main.py async
+
+Hello Async World
+```
+
+
+!!! info
+ Under the hood, Typer is running your async functions with [asyncio.run()](https://docs.python.org/3/library/asyncio-runner.html#asyncio.run)
+
+!!! warning
+ Typer only supports async functions on Python 3.7+
diff --git a/docs_src/async_cmd/async001.py b/docs_src/async_cmd/async001.py
new file mode 100644
index 0000000000..bb4285f47e
--- /dev/null
+++ b/docs_src/async_cmd/async001.py
@@ -0,0 +1,14 @@
+import asyncio
+
+import typer
+
+app = typer.Typer()
+
+
+async def main():
+ await asyncio.sleep(0)
+ print("Hello Async World")
+
+
+if __name__ == "__main__":
+ typer.run(main)
diff --git a/docs_src/async_cmd/async002.py b/docs_src/async_cmd/async002.py
new file mode 100644
index 0000000000..9d2f47b8c4
--- /dev/null
+++ b/docs_src/async_cmd/async002.py
@@ -0,0 +1,20 @@
+import asyncio
+
+import typer
+
+app = typer.Typer()
+
+
+@app.command("sync")
+def command_sync():
+ print("Hello Sync World")
+
+
+@app.command("async")
+async def command_async():
+ await asyncio.sleep(0)
+ print("Hello Async World")
+
+
+if __name__ == "__main__":
+ app()
diff --git a/mkdocs.yml b/mkdocs.yml
index 8022a19589..6bfac160f0 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -50,6 +50,7 @@ nav:
- Typer Callback: tutorial/commands/callback.md
- One or Multiple Commands: tutorial/commands/one-or-multiple.md
- Using the Context: tutorial/commands/context.md
+ - Async functions: tutorial/async-cmd.md
- CLI Option autocompletion: tutorial/options-autocompletion.md
- CLI Parameter Types:
- CLI Parameter Types Intro: tutorial/parameter-types/index.md
diff --git a/pyproject.toml b/pyproject.toml
index 9b173bbe2b..d3c6d940ae 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -47,7 +47,7 @@ test = [
"coverage >=6.2,<7.0",
"pytest-xdist >=1.32.0,<4.0.0",
"pytest-sugar >=0.9.4,<0.10.0",
- "mypy ==0.910",
+ "mypy ==0.971",
"black >=22.3.0,<23.0.0",
"isort >=5.0.6,<6.0.0",
"rich >=10.11.0,<14.0.0",
diff --git a/tests/test_tutorial/test_async_cmd/__init__.py b/tests/test_tutorial/test_async_cmd/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/test_tutorial/test_async_cmd/test_async001.py b/tests/test_tutorial/test_async_cmd/test_async001.py
new file mode 100644
index 0000000000..30004d8cdf
--- /dev/null
+++ b/tests/test_tutorial/test_async_cmd/test_async001.py
@@ -0,0 +1,45 @@
+import subprocess
+import sys
+
+import pytest
+import typer
+from typer.testing import CliRunner
+
+from docs_src.async_cmd import async001 as mod
+
+runner = CliRunner()
+
+
+@pytest.mark.skipif(
+ sys.version_info < (3, 7),
+ reason="typer support for async functions requires python3.7 or higher",
+)
+def test_cli():
+ app = typer.Typer()
+ app.command()(mod.main)
+ result = runner.invoke(app, [])
+ assert result.output == "Hello Async World\n"
+
+
+@pytest.mark.skipif(
+ sys.version_info < (3, 7),
+ reason="typer support for async functions requires python3.7 or higher",
+)
+def test_execute():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ encoding="utf-8",
+ )
+ assert result.stdout == "Hello Async World\n"
+
+
+def test_script():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ encoding="utf-8",
+ )
+ assert "Usage" in result.stdout
diff --git a/tests/test_tutorial/test_async_cmd/test_async002.py b/tests/test_tutorial/test_async_cmd/test_async002.py
new file mode 100644
index 0000000000..24929e7a69
--- /dev/null
+++ b/tests/test_tutorial/test_async_cmd/test_async002.py
@@ -0,0 +1,59 @@
+import subprocess
+import sys
+
+import pytest
+from typer.testing import CliRunner
+
+from docs_src.async_cmd import async002 as mod
+
+app = mod.app
+
+runner = CliRunner()
+
+
+def test_command_sync():
+ result = runner.invoke(app, ["sync"])
+ assert result.output == "Hello Sync World\n"
+
+
+@pytest.mark.skipif(
+ sys.version_info < (3, 7),
+ reason="typer support for async functions requires python3.7 or higher",
+)
+def test_command_async():
+ result = runner.invoke(app, ["async"])
+ assert result.output == "Hello Async World\n"
+
+
+def test_execute_sync():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, "sync"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ encoding="utf-8",
+ )
+ assert result.stdout == "Hello Sync World\n"
+
+
+@pytest.mark.skipif(
+ sys.version_info < (3, 7),
+ reason="typer support for async functions requires python3.7 or higher",
+)
+def test_execute_async():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, "async"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ encoding="utf-8",
+ )
+ assert result.stdout == "Hello Async World\n"
+
+
+def test_script():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ encoding="utf-8",
+ )
+ assert "Usage" in result.stdout
diff --git a/typer/_typing.py b/typer/_typing.py
index d906e2227d..bfa8114fcb 100644
--- a/typer/_typing.py
+++ b/typer/_typing.py
@@ -4,14 +4,13 @@
import sys
from os import PathLike
-from typing import ( # type: ignore
+from typing import (
TYPE_CHECKING,
AbstractSet,
Any,
ClassVar,
Dict,
Generator,
- Iterable,
List,
Mapping,
NewType,
diff --git a/typer/completion.py b/typer/completion.py
index 2712d08da4..c42ad95b81 100644
--- a/typer/completion.py
+++ b/typer/completion.py
@@ -1,6 +1,6 @@
import os
import sys
-from typing import Any, Dict, Tuple
+from typing import Any, MutableMapping, Tuple
import click
@@ -120,7 +120,7 @@ def completion_init() -> None:
# This is only called in new Command method, only used by Click 8.x+
def shell_complete(
cli: click.BaseCommand,
- ctx_args: Dict[str, Any],
+ ctx_args: MutableMapping[str, Any],
prog_name: str,
complete_var: str,
instruction: str,
diff --git a/typer/core.py b/typer/core.py
index c0776368a6..0c73784c23 100644
--- a/typer/core.py
+++ b/typer/core.py
@@ -9,6 +9,7 @@
Callable,
Dict,
List,
+ MutableMapping,
Optional,
Sequence,
TextIO,
@@ -634,7 +635,7 @@ def _typer_format_options(
def _typer_main_shell_completion(
self: click.core.Command,
*,
- ctx_args: Dict[str, Any],
+ ctx_args: MutableMapping[str, Any],
prog_name: str,
complete_var: Optional[str] = None,
) -> None:
@@ -696,7 +697,7 @@ def format_options(
def _main_shell_completion(
self,
- ctx_args: Dict[str, Any],
+ ctx_args: MutableMapping[str, Any],
prog_name: str,
complete_var: Optional[str] = None,
) -> None:
@@ -758,7 +759,7 @@ def format_options(
def _main_shell_completion(
self,
- ctx_args: Dict[str, Any],
+ ctx_args: MutableMapping[str, Any],
prog_name: str,
complete_var: Optional[str] = None,
) -> None:
diff --git a/typer/main.py b/typer/main.py
index aa39e82849..e7464ccb19 100644
--- a/typer/main.py
+++ b/typer/main.py
@@ -8,7 +8,18 @@
from pathlib import Path
from traceback import FrameSummary, StackSummary
from types import TracebackType
-from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ List,
+ Optional,
+ Sequence,
+ Tuple,
+ Type,
+ Union,
+ cast,
+)
from uuid import UUID
import click
@@ -34,7 +45,7 @@
Required,
TyperInfo,
)
-from .utils import get_params_from_function
+from .utils import ensure_sync, get_params_from_function
try:
import rich
@@ -51,7 +62,7 @@
def except_hook(
- exc_type: Type[BaseException], exc_value: BaseException, tb: TracebackType
+ exc_type: Type[BaseException], exc_value: BaseException, tb: Optional[TracebackType]
) -> None:
exception_config: Union[DeveloperExceptionConfig, None] = getattr(
exc_value, _typer_developer_exception_attr_name, None
@@ -235,6 +246,8 @@ def command(
cls = TyperCommand
def decorator(f: CommandFunctionType) -> CommandFunctionType:
+ f = cast(CommandFunctionType, ensure_sync(f))
+
self.registered_commands.append(
CommandInfo(
name=name,
diff --git a/typer/utils.py b/typer/utils.py
index 44816e2420..9518059575 100644
--- a/typer/utils.py
+++ b/typer/utils.py
@@ -1,8 +1,21 @@
import inspect
from copy import copy
-from typing import Any, Callable, Dict, List, Tuple, Type, cast, get_type_hints
-
-from typing_extensions import Annotated
+from functools import wraps
+from typing import (
+ Any,
+ Callable,
+ Coroutine,
+ Dict,
+ List,
+ Tuple,
+ Type,
+ TypeVar,
+ Union,
+ cast,
+ get_type_hints,
+)
+
+from typing_extensions import Annotated, ParamSpec
from ._typing import get_args, get_origin
from .models import ArgumentInfo, OptionInfo, ParameterInfo, ParamMeta
@@ -185,3 +198,23 @@ def get_params_from_function(func: Callable[..., Any]) -> Dict[str, ParamMeta]:
name=param.name, default=default, annotation=annotation
)
return params
+
+
+P = ParamSpec("P")
+R = TypeVar("R")
+
+
+def ensure_sync(f: Callable[P, Union[R, Coroutine[Any, Any, R]]]) -> Callable[P, R]:
+ # If `f` is an async function, wrap it into asyncio.run(f)
+ if not inspect.iscoroutinefunction(f):
+ f_sync = cast(Callable[P, R], f)
+ return f_sync
+
+ @wraps(f)
+ def run_f(*args: P.args, **kwargs: P.kwargs) -> R:
+ import asyncio
+
+ f_async = cast(Callable[P, Coroutine[Any, Any, R]], f)
+ return asyncio.run(f_async(*args, **kwargs))
+
+ return run_f