diff --git a/README.md b/README.md
index a7892a9188..2e83a771d6 100644
--- a/README.md
+++ b/README.md
@@ -360,6 +360,8 @@ By default it also comes with extra standard dependencies:
* shellingham: to automatically detect the current shell when installing completion.
* With `shellingham` you can just use `--install-completion`.
* Without `shellingham`, you have to pass the name of the shell to install completion for, e.g. `--install-completion bash`.
+* anyio: and Typer will automatically detect the appropriate engine to run asynchronous code.
+ * With Trio installed alongside Typer, Typer will use Trio to run asynchronous code by default.
### `typer-slim`
diff --git a/docs/features.md b/docs/features.md
index 5a8643fa35..7b1b25e4f4 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -16,6 +16,10 @@ If you need a 2 minute refresher of how to use Python types (even if you don't u
You will also see a 20 seconds refresher on the section [Tutorial - User Guide: First Steps](tutorial/first-steps.md){.internal-link target=_blank}.
+## Async support
+
+Supports **asyncio** out of the box. [AnyIO](https://github.com/agronholm/anyio) is available as extra dependency for automatic support of [Trio](https://github.com/python-trio/trio).
+
## Editor support
**Typer** was designed to be easy and intuitive to use, to ensure the best development experience. With autocompletion everywhere.
diff --git a/docs/index.md b/docs/index.md
index bc653e84f2..78c5cdad2c 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -366,6 +366,8 @@ By default it also comes with extra standard dependencies:
* shellingham: to automatically detect the current shell when installing completion.
* With `shellingham` you can just use `--install-completion`.
* Without `shellingham`, you have to pass the name of the shell to install completion for, e.g. `--install-completion bash`.
+* anyio: and Typer will automatically detect the appropriate engine to run asynchronous code.
+ * With Trio installed alongside Typer, Typer will use Trio to run asynchronous code by default.
### `typer-slim`
diff --git a/docs/tutorial/async.md b/docs/tutorial/async.md
new file mode 100644
index 0000000000..6292e3471e
--- /dev/null
+++ b/docs/tutorial/async.md
@@ -0,0 +1,70 @@
+# Async support
+
+## Engines
+
+Typer supports `asyncio` out of the box. Trio is supported through
+anyio, which can be installed as optional dependency:
+
+
+
+```console
+$ pip install typer[anyio]
+---> 100%
+Successfully installed typer anyio
+```
+
+
+
+### Default engine selection
+
+*none* | anyio | trio | anyio + trio
+--- | --- | --- | ---
+asyncio | asyncio via anyio | asyncio* | trio via anyio
+
+* If you don't want to install `anyio` when using `trio`, provide your own async_runner function
+
+## Using with run()
+
+Async functions can be run just like normal functions:
+
+{* docs_src/asynchronous/tutorial001.py hl[1,8:9,14] *}
+
+Or using `anyio`:
+
+{* docs_src/asynchronous/tutorial002.py hl[1,8] *}
+
+## Using with commands
+
+Async functions can be registered as commands explicitely just like synchronous functions:
+
+{* docs_src/asynchronous/tutorial003.py hl[1,8:9,15] *}
+
+Or using `anyio`:
+
+{* docs_src/asynchronous/tutorial004.py hl[1,9] *}
+
+Or using `trio` via `anyio`:
+
+{* docs_src/asynchronous/tutorial005.py hl[1,9] *}
+
+## Customizing async engine
+
+You can customize the async engine by providing an additional parameter `async_runner` to the Typer instance or to the command decorator.
+
+When both are provided, the one from the decorator will take precedence over the one from the Typer instance.
+
+Customize a single command:
+
+{* docs_src/asynchronous/tutorial007.py hl[15] *}
+
+Customize the default engine for the Typer instance:
+
+{* docs_src/asynchronous/tutorial008.py hl[6] *}
+
+## Using with callback
+
+The callback function supports asynchronous functions with the `async_runner` parameter as well:
+
+{* docs_src/asynchronous/tutorial006.py hl[15] *}
+
+Because the asynchronous functions are wrapped in a synchronous context before being executed, it is possible to mix async engines between the callback and commands.
diff --git a/docs_src/asynchronous/tutorial001.py b/docs_src/asynchronous/tutorial001.py
new file mode 100644
index 0000000000..dc857b7f43
--- /dev/null
+++ b/docs_src/asynchronous/tutorial001.py
@@ -0,0 +1,14 @@
+import asyncio
+
+import typer
+
+app = typer.Typer()
+
+
+async def main():
+ await asyncio.sleep(1)
+ typer.echo("Hello World")
+
+
+if __name__ == "__main__":
+ typer.run(main)
diff --git a/docs_src/asynchronous/tutorial002.py b/docs_src/asynchronous/tutorial002.py
new file mode 100644
index 0000000000..35f626ab78
--- /dev/null
+++ b/docs_src/asynchronous/tutorial002.py
@@ -0,0 +1,13 @@
+import anyio
+import typer
+
+app = typer.Typer()
+
+
+async def main():
+ await anyio.sleep(1)
+ typer.echo("Hello World")
+
+
+if __name__ == "__main__":
+ typer.run(main)
diff --git a/docs_src/asynchronous/tutorial003.py b/docs_src/asynchronous/tutorial003.py
new file mode 100644
index 0000000000..6266979b8b
--- /dev/null
+++ b/docs_src/asynchronous/tutorial003.py
@@ -0,0 +1,15 @@
+import asyncio
+
+import typer
+
+app = typer.Typer(async_runner=asyncio.run)
+
+
+@app.command()
+async def wait(seconds: int):
+ await asyncio.sleep(seconds)
+ typer.echo(f"Waited for {seconds} seconds")
+
+
+if __name__ == "__main__":
+ app()
diff --git a/docs_src/asynchronous/tutorial004.py b/docs_src/asynchronous/tutorial004.py
new file mode 100644
index 0000000000..7ad9b5d23c
--- /dev/null
+++ b/docs_src/asynchronous/tutorial004.py
@@ -0,0 +1,14 @@
+import anyio
+import typer
+
+app = typer.Typer()
+
+
+@app.command()
+async def wait(seconds: int):
+ await anyio.sleep(seconds)
+ typer.echo(f"Waited for {seconds} seconds")
+
+
+if __name__ == "__main__":
+ app()
diff --git a/docs_src/asynchronous/tutorial005.py b/docs_src/asynchronous/tutorial005.py
new file mode 100644
index 0000000000..4325b9280f
--- /dev/null
+++ b/docs_src/asynchronous/tutorial005.py
@@ -0,0 +1,14 @@
+import trio
+import typer
+
+app = typer.Typer()
+
+
+@app.command()
+async def wait(seconds: int):
+ await trio.sleep(seconds)
+ typer.echo(f"Waited for {seconds} seconds")
+
+
+if __name__ == "__main__":
+ app()
diff --git a/docs_src/asynchronous/tutorial006.py b/docs_src/asynchronous/tutorial006.py
new file mode 100644
index 0000000000..6901707269
--- /dev/null
+++ b/docs_src/asynchronous/tutorial006.py
@@ -0,0 +1,24 @@
+import asyncio
+
+import trio
+import typer
+
+app = typer.Typer()
+
+
+@app.command()
+async def wait_trio(seconds: int):
+ await trio.sleep(seconds)
+ typer.echo(f"Waited for {seconds} seconds using trio (default)")
+
+
+@app.callback(async_runner=lambda c: asyncio.run(c))
+async def wait_asyncio(seconds: int):
+ await asyncio.sleep(seconds)
+ typer.echo(
+ f"Waited for {seconds} seconds before running command using asyncio (customized)"
+ )
+
+
+if __name__ == "__main__":
+ app()
diff --git a/docs_src/asynchronous/tutorial007.py b/docs_src/asynchronous/tutorial007.py
new file mode 100644
index 0000000000..bd91855d1b
--- /dev/null
+++ b/docs_src/asynchronous/tutorial007.py
@@ -0,0 +1,22 @@
+import asyncio
+
+import trio
+import typer
+
+app = typer.Typer()
+
+
+@app.command()
+async def wait_trio(seconds: int):
+ await trio.sleep(seconds)
+ typer.echo(f"Waited for {seconds} seconds using trio (default)")
+
+
+@app.command(async_runner=asyncio.run)
+async def wait_asyncio(seconds: int):
+ await asyncio.sleep(seconds)
+ typer.echo(f"Waited for {seconds} seconds using asyncio (custom runner)")
+
+
+if __name__ == "__main__":
+ app()
diff --git a/docs_src/asynchronous/tutorial008.py b/docs_src/asynchronous/tutorial008.py
new file mode 100644
index 0000000000..e8ffd25f1c
--- /dev/null
+++ b/docs_src/asynchronous/tutorial008.py
@@ -0,0 +1,22 @@
+import asyncio
+
+import anyio
+import typer
+
+app = typer.Typer(async_runner=lambda c: anyio.run(lambda: c, backend="asyncio"))
+
+
+@app.command()
+async def wait_anyio(seconds: int):
+ await anyio.sleep(seconds)
+ typer.echo(f"Waited for {seconds} seconds using asyncio via anyio")
+
+
+@app.command()
+async def wait_asyncio(seconds: int):
+ await asyncio.sleep(seconds)
+ typer.echo(f"Waited for {seconds} seconds using asyncio")
+
+
+if __name__ == "__main__":
+ app()
diff --git a/mkdocs.yml b/mkdocs.yml
index a9a661098b..b8a32bb82c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -133,6 +133,7 @@ nav:
- tutorial/testing.md
- tutorial/using-click.md
- tutorial/package.md
+ - tutorial/async.md
- tutorial/exceptions.md
- tutorial/one-file-per-command.md
- tutorial/typer-command.md
diff --git a/requirements-tests.txt b/requirements-tests.txt
index db41b866d4..9d57720cdb 100644
--- a/requirements-tests.txt
+++ b/requirements-tests.txt
@@ -5,8 +5,12 @@ pytest-cov >=2.10.0,<8.0.0
coverage[toml] >=6.2,<8.0
pytest-xdist >=1.32.0,<4.0.0
pytest-sugar >=0.9.4,<1.2.0
+pytest-mock >=3.11.1
mypy ==1.14.1
ruff ==0.14.5
+anyio >=4.0.0
+filelock >=3.4.0,<4.0.0
+trio >=0.22
# Needed explicitly by typer-slim
rich >=10.11.0
shellingham >=1.3.0
diff --git a/tests/test_completion/conftest.py b/tests/test_completion/conftest.py
new file mode 100644
index 0000000000..5da91079ff
--- /dev/null
+++ b/tests/test_completion/conftest.py
@@ -0,0 +1,26 @@
+import pytest
+from filelock import FileLock
+
+
+@pytest.fixture
+def bashrc_lock():
+ with FileLock(".bachrc.lock"):
+ yield
+
+
+@pytest.fixture
+def zshrc_lock():
+ with FileLock(".zsh.lock"):
+ yield
+
+
+@pytest.fixture
+def fish_config_lock():
+ with FileLock(".fish.lock"):
+ yield
+
+
+@pytest.fixture
+def powershell_profile_lock():
+ with FileLock(".powershell.lock"):
+ yield
diff --git a/tests/test_completion/for_testing/commands_help_tutorial001_async.py b/tests/test_completion/for_testing/commands_help_tutorial001_async.py
new file mode 100644
index 0000000000..9d83a5f657
--- /dev/null
+++ b/tests/test_completion/for_testing/commands_help_tutorial001_async.py
@@ -0,0 +1,58 @@
+import typer
+
+app = typer.Typer(help="Awesome CLI user manager.")
+
+
+@app.command()
+async def create(username: str):
+ """
+ Create a new user with USERNAME.
+ """
+ typer.echo(f"Creating user: {username}")
+
+
+@app.command()
+async def delete(
+ username: str,
+ # force: bool = typer.Option(False, "--force")
+ force: bool = typer.Option(
+ False,
+ prompt="Are you sure you want to delete the user?",
+ help="Force deletion without confirmation.",
+ ),
+):
+ """
+ Delete a user with USERNAME.
+
+ If --force is not used, will ask for confirmation.
+ """
+ typer.echo(f"Deleting user: {username}" if force else "Operation cancelled")
+
+
+@app.command()
+async def delete_all(
+ force: bool = typer.Option(
+ False,
+ prompt="Are you sure you want to delete ALL users?",
+ help="Force deletion without confirmation.",
+ ),
+):
+ """
+ Delete ALL users in the database.
+
+ If --force is not used, will ask for confirmation.
+ """
+
+ typer.echo("Deleting all users" if force else "Operation cancelled")
+
+
+@app.command()
+async def init():
+ """
+ Initialize the users database.
+ """
+ typer.echo("Initializing user database")
+
+
+if __name__ == "__main__":
+ app()
diff --git a/tests/test_completion/for_testing/commands_index_tutorial002_async.py b/tests/test_completion/for_testing/commands_index_tutorial002_async.py
new file mode 100644
index 0000000000..6afab4aaae
--- /dev/null
+++ b/tests/test_completion/for_testing/commands_index_tutorial002_async.py
@@ -0,0 +1,17 @@
+import typer
+
+app = typer.Typer()
+
+
+@app.command()
+def create():
+ typer.echo("Creating user: Hiro Hamada")
+
+
+@app.command()
+def delete():
+ typer.echo("Deleting user: Hiro Hamada")
+
+
+if __name__ == "__main__":
+ app()
diff --git a/tests/test_completion/test_commands_index_tutorial002_async.py b/tests/test_completion/test_commands_index_tutorial002_async.py
new file mode 100644
index 0000000000..6e5d924a09
--- /dev/null
+++ b/tests/test_completion/test_commands_index_tutorial002_async.py
@@ -0,0 +1,21 @@
+from typer.testing import CliRunner
+
+from tests.test_completion.for_testing import (
+ commands_index_tutorial002_async as async_mod,
+)
+
+app = async_mod.app
+
+runner = CliRunner()
+
+
+def test_create():
+ result = runner.invoke(app, ["create"])
+ assert result.exit_code == 0
+ assert "Creating user: Hiro Hamada" in result.output
+
+
+def test_delete():
+ result = runner.invoke(app, ["delete"])
+ assert result.exit_code == 0
+ assert "Deleting user: Hiro Hamada" in result.output
diff --git a/tests/test_completion/test_completion.py b/tests/test_completion/test_completion.py
index 4e1586e51d..a0f5b4cff6 100644
--- a/tests/test_completion/test_completion.py
+++ b/tests/test_completion/test_completion.py
@@ -3,14 +3,20 @@
import sys
from pathlib import Path
-from docs_src.commands.index import tutorial001 as mod
+import pytest
+
+from docs_src.asynchronous import tutorial001 as async_mod
+from docs_src.commands.index import tutorial001 as sync_mod
from ..utils import needs_bash, needs_linux, requires_completion_permission
+mod_params = ("mod", (sync_mod, async_mod))
+
@needs_bash
@needs_linux
-def test_show_completion():
+@pytest.mark.parametrize(*mod_params)
+def test_show_completion(bashrc_lock, mod):
result = subprocess.run(
[
"bash",
@@ -27,7 +33,8 @@ def test_show_completion():
@needs_bash
@needs_linux
@requires_completion_permission
-def test_install_completion():
+@pytest.mark.parametrize(*mod_params)
+def test_install_completion(bashrc_lock, mod):
bash_completion_path: Path = Path.home() / ".bashrc"
text = ""
if bash_completion_path.is_file(): # pragma: no cover
@@ -50,7 +57,8 @@ def test_install_completion():
assert "Completion will take effect once you restart the terminal" in result.stdout
-def test_completion_invalid_instruction():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_invalid_instruction(bashrc_lock, mod):
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__],
capture_output=True,
@@ -64,7 +72,8 @@ def test_completion_invalid_instruction():
assert "Invalid completion instruction." in result.stderr
-def test_completion_source_bash():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_source_bash(bashrc_lock, mod):
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__],
capture_output=True,
@@ -80,7 +89,8 @@ def test_completion_source_bash():
)
-def test_completion_source_invalid_shell():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_source_invalid_shell(bashrc_lock, mod):
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__],
capture_output=True,
@@ -93,7 +103,8 @@ def test_completion_source_invalid_shell():
assert "Shell xxx not supported." in result.stderr
-def test_completion_source_invalid_instruction():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_source_invalid_instruction(bashrc_lock, mod):
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__],
capture_output=True,
@@ -106,7 +117,8 @@ def test_completion_source_invalid_instruction():
assert 'Completion instruction "explode" not supported.' in result.stderr
-def test_completion_source_zsh():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_source_zsh(bashrc_lock, mod):
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__],
capture_output=True,
@@ -119,7 +131,8 @@ def test_completion_source_zsh():
assert "compdef _tutorial001py_completion tutorial001.py" in result.stdout
-def test_completion_source_fish():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_source_fish(bashrc_lock, mod):
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__],
capture_output=True,
@@ -132,7 +145,8 @@ def test_completion_source_fish():
assert "complete --command tutorial001.py --no-files" in result.stdout
-def test_completion_source_powershell():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_source_powershell(bashrc_lock, mod):
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__],
capture_output=True,
@@ -148,7 +162,8 @@ def test_completion_source_powershell():
)
-def test_completion_source_pwsh():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_source_pwsh(bashrc_lock, mod):
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__],
capture_output=True,
diff --git a/tests/test_completion/test_completion_complete.py b/tests/test_completion/test_completion_complete.py
index ea37f68546..0ce22a9753 100644
--- a/tests/test_completion/test_completion_complete.py
+++ b/tests/test_completion/test_completion_complete.py
@@ -2,48 +2,60 @@
import subprocess
import sys
-from docs_src.commands.help import tutorial001 as mod
+import pytest
+from docs_src.commands.help import tutorial001 as sync_mod
-def test_completion_complete_subcommand_bash():
+from .for_testing import commands_help_tutorial001_async as async_mod
+
+mod_params = ("mod", (sync_mod, async_mod))
+
+
+@pytest.mark.parametrize(*mod_params)
+def test_completion_complete_subcommand_bash(mod):
+ filename = os.path.basename(mod.__file__)
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
- "_TUTORIAL001.PY_COMPLETE": "complete_bash",
- "COMP_WORDS": "tutorial001.py del",
+ f"_{filename.upper()}_COMPLETE": "complete_bash",
+ "COMP_WORDS": f"{filename} del",
"COMP_CWORD": "1",
},
)
assert "delete\ndelete-all" in result.stdout
-def test_completion_complete_subcommand_bash_invalid():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_complete_subcommand_bash_invalid(mod):
+ filename = os.path.basename(mod.__file__)
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
- "_TUTORIAL001.PY_COMPLETE": "complete_bash",
- "COMP_WORDS": "tutorial001.py del",
+ f"_{filename.upper()}_COMPLETE": "complete_bash",
+ "COMP_WORDS": f"{filename} del",
"COMP_CWORD": "42",
},
)
assert "create\ndelete\ndelete-all\ninit" in result.stdout
-def test_completion_complete_subcommand_zsh():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_complete_subcommand_zsh(mod):
+ filename = os.path.basename(mod.__file__)
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
- "_TUTORIAL001.PY_COMPLETE": "complete_zsh",
- "_TYPER_COMPLETE_ARGS": "tutorial001.py del",
+ f"_{filename.upper()}_COMPLETE": "complete_zsh",
+ "_TYPER_COMPLETE_ARGS": f"{filename} del",
},
)
assert (
@@ -52,29 +64,33 @@ def test_completion_complete_subcommand_zsh():
) in result.stdout
-def test_completion_complete_subcommand_zsh_files():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_complete_subcommand_zsh_files(mod):
+ filename = os.path.basename(mod.__file__)
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
- "_TUTORIAL001.PY_COMPLETE": "complete_zsh",
- "_TYPER_COMPLETE_ARGS": "tutorial001.py delete ",
+ f"_{filename.upper()}_COMPLETE": "complete_zsh",
+ "_TYPER_COMPLETE_ARGS": f"{filename} delete ",
},
)
assert ("_files") in result.stdout
-def test_completion_complete_subcommand_fish():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_complete_subcommand_fish(mod):
+ filename = os.path.basename(mod.__file__)
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
- "_TUTORIAL001.PY_COMPLETE": "complete_fish",
- "_TYPER_COMPLETE_ARGS": "tutorial001.py del",
+ f"_{filename.upper()}_COMPLETE": "complete_fish",
+ "_TYPER_COMPLETE_ARGS": f"{filename} del",
"_TYPER_COMPLETE_FISH_ACTION": "get-args",
},
)
@@ -84,45 +100,51 @@ def test_completion_complete_subcommand_fish():
)
-def test_completion_complete_subcommand_fish_should_complete():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_complete_subcommand_fish_should_complete(mod):
+ filename = os.path.basename(mod.__file__)
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
- "_TUTORIAL001.PY_COMPLETE": "complete_fish",
- "_TYPER_COMPLETE_ARGS": "tutorial001.py del",
+ f"_{filename.upper()}_COMPLETE": "complete_fish",
+ "_TYPER_COMPLETE_ARGS": f"{filename} del",
"_TYPER_COMPLETE_FISH_ACTION": "is-args",
},
)
assert result.returncode == 0
-def test_completion_complete_subcommand_fish_should_complete_no():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_complete_subcommand_fish_should_complete_no(mod):
+ filename = os.path.basename(mod.__file__)
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
- "_TUTORIAL001.PY_COMPLETE": "complete_fish",
- "_TYPER_COMPLETE_ARGS": "tutorial001.py delete ",
+ f"_{filename.upper()}_COMPLETE": "complete_fish",
+ "_TYPER_COMPLETE_ARGS": "{filename} delete ",
"_TYPER_COMPLETE_FISH_ACTION": "is-args",
},
)
assert result.returncode != 0
-def test_completion_complete_subcommand_powershell():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_complete_subcommand_powershell(mod):
+ filename = os.path.basename(mod.__file__)
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
- "_TUTORIAL001.PY_COMPLETE": "complete_powershell",
- "_TYPER_COMPLETE_ARGS": "tutorial001.py del",
+ f"_{filename.upper()}_COMPLETE": "complete_powershell",
+ "_TYPER_COMPLETE_ARGS": f"{filename} del",
},
)
assert (
@@ -130,15 +152,17 @@ def test_completion_complete_subcommand_powershell():
) in result.stdout
-def test_completion_complete_subcommand_pwsh():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_complete_subcommand_pwsh(mod):
+ filename = os.path.basename(mod.__file__)
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
- "_TUTORIAL001.PY_COMPLETE": "complete_pwsh",
- "_TYPER_COMPLETE_ARGS": "tutorial001.py del",
+ f"_{filename.upper()}_COMPLETE": "complete_pwsh",
+ "_TYPER_COMPLETE_ARGS": f"{filename} del",
},
)
assert (
@@ -146,15 +170,17 @@ def test_completion_complete_subcommand_pwsh():
) in result.stdout
-def test_completion_complete_subcommand_noshell():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_complete_subcommand_noshell(mod):
+ filename = os.path.basename(mod.__file__)
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
- "_TUTORIAL001.PY_COMPLETE": "complete_noshell",
- "_TYPER_COMPLETE_ARGS": "tutorial001.py del",
+ f"_{filename.upper()}_COMPLETE": "complete_noshell",
+ "_TYPER_COMPLETE_ARGS": f"{filename} del",
},
)
- assert ("") in result.stdout
+ assert "" in result.stdout
diff --git a/tests/test_completion/test_completion_complete_no_help.py b/tests/test_completion/test_completion_complete_no_help.py
index 4ac2bf98de..4af97a8eae 100644
--- a/tests/test_completion/test_completion_complete_no_help.py
+++ b/tests/test_completion/test_completion_complete_no_help.py
@@ -2,62 +2,76 @@
import subprocess
import sys
-from docs_src.commands.index import tutorial002 as mod
+import pytest
+from docs_src.commands.index import tutorial002 as sync_mod
-def test_completion_complete_subcommand_zsh():
+from .for_testing import commands_index_tutorial002_async as async_mod
+
+mod_params = ("mod", (sync_mod, async_mod))
+
+
+@pytest.mark.parametrize(*mod_params)
+def test_completion_complete_subcommand_zsh(mod):
+ filename = os.path.basename(mod.__file__)
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
- "_TUTORIAL002.PY_COMPLETE": "complete_zsh",
- "_TYPER_COMPLETE_ARGS": "tutorial002.py ",
+ f"_{filename.upper()}_COMPLETE": "complete_zsh",
+ "_TYPER_COMPLETE_ARGS": f"{filename} ",
},
)
assert "create" in result.stdout
assert "delete" in result.stdout
-def test_completion_complete_subcommand_fish():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_complete_subcommand_fish(mod):
+ filename = os.path.basename(mod.__file__)
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
- "_TUTORIAL002.PY_COMPLETE": "complete_fish",
- "_TYPER_COMPLETE_ARGS": "tutorial002.py ",
+ f"_{filename.upper()}_COMPLETE": "complete_fish",
+ "_TYPER_COMPLETE_ARGS": f"{filename} ",
"_TYPER_COMPLETE_FISH_ACTION": "get-args",
},
)
assert "create\ndelete" in result.stdout
-def test_completion_complete_subcommand_powershell():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_complete_subcommand_powershell(mod):
+ filename = os.path.basename(mod.__file__)
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
- "_TUTORIAL002.PY_COMPLETE": "complete_powershell",
- "_TYPER_COMPLETE_ARGS": "tutorial002.py ",
+ f"_{filename.upper()}_COMPLETE": "complete_powershell",
+ "_TYPER_COMPLETE_ARGS": f"{filename} ",
},
)
- assert ("create::: \ndelete::: ") in result.stdout
+ assert "create::: \ndelete::: " in result.stdout
-def test_completion_complete_subcommand_pwsh():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_complete_subcommand_pwsh(mod):
+ filename = os.path.basename(mod.__file__)
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, " "],
capture_output=True,
encoding="utf-8",
env={
**os.environ,
- "_TUTORIAL002.PY_COMPLETE": "complete_pwsh",
- "_TYPER_COMPLETE_ARGS": "tutorial002.py ",
+ f"_{filename.upper()}_COMPLETE": "complete_pwsh",
+ "_TYPER_COMPLETE_ARGS": f"{filename} ",
},
)
- assert ("create::: \ndelete::: ") in result.stdout
+ assert "create::: \ndelete::: " in result.stdout
diff --git a/tests/test_completion/test_completion_help_tutorial001.py b/tests/test_completion/test_completion_help_tutorial001.py
new file mode 100644
index 0000000000..2530d0089e
--- /dev/null
+++ b/tests/test_completion/test_completion_help_tutorial001.py
@@ -0,0 +1,57 @@
+import subprocess
+import sys
+
+from typer.testing import CliRunner
+
+from tests.test_completion.for_testing import (
+ commands_help_tutorial001_async as async_mod,
+)
+
+app = async_mod.app
+
+runner = CliRunner()
+
+
+def test_init():
+ result = runner.invoke(app, ["init"])
+ assert result.exit_code == 0
+ assert "Initializing user database" in result.output
+
+
+def test_delete():
+ result = runner.invoke(app, ["delete", "--force", "Simone"])
+ assert result.exit_code == 0
+ assert "Deleting user: Simone" in result.output
+
+
+def test_delete_no_force():
+ result = runner.invoke(app, ["delete", "Simone"])
+ # assert result.exit_code == 0
+ assert "Are you sure you want to delete the user?" in result.output
+
+
+def test_delete_all():
+ result = runner.invoke(app, ["delete-all", "--force"])
+ assert result.exit_code == 0
+ assert "Deleting all users" in result.output
+
+
+def test_delete_all_no_force():
+ result = runner.invoke(app, ["delete-all"])
+ # assert result.exit_code == 0
+ assert "Are you sure you want to delete ALL users?" in result.output
+
+
+def test_create():
+ result = runner.invoke(app, ["create", "Simone"])
+ assert result.exit_code == 0
+ assert "Creating user: Simone" in result.output
+
+
+def test_script():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", async_mod.__file__, "--help"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Usage" in result.stdout
diff --git a/tests/test_completion/test_completion_install.py b/tests/test_completion/test_completion_install.py
index 873c1416e9..3d8787d973 100644
--- a/tests/test_completion/test_completion_install.py
+++ b/tests/test_completion/test_completion_install.py
@@ -4,21 +4,26 @@
from pathlib import Path
from unittest import mock
+import pytest
import shellingham
import typer
from typer.testing import CliRunner
-from docs_src.commands.index import tutorial001 as mod
+from docs_src.asynchronous import tutorial001 as async_mod
+from docs_src.commands.index import tutorial001 as sync_mod
from ..utils import requires_completion_permission
+mod_params = ("mod", (sync_mod, async_mod))
+
runner = CliRunner()
app = typer.Typer()
-app.command()(mod.main)
+app.command()(sync_mod.main)
@requires_completion_permission
-def test_completion_install_no_shell():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_install_no_shell(mod):
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, "--install-completion"],
capture_output=True,
@@ -32,7 +37,8 @@ def test_completion_install_no_shell():
@requires_completion_permission
-def test_completion_install_bash():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_install_bash(bashrc_lock, mod):
bash_completion_path: Path = Path.home() / ".bashrc"
text = ""
if bash_completion_path.is_file():
@@ -72,7 +78,8 @@ def test_completion_install_bash():
@requires_completion_permission
-def test_completion_install_zsh():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_install_zsh(zshrc_lock, mod):
completion_path: Path = Path.home() / ".zshrc"
text = ""
if not completion_path.is_file(): # pragma: no cover
@@ -110,7 +117,8 @@ def test_completion_install_zsh():
@requires_completion_permission
-def test_completion_install_fish():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_install_fish(fish_config_lock, mod):
script_path = Path(mod.__file__)
completion_path: Path = (
Path.home() / f".config/fish/completions/{script_path.name}.fish"
@@ -140,7 +148,8 @@ def test_completion_install_fish():
@requires_completion_permission
-def test_completion_install_powershell():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_install_powershell(powershell_profile_lock, mod):
completion_path: Path = (
Path.home() / ".config/powershell/Microsoft.PowerShell_profile.ps1"
)
diff --git a/tests/test_completion/test_completion_show.py b/tests/test_completion/test_completion_show.py
index 10d8b6ff39..68bf3f876f 100644
--- a/tests/test_completion/test_completion_show.py
+++ b/tests/test_completion/test_completion_show.py
@@ -3,18 +3,22 @@
import sys
from unittest import mock
+import pytest
import shellingham
import typer
from typer.testing import CliRunner
-from docs_src.commands.index import tutorial001 as mod
+from docs_src.asynchronous import tutorial001 as async_mod
+from docs_src.commands.index import tutorial001 as sync_mod
runner = CliRunner()
-app = typer.Typer()
-app.command()(mod.main)
+mod_params = ("mod", (sync_mod, async_mod))
-def test_completion_show_no_shell():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_show_no_shell(mod):
+ app = typer.Typer()
+ app.command()(mod.main)
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, "--show-completion"],
capture_output=True,
@@ -27,7 +31,10 @@ def test_completion_show_no_shell():
assert "Option '--show-completion' requires an argument" in result.stderr
-def test_completion_show_bash():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_show_bash(mod):
+ app = typer.Typer()
+ app.command()(mod.main)
result = subprocess.run(
[
sys.executable,
@@ -51,7 +58,10 @@ def test_completion_show_bash():
)
-def test_completion_source_zsh():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_source_zsh(mod):
+ app = typer.Typer()
+ app.command()(mod.main)
result = subprocess.run(
[
sys.executable,
@@ -72,7 +82,10 @@ def test_completion_source_zsh():
assert "compdef _tutorial001py_completion tutorial001.py" in result.stdout
-def test_completion_source_fish():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_source_fish(mod):
+ app = typer.Typer()
+ app.command()(mod.main)
result = subprocess.run(
[
sys.executable,
@@ -93,7 +106,10 @@ def test_completion_source_fish():
assert "complete --command tutorial001.py --no-files" in result.stdout
-def test_completion_source_powershell():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_source_powershell(mod):
+ app = typer.Typer()
+ app.command()(mod.main)
result = subprocess.run(
[
sys.executable,
@@ -117,7 +133,10 @@ def test_completion_source_powershell():
)
-def test_completion_source_pwsh():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_source_pwsh(mod):
+ app = typer.Typer()
+ app.command()(mod.main)
result = subprocess.run(
[
sys.executable,
@@ -141,7 +160,10 @@ def test_completion_source_pwsh():
)
-def test_completion_show_invalid_shell():
+@pytest.mark.parametrize(*mod_params)
+def test_completion_show_invalid_shell(mod):
+ app = typer.Typer()
+ app.command()(mod.main)
with mock.patch.object(
shellingham, "detect_shell", return_value=("xshell", "/usr/bin/xshell")
):
diff --git a/tests/test_tutorial/test_asynchronous/__init__.py b/tests/test_tutorial/test_asynchronous/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/test_tutorial/test_asynchronous/test_tutorial001.py b/tests/test_tutorial/test_asynchronous/test_tutorial001.py
new file mode 100644
index 0000000000..d612f6fb65
--- /dev/null
+++ b/tests/test_tutorial/test_asynchronous/test_tutorial001.py
@@ -0,0 +1,38 @@
+import subprocess
+import sys
+from unittest.mock import patch
+
+import typer
+from typer.testing import CliRunner
+
+from docs_src.asynchronous import tutorial001 as async_mod
+
+runner = CliRunner()
+
+app = typer.Typer()
+app.command()(async_mod.main)
+
+
+@patch("importlib.util.find_spec")
+def test_asyncio(mocker):
+ mocker.side_effect = [True, None]
+ result = runner.invoke(app)
+ assert result.exit_code == 0
+ assert "Hello World\n" in result.output
+
+
+@patch("importlib.util.find_spec")
+def test_asyncio_no_anyio(mocker):
+ mocker.side_effect = [None, None]
+ result = runner.invoke(app)
+ assert result.exit_code == 0
+ assert "Hello World\n" in result.output
+
+
+def test_script():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", async_mod.__file__, "--help"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Usage" in result.stdout
diff --git a/tests/test_tutorial/test_asynchronous/test_tutorial002.py b/tests/test_tutorial/test_asynchronous/test_tutorial002.py
new file mode 100644
index 0000000000..8e0ab9cc3b
--- /dev/null
+++ b/tests/test_tutorial/test_asynchronous/test_tutorial002.py
@@ -0,0 +1,28 @@
+import subprocess
+import sys
+
+import typer
+from typer.testing import CliRunner
+
+from docs_src.asynchronous import tutorial002 as async_mod
+
+runner = CliRunner()
+
+app = typer.Typer()
+app.command()(async_mod.main)
+
+
+def test_anyio():
+ result = runner.invoke(app)
+
+ assert result.exit_code == 0
+ assert "Hello World\n" in result.output
+
+
+def test_script():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", async_mod.__file__, "--help"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Usage" in result.stdout
diff --git a/tests/test_tutorial/test_asynchronous/test_tutorial003.py b/tests/test_tutorial/test_asynchronous/test_tutorial003.py
new file mode 100644
index 0000000000..e27e2894c8
--- /dev/null
+++ b/tests/test_tutorial/test_asynchronous/test_tutorial003.py
@@ -0,0 +1,25 @@
+import subprocess
+import sys
+
+from typer.testing import CliRunner
+
+from docs_src.asynchronous import tutorial003 as mod
+
+app = mod.app
+
+runner = CliRunner()
+
+
+def test_wait():
+ result = runner.invoke(app, ["2"])
+ assert result.exit_code == 0
+ assert "Waited for 2 seconds" in result.output
+
+
+def test_script():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Usage" in result.stdout
diff --git a/tests/test_tutorial/test_asynchronous/test_tutorial004.py b/tests/test_tutorial/test_asynchronous/test_tutorial004.py
new file mode 100644
index 0000000000..846081862a
--- /dev/null
+++ b/tests/test_tutorial/test_asynchronous/test_tutorial004.py
@@ -0,0 +1,25 @@
+import subprocess
+import sys
+
+from typer.testing import CliRunner
+
+from docs_src.asynchronous import tutorial004 as mod
+
+app = mod.app
+
+runner = CliRunner()
+
+
+def test_wait():
+ result = runner.invoke(app, ["2"])
+ assert result.exit_code == 0
+ assert "Waited for 2 seconds" in result.output
+
+
+def test_script():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Usage" in result.stdout
diff --git a/tests/test_tutorial/test_asynchronous/test_tutorial005.py b/tests/test_tutorial/test_asynchronous/test_tutorial005.py
new file mode 100644
index 0000000000..5c9f94c5b1
--- /dev/null
+++ b/tests/test_tutorial/test_asynchronous/test_tutorial005.py
@@ -0,0 +1,25 @@
+import subprocess
+import sys
+
+from typer.testing import CliRunner
+
+from docs_src.asynchronous import tutorial005 as mod
+
+app = mod.app
+
+runner = CliRunner()
+
+
+def test_wait():
+ result = runner.invoke(app, ["2"])
+ assert result.exit_code == 0
+ assert "Waited for 2 seconds" in result.output
+
+
+def test_script():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Usage" in result.stdout
diff --git a/tests/test_tutorial/test_asynchronous/test_tutorial006.py b/tests/test_tutorial/test_asynchronous/test_tutorial006.py
new file mode 100644
index 0000000000..50d7e4e914
--- /dev/null
+++ b/tests/test_tutorial/test_asynchronous/test_tutorial006.py
@@ -0,0 +1,28 @@
+import subprocess
+import sys
+
+from typer.testing import CliRunner
+
+from docs_src.asynchronous import tutorial006 as mod
+
+app = mod.app
+
+runner = CliRunner()
+
+
+def test_wait_asyncio():
+ result = runner.invoke(app, ["1", "wait-trio", "2"])
+ assert result.exit_code == 0
+ assert (
+ "Waited for 1 seconds before running command using asyncio (customized)\n"
+ "Waited for 2 seconds using trio (default)" in result.output
+ )
+
+
+def test_script():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Usage" in result.stdout
diff --git a/tests/test_tutorial/test_asynchronous/test_tutorial007.py b/tests/test_tutorial/test_asynchronous/test_tutorial007.py
new file mode 100644
index 0000000000..66a0400a7d
--- /dev/null
+++ b/tests/test_tutorial/test_asynchronous/test_tutorial007.py
@@ -0,0 +1,31 @@
+import subprocess
+import sys
+
+from typer.testing import CliRunner
+
+from docs_src.asynchronous import tutorial007 as mod
+
+app = mod.app
+
+runner = CliRunner()
+
+
+def test_wait_trio():
+ result = runner.invoke(app, ["wait-trio", "2"])
+ assert result.exit_code == 0
+ assert "Waited for 2 seconds using trio (default)" in result.output
+
+
+def test_wait_asyncio():
+ result = runner.invoke(app, ["wait-asyncio", "2"])
+ assert result.exit_code == 0
+ assert "Waited for 2 seconds using asyncio (custom runner)" in result.output
+
+
+def test_script():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Usage" in result.stdout
diff --git a/tests/test_tutorial/test_asynchronous/test_tutorial008.py b/tests/test_tutorial/test_asynchronous/test_tutorial008.py
new file mode 100644
index 0000000000..9a446c5871
--- /dev/null
+++ b/tests/test_tutorial/test_asynchronous/test_tutorial008.py
@@ -0,0 +1,31 @@
+import subprocess
+import sys
+
+from typer.testing import CliRunner
+
+from docs_src.asynchronous import tutorial008 as mod
+
+app = mod.app
+
+runner = CliRunner()
+
+
+def test_wait_anyio():
+ result = runner.invoke(app, ["wait-anyio", "2"])
+ assert result.exit_code == 0
+ assert "Waited for 2 seconds using asyncio via anyio" in result.output
+
+
+def test_wait_asyncio():
+ result = runner.invoke(app, ["wait-asyncio", "2"])
+ assert result.exit_code == 0
+ assert "Waited for 2 seconds using asyncio" in result.output
+
+
+def test_script():
+ result = subprocess.run(
+ [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"],
+ capture_output=True,
+ encoding="utf-8",
+ )
+ assert "Usage" in result.stdout
diff --git a/typer/main.py b/typer/main.py
index 71a25e6c4b..55e759f316 100644
--- a/typer/main.py
+++ b/typer/main.py
@@ -1,3 +1,4 @@
+import importlib
import inspect
import os
import platform
@@ -7,11 +8,22 @@
import traceback
from datetime import datetime
from enum import Enum
-from functools import update_wrapper
+from functools import update_wrapper, wraps
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,
+ Coroutine,
+ Dict,
+ List,
+ Optional,
+ Sequence,
+ Tuple,
+ Type,
+ Union,
+)
from uuid import UUID
import click
@@ -31,6 +43,7 @@
from .models import (
AnyType,
ArgumentInfo,
+ AsyncRunner,
CommandFunctionType,
CommandInfo,
Default,
@@ -45,11 +58,31 @@
ParameterInfo,
ParamMeta,
Required,
+ SyncCommandFunctionType,
TyperInfo,
TyperPath,
)
from .utils import get_params_from_function
+
+def run_as_sync(coroutine: Coroutine[Any, Any, Any]) -> Any:
+ """
+ When using anyio we try to predict the appropriate backend assuming that
+ alternative async engines are not mixed and only installed on demand.
+ """
+
+ if importlib.util.find_spec("anyio"):
+ import anyio
+
+ backend = "trio" if importlib.util.find_spec("trio") else "asyncio"
+
+ return anyio.run(lambda: coroutine, backend=backend)
+ else:
+ import asyncio
+
+ return asyncio.run(coroutine)
+
+
_original_except_hook = sys.excepthook
_typer_developer_exception_attr_name = "__typer_developer_exception__"
@@ -132,6 +165,7 @@ def __init__(
hidden: bool = Default(False),
deprecated: bool = Default(False),
add_completion: bool = True,
+ async_runner: AsyncRunner = run_as_sync,
# Rich settings
rich_markup_mode: MarkupMode = Default(DEFAULT_MARKUP_MODE),
rich_help_panel: Union[str, None] = Default(None),
@@ -168,6 +202,22 @@ def __init__(
self.registered_groups: List[TyperInfo] = []
self.registered_commands: List[CommandInfo] = []
self.registered_callback: Optional[TyperInfo] = None
+ self.async_runner = async_runner
+
+ def to_sync(
+ self, f: CommandFunctionType, async_runner: Optional[AsyncRunner]
+ ) -> SyncCommandFunctionType: # type: ignore
+ if inspect.iscoroutinefunction(f):
+ run_sync: AsyncRunner = async_runner or self.async_runner
+
+ @wraps(f)
+ def execute(*args: Any, **kwargs: Any) -> Any:
+ return run_sync(f(*args, **kwargs))
+
+ else:
+ execute = f
+
+ return execute # type: ignore
def callback(
self,
@@ -178,6 +228,7 @@ def callback(
subcommand_metavar: Optional[str] = Default(None),
chain: bool = Default(False),
result_callback: Optional[Callable[..., Any]] = Default(None),
+ async_runner: Optional[AsyncRunner] = None,
# Command
context_settings: Optional[Dict[Any, Any]] = Default(None),
help: Optional[str] = Default(None),
@@ -199,7 +250,7 @@ def decorator(f: CommandFunctionType) -> CommandFunctionType:
chain=chain,
result_callback=result_callback,
context_settings=context_settings,
- callback=f,
+ callback=self.to_sync(f, async_runner),
help=help,
epilog=epilog,
short_help=short_help,
@@ -209,7 +260,7 @@ def decorator(f: CommandFunctionType) -> CommandFunctionType:
deprecated=deprecated,
rich_help_panel=rich_help_panel,
)
- return f
+ return f # self.to_sync(f, async_runner) # TESTING
return decorator
@@ -227,6 +278,7 @@ def command(
no_args_is_help: bool = False,
hidden: bool = False,
deprecated: bool = False,
+ async_runner: Optional[AsyncRunner] = None,
# Rich settings
rich_help_panel: Union[str, None] = Default(None),
) -> Callable[[CommandFunctionType], CommandFunctionType]:
@@ -239,7 +291,7 @@ def decorator(f: CommandFunctionType) -> CommandFunctionType:
name=name,
cls=cls,
context_settings=context_settings,
- callback=f,
+ callback=self.to_sync(f, async_runner),
help=help,
epilog=epilog,
short_help=short_help,
@@ -252,7 +304,10 @@ def decorator(f: CommandFunctionType) -> CommandFunctionType:
rich_help_panel=rich_help_panel,
)
)
- return f
+ # test = f()
+ # test2 = self.to_sync(f, async_runner)
+ # test3 = test2()
+ return f # self.to_sync(f, async_runner) # TESTING
return decorator
@@ -1071,7 +1126,9 @@ def wrapper(ctx: click.Context, args: List[str], incomplete: Optional[str]) -> A
return wrapper
-def run(function: Callable[..., Any]) -> None:
+def run(
+ function: Union[Callable[..., Any], Callable[..., Coroutine[Any, Any, Any]]],
+) -> None:
app = Typer(add_completion=False)
app.command()(function)
app()
diff --git a/typer/models.py b/typer/models.py
index e0bddb965b..5cbea37876 100644
--- a/typer/models.py
+++ b/typer/models.py
@@ -4,6 +4,7 @@
TYPE_CHECKING,
Any,
Callable,
+ Coroutine,
Dict,
List,
Optional,
@@ -69,7 +70,22 @@ def __bool__(self) -> bool:
DefaultType = TypeVar("DefaultType")
-CommandFunctionType = TypeVar("CommandFunctionType", bound=Callable[..., Any])
+
+AsyncCommandFunctionType = TypeVar(
+ "AsyncCommandFunctionType", bound=Callable[..., Coroutine[Any, Any, Any]]
+)
+
+SyncCommandFunctionType = TypeVar("SyncCommandFunctionType", bound=Callable[..., Any])
+
+
+CommandFunctionType = TypeVar(
+ "CommandFunctionType",
+ Callable[..., Any],
+ Callable[..., Coroutine[Any, Any, Any]],
+)
+
+
+AsyncRunner = Callable[[Coroutine[Any, Any, Any]], Any]
def Default(value: DefaultType) -> DefaultType: