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

Async support optionally using anyio #340

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 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
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
9 changes: 2 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.8", "3.9", "3.10", "3.11"]
click-7: [true, false]
fail-fast: false

Expand All @@ -25,17 +25,12 @@ jobs:
- name: Install Flit
run: pip install flit
- name: Install Dependencies
if: ${{ matrix.python-version != '3.6' }}
run: python -m flit install --symlink
- name: Install Dependencies
if: ${{ matrix.python-version == '3.6' }}
# This doesn't install the editable install, so coverage doesn't get subprocesses
run: python -m pip install ".[test]"
- name: Install Click 7
if: matrix.click-7
run: pip install "click<8.0.0"
- name: Lint
if: ${{ matrix.python-version != '3.6' && matrix.click-7 == false }}
if: ${{ matrix.click-7 == false }}
run: bash scripts/lint.sh
- run: mkdir coverage
- name: Test
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ dist
.idea
site
.coverage
.coverage.*
htmlcov
.pytest_cache
coverage.xml
.*.lock
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ But you can also install extras:
* <a href="https://github.com/sarugaku/shellingham" class="external-link" target="_blank"><code>shellingham</code></a>: and Typer will 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.

You can install `typer` with `rich` and `shellingham` with `pip install typer[all]`.

Expand Down
4 changes: 4 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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 @@ -309,6 +309,8 @@ But you can also install extras:
* <a href="https://github.com/sarugaku/shellingham" class="external-link" target="_blank"><code>shellingham</code></a>: and Typer will 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.

You can install `typer` with `rich` and `shellingham` with `pip install typer[all]`.

Expand Down
88 changes: 88 additions & 0 deletions docs/tutorial/async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# 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:

```Python
{!../docs_src/asynchronous/tutorial001.py!}
```

Or using `anyio`:

```Python
{!../docs_src/asynchronous/tutorial002.py!}
```

<small>Important to note, `typer.run()` doesn't provide means to customize the async run behavior.</small>

## Using with commands

Async functions can be registered as commands just like synchronous functions:

```Python
{!../docs_src/asynchronous/tutorial003.py!}
```

Or using `anyio`:

```Python
{!../docs_src/asynchronous/tutorial004.py!}
```

Or using `trio` via `anyio`:

```Python
{!../docs_src/asynchronous/tutorial005.py!}
```

## Using with callback

The callback function supports asynchronous functions just like commands including the `async_runner` parameter:

```Python
{!../docs_src/asynchronous/tutorial006.py!}
```

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.

## Customizing async engine

Customizing the used async engine is as simple a providing an additional parameter to the Typer instance or the decorators.

The `async_runner` provided to the decorator always overwrites the typer instances `async_runner`.

Customize a single command:

```Python
{!../docs_src/asynchronous/tutorial007.py!}
```

Customize the default engine for the Typer instance:

```Python
{!../docs_src/asynchronous/tutorial008.py!}
```
15 changes: 15 additions & 0 deletions docs_src/asynchronous/tutorial001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import asyncio

import typer

app = typer.Typer()


@app.command()
async def main():
await asyncio.sleep(1)
typer.echo("Hello World")


if __name__ == "__main__":
app()
14 changes: 14 additions & 0 deletions docs_src/asynchronous/tutorial002.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 main():
await anyio.sleep(1)
typer.echo("Hello World")


if __name__ == "__main__":
app()
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 @@ -80,6 +80,7 @@ nav:
- Testing: tutorial/testing.md
- Using Click: tutorial/using-click.md
- Building a Package: tutorial/package.md
- Async support: 'tutorial/async.md'
- tutorial/exceptions.md
- Typer CLI - completion for small scripts: typer-cli.md
- Alternatives, Inspiration and Comparisons: alternatives.md
Expand Down
10 changes: 9 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ requires = [
"typing-extensions >= 3.7.4.3",
]
description-file = "README.md"
requires-python = ">=3.6"
requires-python = ">=3.8"

[tool.flit.metadata.urls]
Documentation = "https://typer.tiangolo.com/"
Expand All @@ -47,10 +47,14 @@ test = [
"coverage >=6.2,<7.0",
"pytest-xdist >=1.32.0,<4.0.0",
"pytest-sugar >=0.9.4,<0.10.0",
"pytest-mock >= 3.11.1",
"mypy ==0.971",
"black >=22.3.0,<23.0.0",
"isort >=5.0.6,<6.0.0",
"rich >=10.11.0,<14.0.0",
"anyio >= 4.0.0",
"filelock >= 3.4.0, < 4.0.0",
"trio >= 0.22",
]
doc = [
"mkdocs >=1.1.2,<2.0.0",
Expand All @@ -70,6 +74,10 @@ all = [
"rich >=10.11.0,<14.0.0",
]

anyio = [
"anyio >= 4.0.0",
]

[tool.isort]
profile = "black"
known_third_party = ["typer", "click"]
Expand Down
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
Loading