-
Notifications
You must be signed in to change notification settings - Fork 51
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: provide a typer integration, highly inspired by the click one
- Loading branch information
Showing
11 changed files
with
353 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
.. _typer: | ||
|
||
Typer | ||
================================= | ||
|
||
|
||
Though it is not required, you can use dishka-click integration. It features automatic injection to command handlers | ||
In contrast with other integrations there is no scope management. | ||
|
||
|
||
|
||
How to use | ||
**************** | ||
|
||
1. Import | ||
|
||
.. code-block:: python | ||
from dishka.integrations.typer import setup_dishka, inject | ||
2. Create a container and set it up with the typer app. Pass ``auto_inject=True`` if you do not want to use the ``@inject`` decorator explicitly. | ||
|
||
.. code-block:: python | ||
app = typer.Typer() | ||
container = make_container(MyProvider()) | ||
setup_dishka(container=container, app=app, auto_inject=True) | ||
3. Mark those of your command handlers parameters which are to be injected with ``FromDishka[]`` | ||
|
||
.. code-block:: python | ||
app = typer.Typer() | ||
@app.command(name="greet") | ||
def greet_user(greeter: FromDishka[Greeter], user: Annotated[str, typer.Argument()]) -> None: | ||
... | ||
3a. *(optional)* decorate them using ``@inject`` if you want to mark commands explicitly | ||
|
||
.. code-block:: python | ||
@app.command(name="greet") | ||
@inject # Use this decorator *before* the command decorator | ||
def greet_user(greeter: FromDishka[Greeter], user: Annotated[str, typer.Argument()]) -> None: | ||
... |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
"""Example typer app greeting a user, taken from | ||
https://typer.tiangolo.com/#example-upgrade with small modifications. | ||
""" | ||
|
||
from typing import Annotated, Protocol | ||
import typer | ||
from functools import partial | ||
from dishka import make_container, Provider | ||
from dishka.entities.scope import Scope | ||
from dishka.integrations.typer import FromDishka, inject, setup_dishka | ||
|
||
|
||
class Greeter(Protocol): | ||
"""Protocol to be extra generic on our greeting infrastructure.""" | ||
def __call__(self, text: str) -> None: ... | ||
|
||
|
||
provider = Provider(scope=Scope.APP) | ||
|
||
# We provide an advanced greeting experience with `typer.secho` | ||
# For a less advanced implementation, we could use `print` | ||
provider.provide(lambda: partial(typer.secho, fg="blue"), provides=Greeter) | ||
|
||
|
||
app = typer.Typer() | ||
|
||
|
||
@app.command() | ||
def hello( | ||
greeter: FromDishka[Greeter], | ||
name: Annotated[str, typer.Argument(..., help="The name to greet")], | ||
) -> None: | ||
greeter(f"Hello {name}") | ||
|
||
|
||
@app.command() | ||
def goodbye(greeter: FromDishka[Greeter], name: str, formal: bool = False) -> None: | ||
if formal: | ||
greeter(f"Goodbye Ms. {name}. Have a good day.") | ||
else: | ||
greeter(f"Bye {name}!") | ||
|
||
|
||
# Setup dishka to inject the dependency container | ||
# *Must* be after defining the commands when using auto_inject | ||
container = make_container(provider) | ||
setup_dishka(container=container, app=app, auto_inject=True) | ||
|
||
|
||
if __name__ == "__main__": | ||
app() | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
"""Example typer app greeting a user, taken from | ||
https://typer.tiangolo.com/#example-upgrade with small modifications. | ||
""" | ||
|
||
from typing import Annotated, Protocol | ||
import typer | ||
from functools import partial | ||
from dishka import make_container, Provider | ||
from dishka.entities.scope import Scope | ||
from dishka.integrations.typer import FromDishka, inject, setup_dishka | ||
|
||
|
||
class Greeter(Protocol): | ||
"""Protocol to be extra generic on our greeting infrastructure.""" | ||
def __call__(self, text: str) -> None: ... | ||
|
||
|
||
provider = Provider(scope=Scope.APP) | ||
|
||
# We provide an advanced greeting experience with `typer.secho` | ||
# For a less advanced implementation, we could use `print` | ||
provider.provide(lambda: partial(typer.secho, fg="blue"), provides=Greeter) | ||
|
||
|
||
app = typer.Typer() | ||
|
||
|
||
# Setup dishka to inject the dependency container | ||
# Can be done before or after defining the commands when using @inject manually | ||
container = make_container(provider) | ||
setup_dishka(container=container, app=app, auto_inject=False) | ||
|
||
|
||
@app.command() | ||
@inject | ||
def hello( | ||
greeter: FromDishka[Greeter], | ||
name: Annotated[str, typer.Argument(..., help="The name to greet")], | ||
) -> None: | ||
greeter(f"Hello {name}") | ||
|
||
|
||
@app.command() | ||
@inject | ||
def goodbye(greeter: FromDishka[Greeter], name: str, formal: bool = False) -> None: | ||
if formal: | ||
greeter(f"Goodbye Ms. {name}. Have a good day.") | ||
else: | ||
greeter(f"Bye {name}!") | ||
|
||
|
||
if __name__ == "__main__": | ||
app() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
-r test.txt | ||
typer==0.15.1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
-r test.txt | ||
typer |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
"""Integration for Typer https://typer.tiangolo.com""" | ||
__all__ = [ | ||
"FromDishka", | ||
"inject", | ||
"setup_dishka", | ||
] | ||
|
||
from collections.abc import Callable | ||
from typing import Final, TypeVar | ||
|
||
import typer | ||
from click import get_current_context | ||
from typer.models import CommandInfo | ||
|
||
from dishka import Container, FromDishka | ||
from .base import is_dishka_injected, wrap_injection | ||
|
||
T = TypeVar("T") | ||
CONTAINER_NAME: Final = "dishka_container" | ||
|
||
|
||
def inject(func: Callable[..., T]) -> Callable[..., T]: | ||
return wrap_injection( | ||
func=func, | ||
container_getter=lambda _, __: get_current_context().meta[ | ||
CONTAINER_NAME | ||
], | ||
remove_depends=True, | ||
is_async=False, | ||
) | ||
|
||
|
||
def _inject_commands(app: typer.Typer) -> None: | ||
for command in app.registered_commands: | ||
if command.callback is not None and not is_dishka_injected(command.callback): | ||
command.callback = inject(command.callback) | ||
|
||
for group in app.registered_groups: | ||
if group.typer_instance is not None: | ||
_inject_commands(group.typer_instance) | ||
|
||
|
||
def setup_dishka( | ||
container: Container, | ||
app: typer.Typer, | ||
*, | ||
finalize_container: bool = True, | ||
auto_inject: bool = False, | ||
) -> None: | ||
|
||
@app.callback() | ||
def inject_dishka_container(context: typer.Context) -> None: | ||
context.meta[CONTAINER_NAME] = container | ||
|
||
if finalize_container: | ||
context.call_on_close(container.close) | ||
|
||
if auto_inject: | ||
_inject_commands(app) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import pytest | ||
|
||
pytest.importorskip("typer") | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
from contextlib import contextmanager | ||
from typing import Annotated, Any, Callable, ContextManager, Iterable, Iterator | ||
|
||
import typer | ||
import pytest | ||
from typer.testing import CliRunner | ||
|
||
from dishka import FromDishka, make_container | ||
from dishka.integrations.typer import inject, setup_dishka | ||
from dishka.provider import Provider | ||
from ..common import ( | ||
APP_DEP_VALUE, | ||
AppDep, | ||
AppMock, | ||
AppProvider, | ||
) | ||
|
||
|
||
AppFactory = Callable[[Callable[..., Any], Provider], ContextManager[typer.Typer]] | ||
|
||
|
||
@contextmanager | ||
def dishka_app(handler: Callable[..., Any], provider: Provider) -> Iterator[typer.Typer]: | ||
app = typer.Typer() | ||
app.command(name="test")(inject(handler)) | ||
|
||
container = make_container(provider) | ||
setup_dishka(container=container, app=app, finalize_container=False) | ||
|
||
yield app | ||
container.close() | ||
|
||
|
||
@contextmanager | ||
def dishka_auto_app(handler: Callable[..., Any], provider: Provider) -> Iterator[typer.Typer]: | ||
app = typer.Typer() | ||
app.command(name="test")(handler) | ||
|
||
container = make_container(provider) | ||
setup_dishka(container=container, app=app, finalize_container=False, auto_inject=True) | ||
|
||
yield app | ||
container.close() | ||
|
||
|
||
@contextmanager | ||
def dishka_nested_group_app(handler: Callable[..., Any], provider: Provider) -> Iterator[typer.Typer]: | ||
app = typer.Typer() | ||
group = typer.Typer() | ||
group.command(name="sub")(handler) | ||
app.add_typer(group, name="test") | ||
|
||
container = make_container(provider) | ||
setup_dishka(container=container, app=app, finalize_container=False, auto_inject=True) | ||
|
||
yield app | ||
container.close() | ||
|
||
|
||
def handle_with_app( | ||
a: FromDishka[AppDep], | ||
mock: FromDishka[AppMock], | ||
) -> None: | ||
mock(a) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"app_factory", | ||
[ | ||
dishka_app, | ||
dishka_auto_app, | ||
], | ||
) | ||
def test_app_dependency(app_provider: AppProvider, app_factory: AppFactory) -> None: | ||
runner = CliRunner() | ||
with app_factory(handle_with_app, app_provider) as command: | ||
result = runner.invoke(command, ["test"]) | ||
assert result.exit_code == 0, result.stdout | ||
app_provider.app_mock.assert_called_with(APP_DEP_VALUE) | ||
app_provider.app_released.assert_not_called() | ||
app_provider.app_released.assert_called_once() | ||
|
||
|
||
def test_app_dependency_with_nested_groups(app_provider: AppProvider): | ||
runner = CliRunner() | ||
with dishka_nested_group_app(handle_with_app, app_provider) as command: | ||
result = runner.invoke(command, ["test", "sub"]) | ||
assert result.exit_code == 0 | ||
app_provider.app_mock.assert_called_with(APP_DEP_VALUE) | ||
app_provider.app_released.assert_not_called() | ||
app_provider.app_released.assert_called() | ||
|
||
|
||
def handle_with_app_and_options( | ||
a: FromDishka[AppDep], | ||
mock: FromDishka[AppMock], | ||
name: Annotated[str, typer.Argument()] = "Wade", | ||
surname: Annotated[str | None, typer.Option()] = None, | ||
) -> None: | ||
mock(a, name, surname) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"app_factory", | ||
[ | ||
dishka_app, | ||
dishka_auto_app, | ||
], | ||
) | ||
def test_app_dependency_with_option(app_provider: AppProvider, app_factory: AppFactory) -> None: | ||
runner = CliRunner() | ||
with app_factory(handle_with_app_and_options, app_provider) as command: | ||
result = runner.invoke(command, ["test", "Wade"]) | ||
assert result.exit_code == 0, result.stdout | ||
app_provider.app_mock.assert_called_with(APP_DEP_VALUE, "Wade", None) | ||
app_provider.request_released.assert_not_called() | ||
|
||
|
||
def test_app_dependency_with_nested_groups_and_option(app_provider: AppProvider) -> None: | ||
runner = CliRunner() | ||
with dishka_nested_group_app(handle_with_app_and_options, app_provider) as command: | ||
result = runner.invoke(command, ["test", "sub", "Wade", "--surname", "Wilson"]) | ||
assert result.exit_code == 0, result.stdout | ||
app_provider.app_mock.assert_called_with(APP_DEP_VALUE, "Wade", "Wilson") | ||
app_provider.request_released.assert_not_called() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters