Skip to content

Commit 11461be

Browse files
authored
Merge pull request #161 from modern-python/docs
add page about DI
2 parents e317ce3 + 1f0cdc9 commit 11461be

4 files changed

Lines changed: 231 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
`modern-di` is a python dependency injection framework which supports the following:
1313

1414
- Automatic dependencies graph based on type annotations
15+
- Also, explicit dependencies are allowed where needed
1516
- Scopes and context management
1617
- Python 3.10+ support
1718
- Fully typed and tested

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Welcome to the `modern-di` documentation!
55
`modern-di` is a Python dependency injection framework which supports the following:
66

77
- Automatic dependencies graph based on type annotations
8+
- Also, explicit dependencies are allowed where needed
89
- Scopes and context management
910
- Python 3.10+ support
1011
- Fully typed and tested

docs/introduction/about-di.md

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
# What is Dependency Injection?
2+
3+
Dependency Injection (DI) is a design pattern where dependencies are provided (injected) from outside rather than created inside a class.
4+
5+
## The Problem
6+
7+
Without DI, classes create their own dependencies, leading to tight coupling:
8+
9+
```python
10+
class UserService:
11+
def __init__(self) -> None:
12+
self.email = EmailService() # ❌ Tight coupling
13+
14+
def register_user(self, email: str) -> None:
15+
self.email.send_email(email, "Welcome!")
16+
```
17+
18+
**Issues:** Hard to test, can't swap implementations, hidden dependencies.
19+
20+
## The Solution
21+
22+
With DI, dependencies are injected from outside:
23+
24+
```python
25+
class UserService:
26+
def __init__(self, email: EmailSender) -> None: # ✅ Injected
27+
self.email = email
28+
29+
def register_user(self, email: str) -> None:
30+
self.email.send_email(email, "Welcome!")
31+
```
32+
33+
**Benefits:** Easy testing, loose coupling, explicit dependencies.
34+
35+
## Why Use Dependency Injection?
36+
37+
### 1. Testability
38+
39+
Inject mocks for testing:
40+
41+
```python
42+
def test_user_service() -> None:
43+
mock_email = Mock(spec=EmailSender)
44+
service = UserService(email=mock_email)
45+
46+
service.register_user("test@example.com")
47+
48+
mock_email.send_email.assert_called_once()
49+
```
50+
51+
### 2. Loose Coupling
52+
53+
Depend on abstractions, not implementations:
54+
55+
```python
56+
class UserService:
57+
def __init__(self, cache: CacheBackend) -> None:
58+
self.cache = cache
59+
60+
# Swap implementations easily
61+
service = UserService(cache=RedisCache()) # Production
62+
service = UserService(cache=DictCache()) # Development
63+
service = UserService(cache=MockCache()) # Testing
64+
```
65+
66+
## Lifetime Management in DI
67+
68+
Objects can have different lifetime cycles (singleton, scoped, transient).
69+
70+
Here are examples in `modern-di`:
71+
72+
```python
73+
74+
from modern_di import Group, Scope, providers
75+
76+
class AppModule(Group):
77+
# Singleton: one instance for entire app
78+
config = providers.Factory(
79+
scope=Scope.APP,
80+
creator=AppConfig,
81+
cache_settings=providers.CacheSettings()
82+
)
83+
84+
# Scoped: one instance per request
85+
db_session = providers.Factory(
86+
scope=Scope.REQUEST,
87+
creator=DatabaseSession,
88+
cache_settings=providers.CacheSettings()
89+
)
90+
91+
# Transient: new instance each time
92+
request_id = providers.Factory(
93+
scope=Scope.REQUEST,
94+
creator=lambda: str(uuid.uuid4())
95+
)
96+
```
97+
98+
## Manual DI vs modern-di
99+
100+
### Manual Wiring
101+
102+
```python
103+
config = AppConfig()
104+
db = DatabaseConnection(config)
105+
email = EmailService(config)
106+
user_service = UserService(db, email)
107+
```
108+
109+
**Problems:** Unwieldy at scale, no lifetime management, scattered configuration.
110+
111+
### Using modern-di
112+
113+
```python
114+
import dataclasses
115+
from modern_di import Container, Group, Scope, providers
116+
117+
118+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
119+
class AppConfig:
120+
db_host: str = "localhost"
121+
db_port: int = 5432
122+
123+
124+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
125+
class DatabaseConnection:
126+
config: AppConfig # ✅ Auto-injected from type hint
127+
128+
129+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
130+
class UserService:
131+
db: DatabaseConnection # ✅ Auto-injected
132+
133+
134+
# Declare dependencies
135+
class AppModule(Group):
136+
config = providers.Factory(
137+
scope=Scope.APP,
138+
creator=AppConfig,
139+
cache_settings=providers.CacheSettings()
140+
)
141+
db = providers.Factory(scope=Scope.REQUEST, creator=DatabaseConnection)
142+
user_service = providers.Factory(scope=Scope.REQUEST, creator=UserService)
143+
144+
145+
# Resolve entire dependency graph
146+
container = Container(groups=[AppModule])
147+
user_service = container.resolve(UserService)
148+
```
149+
150+
## Why Choose modern-di?
151+
152+
### 1. Zero Boilerplate
153+
154+
Type annotations auto-wire dependencies - no manual registration needed.
155+
156+
### 2. Scope Management
157+
158+
Hierarchical containers with automatic inheritance:
159+
160+
```python
161+
app_container = Container(groups=[AppModule], scope=Scope.APP)
162+
request_container = app_container.build_child_container(scope=Scope.REQUEST)
163+
164+
# Resolves from correct scope automatically
165+
db_pool = request_container.resolve(DatabasePool) # APP scope
166+
db_session = request_container.resolve(DatabaseSession) # REQUEST scope
167+
```
168+
169+
### 3. Easy Testing
170+
171+
Override any dependency:
172+
173+
```python
174+
@pytest.fixture
175+
def test_container() -> Container:
176+
container = Container(groups=[AppModule])
177+
container.override(AppModule.db, Mock(spec=DatabaseConnection))
178+
return container
179+
```
180+
181+
### 4. Resource Cleanup
182+
183+
Define finalizers for automatic cleanup:
184+
185+
```python
186+
class AppModule(Group):
187+
db_session = providers.Factory(
188+
scope=Scope.REQUEST,
189+
creator=DatabaseSession,
190+
cache_settings=providers.CacheSettings(
191+
finalizer=lambda session: session.close() # ✅ Define cleanup
192+
)
193+
)
194+
195+
# Automatic finalizer execution
196+
request_container = app_container.build_child_container(scope=Scope.REQUEST)
197+
try:
198+
session = request_container.resolve(DatabaseSession)
199+
finally:
200+
request_container.close_sync() # Finalizers called
201+
```
202+
203+
### 5. Framework Integrations
204+
205+
Works with FastAPI, LiteStar, FastStream:
206+
207+
```python
208+
from modern_di_fastapi import FromDI
209+
210+
211+
@app.get("/users/{user_id}")
212+
async def get_user(
213+
user_id: int,
214+
user_service: UserService = FromDI(UserService),
215+
) -> dict:
216+
return {"user": user_service.get_user(user_id)}
217+
```
218+
219+
## Summary
220+
221+
Dependency Injection:
222+
223+
- **Decouples** classes from dependencies
224+
- **Improves testability** with easy mocking
225+
- **Centralizes configuration**
226+
- **Manages lifetimes** automatically
227+
228+
`modern-di` automates DI with minimal boilerplate through type annotations, providing scope management, testing utilities, and framework integrations.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ edit_uri: edit/main/docs/
55
nav:
66
- Quick-Start: index.md
77
- Introduction:
8+
- About DI: introduction/about-di.md
89
- Resolving: introduction/resolving.md
910
- Providers:
1011
- Factories: providers/factories.md

0 commit comments

Comments
 (0)