Skip to content

Commit be69f9e

Browse files
Improve DI Docs (#10)
- Improve the documentation about Dependency Injection, describes how to replace Rodi with [Dependency Injector](https://python-dependency-injector.ets-labs.org/). - Improve the extensions page.
1 parent 06c1b31 commit be69f9e

File tree

2 files changed

+209
-30
lines changed

2 files changed

+209
-30
lines changed

blacksheep/docs/dependency-injection.md

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,8 @@ class ContainerProtocol:
524524
"""
525525
```
526526

527+
### Using Punq instead of Rodi
528+
527529
The following example demonstrates how to use
528530
[`punq`](https://github.com/bobthemighty/punq) for dependency injection as an
529531
alternative to `rodi`.
@@ -628,3 +630,184 @@ because not all libraries for dependency injection implement the notion of
628630
`transient`).
629631

630632
///
633+
634+
### Using Dependency Injector instead of Rodi
635+
636+
The following example illustrates how to use [Dependency Injector](https://python-dependency-injector.ets-labs.org/) instead of Rodi.
637+
638+
```python {linenums="1" hl_lines="3 19-24 31 40-41 43 75 84 95-100"}
639+
from typing import Type, TypeVar, get_type_hints
640+
641+
from dependency_injector import containers, providers
642+
643+
from blacksheep import Application, get
644+
645+
T = TypeVar("T")
646+
647+
648+
class APIClient: ...
649+
650+
651+
class SomeService:
652+
653+
def __init__(self, api_client: APIClient) -> None:
654+
self.api_client = api_client
655+
656+
657+
# Define the Dependency Injector container
658+
class AppContainer(containers.DeclarativeContainer):
659+
APIClient = providers.Singleton(APIClient)
660+
SomeService = providers.Factory(
661+
SomeService, api_client=APIClient
662+
)
663+
664+
665+
# Create the container instance
666+
container = AppContainer()
667+
668+
669+
class DependencyInjectorConnector:
670+
"""
671+
This class connects a Dependency Injector container with a
672+
BlackSheep application.
673+
Dependencies are registered using the code API offered by
674+
Dependency Injector. The BlackSheep application activates services
675+
using the container when needed.
676+
"""
677+
678+
def __init__(self, container: containers.Container) -> None:
679+
self._container = container
680+
681+
def register(self, obj_type: Type[T]) -> None:
682+
"""
683+
Registers a type with the container.
684+
The code below inspects the object's constructor's types annotations to
685+
automatically configure the provider to activate the type.
686+
687+
It is not necessary to use @inject or Provide core on the __init__ method. This
688+
helps reducing code verbosity and keeping the source code not polluted by DI
689+
specific code.
690+
"""
691+
constructor = getattr(obj_type, "__init__", None)
692+
693+
if not constructor:
694+
raise ValueError(
695+
f"Type {obj_type.__name__} does not have an __init__ method."
696+
)
697+
698+
# Get the type hints for the constructor parameters
699+
type_hints = get_type_hints(constructor)
700+
701+
# Exclude 'self' from the parameters
702+
dependencies = {
703+
param_name: getattr(self._container, param_type.__name__)
704+
for param_name, param_type in type_hints.items()
705+
if param_name not in {"self", "return"}
706+
and hasattr(self._container, param_type.__name__)
707+
}
708+
709+
# Create a provider for the type with its dependencies
710+
provider = providers.Factory(obj_type, **dependencies)
711+
setattr(self._container, obj_type.__name__, provider)
712+
713+
def resolve(self, obj_type: Type[T], _) -> T:
714+
"""Resolves an instance of the given type."""
715+
provider = getattr(self._container, obj_type.__name__, None)
716+
if provider is None:
717+
raise TypeError(
718+
f"Type {obj_type.__name__} is not registered in the container."
719+
)
720+
return provider()
721+
722+
def __contains__(self, item: Type[T]) -> bool:
723+
"""Checks if a type is registered in the container."""
724+
return hasattr(self._container, item.__name__)
725+
726+
727+
app = Application(
728+
services=DependencyInjectorConnector(container), show_error_details=True
729+
)
730+
731+
732+
@get("/")
733+
def home(service: SomeService):
734+
print(service)
735+
# DependencyInjector resolved the dependencies
736+
assert isinstance(service, SomeService)
737+
assert isinstance(service.api_client, APIClient)
738+
return id(service)
739+
740+
```
741+
742+
**Notes:**
743+
744+
- By using **composition**, we can integrate a third-party dependency injection
745+
library like `dependency_injector` into BlackSheep without tightly coupling
746+
the framework to the library.
747+
- We need a class like `DependencyInjectorConnector` that acts as a
748+
bridge between `dependency_injector` and BlackSheep.
749+
- When wiring dependencies for your application, you use the code API offered
750+
by **Dependency Injector**.
751+
- BlackSheep remains agnostic about the specific dependency injection library
752+
being used, but it needs the interface provided by the connector.
753+
- In this case, **Dependency Injector** _Provide_ and _@inject_ constructs are
754+
not needed on request handlers because BlackSheep handles the injection of
755+
parameters into request handlers and infers when it needs to resolve a type
756+
using the provided _connector_.
757+
758+
In the example above, the name of the properties must match the type names
759+
simply because `DependencyInjectorConnector` is obtaining `providers` by exact
760+
type names. We could easily follow the convention of using **snake_case** or
761+
a more robust approach of obtaining providers by types by changing the
762+
connector's logic. Expand the sections below to show different examples.
763+
764+
The connector can resolve types for controllers' `__init__` methods:
765+
766+
```python
767+
class APIClient: ...
768+
769+
770+
class SomeService:
771+
772+
def __init__(self, api_client: APIClient) -> None:
773+
self.api_client = api_client
774+
775+
776+
class AnotherService: ...
777+
778+
779+
# Define the Dependency Injector container
780+
class AppContainer(containers.DeclarativeContainer):
781+
APIClient = providers.Singleton(APIClient)
782+
SomeService = providers.Factory(SomeService, api_client=APIClient)
783+
AnotherService = providers.Factory(AnotherService)
784+
785+
786+
class TestController(Controller):
787+
788+
def __init__(self, another_dep: AnotherService) -> None:
789+
super().__init__()
790+
self._another_dep = (
791+
another_dep # another_dep is resolved by Dependency Injector
792+
)
793+
794+
@app.controllers_router.get("/controller-test")
795+
def controller_test(self, service: SomeService):
796+
# DependencyInjector resolved the dependencies
797+
assert isinstance(self._another_dep, AnotherService)
798+
799+
assert isinstance(service, SomeService)
800+
assert isinstance(service.api_client, APIClient)
801+
return id(service)
802+
```
803+
804+
_[Full example](https://github.com/Neoteroi/BlackSheep-Examples/blob/main/dependency-injector/main.py)._
805+
806+
/// admonition | :snake: Examples.
807+
type: hint
808+
809+
The [_BlackSheep-Examples_](https://github.com/Neoteroi/BlackSheep-Examples/blob/main/dependency-injector/). repository contains examples for integrating with
810+
_Dependency Injector_, including an example illustrating how to use `snake_case` for providers in
811+
the Dependency Injector's container: [_BlackSheep-Examples_](https://github.com/Neoteroi/BlackSheep-Examples/blob/main/dependency-injector/docs/example2.py).
812+
813+
///

blacksheep/docs/extensions.md

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,24 @@
33
This page provides a list of BlackSheep projects, extensions, or packages
44
providing integration with BlackSheep.
55

6-
## Torino
7-
Torino is an advanced project built using BlackSheep for its API part,
8-
including a single page application built using React, TypeScript, HTML5, and
9-
SASS. It consists of a private file storage and photo gallery for Azure Storage
10-
Account. The project provides examples of how to integrate a BlackSheep API to
11-
PostgreSQL or SQLite using SQLAlchemy, with migrations, and also how to
12-
structure a project using dependency injection, integrate with Azure
13-
Application Insights, and more.
6+
## Apitally
7+
[Apitally](https://apitally.io/blacksheep) is a lightweight monitoring and
8+
analytics tool for APIs, with built-in support for BlackSheep. It tracks
9+
API usage, errors, and performance, and includes request logging and alerting
10+
features.
1411

15-
<span class="small">[🏠 Homepage](https://github.com/Neoteroi/Torino)</span>
12+
![Apitally](https://camo.githubusercontent.com/9f4ba2c60ba0e8d1c15beac2517415cf8f6ba1c1e5f12b18d947f4811264c45c/68747470733a2f2f6173736574732e61706974616c6c792e696f2f73637265656e73686f74732f6f766572766965772e706e67)
13+
14+
<span class="small">[🏠 Homepage](https://github.com/apitally/apitally-py)</span>
15+
16+
## Piccolo-ORM
17+
[Piccolo](https://github.com/piccolo-orm/piccolo) is a fast, user-friendly ORM and query builder which supports asyncio.
18+
Piccolo provides a CLI that lets you scaffold new ASGI applications, including
19+
support for BlackSheep.
20+
21+
![Piccolo-ORM](https://raw.githubusercontent.com/piccolo-orm/piccolo/master/docs/logo_hero.png)
22+
23+
<span class="small">[🏠 Homepage](https://github.com/piccolo-orm/piccolo)</span>
1624

1725
## BlackSheep-SQLAlchemy
1826
Extension for BlackSheep that simplifies the use of
@@ -26,25 +34,13 @@ provides integration to collect telemetries about web requests.
2634

2735
<span class="small">[🏠 Homepage](https://github.com/Cdayz/blacksheep-prometheus)</span>
2836

29-
## Piccolo-ORM
30-
Piccolo is a fast, user-friendly ORM and query builder which supports asyncio.
31-
Piccolo provides a CLI that lets you scaffold new ASGI applications, including
32-
support for BlackSheep.
33-
34-
<span class="small">[🏠 Homepage](https://github.com/piccolo-orm/piccolo)</span>
35-
36-
## Venezia
37-
Venezia is an advanced demo project for a BlackSheep web service deployed to
38-
Azure App Service, using a PostgreSQL database, GitHub Workflows, and
39-
ARM templates. It includes an example integration with
40-
[Azure Application Insights](https://github.com/Neoteroi/Venezia/blob/dev/server/app/logs.py).
41-
42-
<span class="small">[🏠 Homepage](https://github.com/Neoteroi/Venezia)</span>
43-
44-
## Apitally
45-
[Apitally](https://apitally.io/blacksheep) is a lightweight monitoring and
46-
analytics tool for APIs, with built-in support for BlackSheep. It tracks
47-
API usage, errors, and performance, and includes request logging and alerting
48-
features.
37+
## Torino
38+
Torino is an advanced example built using BlackSheep for its API part,
39+
including a single page application built using React, TypeScript, HTML5, and
40+
SASS. It consists of a private file storage and photo gallery for Azure Storage
41+
Account. The project provides examples of how to integrate a BlackSheep API to
42+
PostgreSQL or SQLite using SQLAlchemy, with migrations, and also how to
43+
structure a project using dependency injection, integrate with Azure
44+
Application Insights, and more.
4945

50-
<span class="small">[🏠 Homepage](https://github.com/apitally/apitally-py)</span>
46+
<span class="small">[🏠 Homepage](https://github.com/Neoteroi/Torino)</span>

0 commit comments

Comments
 (0)