Skip to content

Commit 607157c

Browse files
committed
readme update
1 parent 98b8ae7 commit 607157c

1 file changed

Lines changed: 161 additions & 22 deletions

File tree

readme.md

Lines changed: 161 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ pip install wireup
7979

8080
## Quick Start
8181

82+
Wireup works anywhere, in APIs, CLIs, workers, scripts. Here's what it looks like in FastAPI:
83+
8284
```python
8385
import fastapi
8486
import wireup
@@ -164,7 +166,8 @@ container = wireup.create_sync_container(injectables=[make_settings, make_databa
164166

165167
**3. Package-level registration**
166168

167-
No need to list every injectable manually. Provide entire modules or packages to register all at once.
169+
Provide entire modules or packages to register all at once, or export explicit injectable lists from each package when
170+
you want a more visible composition root in larger applications.
168171

169172
```python
170173
import app
@@ -181,34 +184,92 @@ container = wireup.create_sync_container(
181184

182185
## More Features
183186

184-
### 🎯 Function Injection
185187

186-
Inject dependencies into CLI commands, background tasks, event handlers, or any standalone function that needs container access.
188+
### 🔑 The `@injectable` API
189+
190+
Instead of separate registration APIs for services, factories, resources, and async resources, Wireup's `@injectable` API uses standard Python constructs to determine how a dependency behaves.
187191

188192
```python
189-
@inject_from_container(container)
190-
def migrate_database(db: Injected[Database], settings: Injected[Settings]) -> None:
191-
...
192-
```
193+
# Class → dependency
194+
@injectable
195+
class UserService: ...
193196

194-
### 📝 Interfaces & Abstractions
197+
# Dataclass → dependency (auto-generated __init__)
198+
@injectable
199+
@dataclass
200+
class OrderProcessor:
201+
payment_gateway: PaymentGateway
202+
inventory_service: InventoryService
195203

196-
Bind implementations to interfaces using Protocols or ABCs.
204+
# Function → factory
205+
@injectable
206+
def make_client() -> Client:
207+
return Client()
208+
209+
# Generator → resource with cleanup
210+
@injectable
211+
def database() -> Iterator[Database]:
212+
db = Database()
213+
try:
214+
yield db
215+
finally:
216+
db.close()
217+
218+
# Async generator → async resource with cleanup
219+
@injectable
220+
async def database() -> AsyncIterator[Database]:
221+
async with Database() as db:
222+
yield db
223+
```
224+
225+
These modifiers compose. A request-scoped async resource with cleanup is just:
197226

198227
```python
199-
class Notifier(Protocol):
200-
def notify(self) -> None: ...
228+
@injectable(lifetime="scoped") # Scoped lifetime.
229+
# async def → async dependency
230+
async def make_foo() -> AsyncIterator[Foo]:
231+
async with Foo() as foo:
232+
yield foo # yield → cleanup
233+
```
201234

202-
@injectable(as_type=Notifier)
203-
class SlackNotifier:
204-
def notify(self) -> None: ...
235+
No separate provider type needed. Change the function signature and the registration evolves with it. Learn one API and use it everywhere.
205236

206-
# SlackNotifier is injected wherever Notifier is requested
207-
@app.post("/notify")
208-
def send_notification(notifier: Injected[Notifier]) -> None:
209-
notifier.notify()
237+
238+
### 🧰 Advanced Wiring
239+
240+
Wireup keeps the API small, but it is built for larger application graphs.
241+
242+
| Need | Wireup |
243+
| --- | --- |
244+
| One shared instance | [`@injectable`](https://maldoinc.github.io/wireup/latest/injectables/) / `lifetime="singleton"` |
245+
| Per request, job, command, handler, or WebSocket | [`@injectable(lifetime="scoped")`](https://maldoinc.github.io/wireup/latest/lifetimes_and_scopes/) |
246+
| Fresh instance each resolution | [`@injectable(lifetime="transient")`](https://maldoinc.github.io/wireup/latest/lifetimes_and_scopes/) |
247+
| Setup and cleanup | [`yield` from a sync or async factory](https://maldoinc.github.io/wireup/latest/resources/) |
248+
| Configuration | Both [`Inject(config=...)`](https://maldoinc.github.io/wireup/latest/configuration/) values and injectable settings objects |
249+
| Register an already-created object | [`wireup.instance(...)`](https://maldoinc.github.io/wireup/latest/injectables/) |
250+
| Dynamic function injection and injectable lookup | Inject the [root or active scoped container](https://maldoinc.github.io/wireup/latest/container/#injecting-the-container) |
251+
| Interfaces and protocols | [`as_type=...`](https://maldoinc.github.io/wireup/latest/interfaces/) or factory return annotations |
252+
| Multiple implementations | [Qualifiers](https://maldoinc.github.io/wireup/latest/interfaces/) |
253+
| All implementations | [`Sequence[T]` or `Mapping[Hashable, T]`](https://maldoinc.github.io/wireup/latest/interfaces/#collection-injection) |
254+
| Isolated scopes with explicit context sharing (batch jobs, fan-out tasks, multi-tenant processing) | [`container.enter_scope({...})`](https://maldoinc.github.io/wireup/latest/lifetimes_and_scopes/#sharing-context-across-scopes) |
255+
| Environment-specific graph | [Conditional registration](https://maldoinc.github.io/wireup/latest/conditional_registration/) with normal Python |
256+
| Generic repositories/services | [Generic dependencies](https://maldoinc.github.io/wireup/latest/generic_dependencies/) |
257+
| Modular or parametrized registration | [Functions that return injectables](https://maldoinc.github.io/wireup/latest/reusable_bundles/) |
258+
| Optional or conditional dependencies | [Params with defaults are skipped when unregistered; factories can return `T \| None`](https://maldoinc.github.io/wireup/latest/injectables/#optional-dependencies-and-default-values) |
259+
260+
261+
262+
### 🎯 Function Injection
263+
264+
Inject dependencies into CLI commands, background tasks, event handlers, or any standalone function that needs container access.
265+
266+
```python
267+
@inject_from_container(container)
268+
def migrate_database(db: Injected[Database], settings: Injected[Settings]) -> None:
269+
...
210270
```
211271

272+
212273
### 🏭 Factories & Resources
213274

214275
Defer instantiation to specialized factories when complex initialization or cleanup is required.
@@ -244,21 +305,23 @@ async def weather_client_factory() -> AsyncIterator[WeatherClient]:
244305

245306
### 🔄 Lifetimes & Scopes
246307

247-
Declare dependencies as `singleton`, `scoped`, or `transient` to control reuse explicitly.
308+
Wireup has three lifetimes: `singleton`, `scoped`, and `transient`, plus explicit scope forking from root for unit-of-work patterns like batch jobs, fan-out tasks, and multi-tenant request processing.
248309

249310
```python
250311
# Singleton: one instance per application (default)
251312
@injectable
252313
class Settings:
253314
pass
254315

255-
# Async singleton with cleanup — no lru_cache, no app.state
316+
# Async singleton with cleanup
256317
@injectable
257318
async def database_factory(settings: Settings) -> AsyncIterator[AsyncConnection]:
258319
async with create_async_engine(settings.db_url).connect() as connection:
259320
yield connection
260321

261-
# Scoped: one instance per request, shared within that request
322+
# Scoped: one instance for the duration of the current scope.
323+
# In a web request, that's the request. In a WebSocket, the connection.
324+
# In a CLI command, the command lifetime. In a worker job, the job.
262325
@injectable(lifetime="scoped")
263326
class RequestContext:
264327
def __init__(self) -> None:
@@ -270,6 +333,81 @@ class OrderProcessor:
270333
pass
271334
```
272335

336+
Wireup enforces lifetime rules at startup to prevent scope leakage, such as an application-wide singleton accidentally
337+
holding request-scoped state.
338+
339+
340+
### 📝 Interfaces & Abstractions
341+
342+
Bind implementations to interfaces using Protocols or ABCs.
343+
344+
```python
345+
class Notifier(Protocol):
346+
def notify(self) -> None: ...
347+
348+
@injectable(as_type=Notifier)
349+
class SlackNotifier:
350+
def notify(self) -> None: ...
351+
352+
# SlackNotifier is injected wherever Notifier is requested
353+
@app.post("/notify")
354+
def send_notification(notifier: Injected[Notifier]) -> None:
355+
notifier.notify()
356+
```
357+
358+
When multiple implementations exist, distinguish them with qualifiers:
359+
360+
```python
361+
@injectable()
362+
def primary_database(settings: Settings) -> Database:
363+
return Database(url=settings.db.primary_dsn)
364+
365+
@injectable(qualifier="readonly")
366+
def readonly_database(settings: Settings) -> Database:
367+
return Database(url=settings.db.replica_dsn)
368+
369+
# Primary (no qualifier) and replica (qualifier) in one service
370+
@injectable
371+
class ReportService:
372+
def __init__(
373+
self,
374+
db: Database,
375+
replica: Annotated[Database, Inject(qualifier="readonly")],
376+
) -> None:
377+
self.db = db
378+
self.replica = replica
379+
```
380+
381+
See [Interfaces & Qualifiers](https://maldoinc.github.io/wireup/latest/interfaces/) for collection injection (`Sequence[T]`, `Mapping[K, T]`) and more.
382+
383+
### 🔀 Fan-out & Isolated Worker Scopes
384+
385+
Wireup uses **isolated scopes with explicit context sharing** rather than nested scope inheritance. Each worker scope gets exactly the context it needs without implicit leakage of the parent's full graph.
386+
387+
The container is injectable too, so you never need a global reference or `app.state` to create child scopes.
388+
389+
```python
390+
@app.post("/batch")
391+
async def process_batch(
392+
doc_ids: list[str],
393+
container: Injected[wireup.AsyncContainer],
394+
ctx: Injected[TenantContext],
395+
) -> list[Result]:
396+
# TenantContext is shared while everything else is isolated per worker.
397+
async def process_one(doc_id: str) -> Result:
398+
async with container.enter_scope({TenantContext: ctx}) as scope:
399+
# DocumentService and all its dependencies (db connections, transactions, etc.)
400+
# are isolated per worker
401+
document_service = await scope.get(DocumentService)
402+
return await process_document(document_service, doc_id)
403+
404+
return await asyncio.gather(*[process_one(doc_id) for doc_id in doc_ids])
405+
```
406+
407+
Because child scopes don't silently inherit the parent graph, parallel workers can never accidentally share or corrupt each other's state.
408+
409+
See [Lifetimes & Scopes](https://maldoinc.github.io/wireup/latest/lifetimes_and_scopes/) for the full model.
410+
273411
### 🛡️ Startup Validation
274412

275413
Wireup validates the dependency graph when the container is created. See <a href="https://maldoinc.github.io/wireup/latest/what_wireup_validates/">What Wireup Validates</a> for the full rules and limits.
@@ -299,10 +437,11 @@ with container.override.injectable(target=Database, new=in_memory_database):
299437
response = client.get("/users")
300438
```
301439

440+
302441
## 📚 Documentation
303442

304443
See the docs for integrations, lifetimes, factories, testing, and more advanced patterns.
305444

306445
[https://maldoinc.github.io/wireup](https://maldoinc.github.io/wireup)
307446

308-
If Wireup is useful to you, a star on GitHub helps others find it.
447+
If Wireup is useful to you, a star on GitHub helps others find it.

0 commit comments

Comments
 (0)