Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
78d6119
Added async support for command and callback including using anyio op…
skeletorXVI Nov 18, 2021
3334fd8
Improve extendability, typing and async engine detection
skeletorXVI Nov 19, 2021
acd1746
Fix typo in pyproject.toml
skeletorXVI Nov 19, 2021
801c890
Remove trio extra dependency
skeletorXVI Nov 19, 2021
624ea30
Added documentation for async support
skeletorXVI Nov 19, 2021
d676044
Fix formatting
skeletorXVI Nov 19, 2021
1b651f5
Modify completion unit tests to also cover async implementation with …
skeletorXVI Nov 26, 2021
288e6e3
Merge remote-tracking branch 'origin/master' into feature/anyio-support
skeletorXVI Aug 12, 2022
2aaa798
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Aug 12, 2022
61baacc
Merge remote-tracking branch 'origin/master' into feature/anyio-support
xqSimone Oct 2, 2023
d6b15f4
added missing tests
xqSimone Oct 11, 2023
7c8285a
formatting
xqSimone Oct 11, 2023
54c908a
fix trio import
xqSimone Oct 12, 2023
638e724
update trio and anyio, fix tests
xqSimone Oct 17, 2023
aba2b6d
fix anyio version
xqSimone Oct 17, 2023
60c1574
anyio 4.0.0 requires python 3.8
xqSimone Oct 18, 2023
150fe7a
anyio 4.0.0 requires python 3.8 pipeline fix
xqSimone Oct 18, 2023
4be4aa4
increase test coverage
xqSimone Oct 18, 2023
e3d9232
fix async tutorial006 and its tests
xqSimone Oct 18, 2023
c30baef
remove unused test
xqSimone Oct 18, 2023
b45e696
increase test coverage
xqSimone Oct 18, 2023
1aba3c2
remove code for python < 3.8
xqSimone Oct 19, 2023
9e5f303
revert unrelated changes to avoid merge conflicts
svlandeg Feb 6, 2025
0a7ee50
revert unrelated changes to avoid merge conflicts
svlandeg Feb 6, 2025
61785fe
fix typo
svlandeg Feb 6, 2025
8d73664
Merge branch 'master' into feature/anyio-support
svlandeg Feb 6, 2025
99b7cc1
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Feb 6, 2025
252dd49
add test requirements
svlandeg Feb 6, 2025
eed2a5a
Merge remote-tracking branch 'upstream_skeletor/feature/anyio-support…
svlandeg Feb 6, 2025
290620c
try to make mypy happy for now
svlandeg Feb 6, 2025
858a166
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Feb 6, 2025
54de531
try to make mypy happy for now (2)
svlandeg Feb 6, 2025
2ee86fa
Merge remote-tracking branch 'upstream_skeletor/feature/anyio-support…
svlandeg Feb 6, 2025
094ce2f
Merge branch 'master' into feature/anyio-support
svlandeg Aug 27, 2025
2622e84
use capture_output
svlandeg Aug 27, 2025
3a1508a
remove unused ignore statements
svlandeg Aug 27, 2025
a625f1e
restore app definition
svlandeg Aug 27, 2025
654541d
remove assertion of exit code (for now)
svlandeg Aug 27, 2025
48b441e
Merge branch 'master' into feature/anyio-support
svlandeg Sep 2, 2025
c23264b
dropping 3.7 test
svlandeg Sep 2, 2025
20df81b
use new docs format
svlandeg Sep 2, 2025
4873df6
streamline documentation
svlandeg Sep 2, 2025
c4e06e6
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Sep 2, 2025
63563c4
fix line numbers
svlandeg Sep 2, 2025
c5b48ce
Merge branch 'master' into feature/anyio-support
svlandeg Nov 19, 2025
3e317fb
remove duplicate import
svlandeg Nov 19, 2025
f13613f
Merge branch 'master' into feature/anyio-support
svlandeg Nov 19, 2025
c1f9569
remove unused ignore statement
svlandeg Nov 19, 2025
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ By default it also comes with extra standard dependencies:
* <a href="https://github.com/sarugaku/shellingham" class="external-link" target="_blank"><code>shellingham</code></a>: 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`.
* <a href="https://github.com/agronholm/anyio" class="external-link" target="_blank"><code>anyio</code></a>: and Typer will automatically detect the appropriate engine to run asynchronous code.
* With <a href="https://github.com/python-trio/trio" class="external-link" target="_blank"><code>Trio</code></a> installed alongside Typer, Typer will use Trio to run asynchronous code by default.

### `typer-slim`

Expand Down
4 changes: 4 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,8 @@ By default it also comes with extra standard dependencies:
* <a href="https://github.com/sarugaku/shellingham" class="external-link" target="_blank"><code>shellingham</code></a>: 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`.
* <a href="https://github.com/agronholm/anyio" class="external-link" target="_blank"><code>anyio</code></a>: and Typer will automatically detect the appropriate engine to run asynchronous code.
* With <a href="https://github.com/python-trio/trio" class="external-link" target="_blank"><code>Trio</code></a> installed alongside Typer, Typer will use Trio to run asynchronous code by default.

### `typer-slim`

Expand Down
70 changes: 70 additions & 0 deletions docs/tutorial/async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Async support

## Engines

Typer supports `asyncio` out of the box. <a href="https://github.com/python-trio/trio" class="external-link" target="_blank"><code>Trio</code></a> is supported through
<a href="https://github.com/agronholm/anyio" class="external-link" target="_blank"><code>anyio</code></a>, which can be installed as optional dependency:

<div class="termy">

```console
$ pip install typer[anyio]
---> 100%
Successfully installed typer anyio
```

</div>

### Default engine selection

*none* | anyio | trio | anyio + trio
--- | --- | --- | ---
asyncio | asyncio via anyio | asyncio* | trio via anyio

<small>* If you don't want to install `anyio` when using `trio`, provide your own async_runner function</small>

## 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.
14 changes: 14 additions & 0 deletions docs_src/asynchronous/tutorial001.py
Original file line number Diff line number Diff line change
@@ -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)
13 changes: 13 additions & 0 deletions docs_src/asynchronous/tutorial002.py
Original file line number Diff line number Diff line change
@@ -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)
15 changes: 15 additions & 0 deletions docs_src/asynchronous/tutorial003.py
Original file line number Diff line number Diff line change
@@ -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()
14 changes: 14 additions & 0 deletions docs_src/asynchronous/tutorial004.py
Original file line number Diff line number Diff line change
@@ -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()
14 changes: 14 additions & 0 deletions docs_src/asynchronous/tutorial005.py
Original file line number Diff line number Diff line change
@@ -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()
24 changes: 24 additions & 0 deletions docs_src/asynchronous/tutorial006.py
Original file line number Diff line number Diff line change
@@ -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()
22 changes: 22 additions & 0 deletions docs_src/asynchronous/tutorial007.py
Original file line number Diff line number Diff line change
@@ -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()
22 changes: 22 additions & 0 deletions docs_src/asynchronous/tutorial008.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions requirements-tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
26 changes: 26 additions & 0 deletions tests/test_completion/conftest.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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()
21 changes: 21 additions & 0 deletions tests/test_completion/test_commands_index_tutorial002_async.py
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading