Manual Registration
archtool auto-discovers and wires the vast majority of dependencies. But some objects can't be auto-discovered: async resources initialised at startup, third-party instances, or conditionally-swapped implementations.
For these, use injector.register() before calling inject().
The pattern
injector = DependencyInjector(modules_list=[...], project_root=ROOT)
# Pre-register manually
injector.register(key=SomeABC, value=some_instance)
# Then wire everything — manual registrations are respected, auto-discovery skips them
injector.inject()
register() parameters:
| Parameter | Description |
|---|---|
key |
Interface class used as the lookup key |
value |
Any instance to register |
inject_into |
When True (default), pass-2 wires this instance's own annotations. Set to False for third-party objects. |
Async resources (database pools, HTTP clients)
archtool's inject() is synchronous — it runs at startup before the event loop starts. Async resources must be created outside inject() and handed in via register().
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
async def create_app():
engine = create_async_engine(DATABASE_URL, echo=False)
session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
injector = DependencyInjector(modules_list=[...], project_root=ROOT)
injector.register(key=async_sessionmaker, value=session_maker, inject_into=False)
injector.inject()
# Any repo annotated `session_maker: async_sessionmaker` gets it automatically
return injector
Config objects
If your services or repos need configuration, wrap it in a simple class and register it:
from dataclasses import dataclass
@dataclass
class AppConfig:
stripe_key: str
s3_bucket: str
class AppConfigABC(ABC): ...
config = AppConfig(
stripe_key=os.environ["STRIPE_KEY"],
s3_bucket=os.environ["S3_BUCKET"],
)
injector.register(key=AppConfigABC, value=config)
injector.inject()
# app/payments/services.py
class PaymentService(PaymentServiceABC):
config: AppConfigABC # archtool wires the pre-registered AppConfig
Test stubs
Pre-registration is the recommended way to swap in test doubles:
# tests/test_order_service.py
def test_place_order():
stub_repo = StubOrderRepo(returns=[...])
injector = DependencyInjector(modules_list=[AppModule("app.orders")], project_root=ROOT)
injector.register(key=OrderRepoABC, value=stub_repo)
injector.inject() # OrderRepo auto-discovery is skipped; stub is used instead
svc = injector.get_dependency(OrderServiceABC)
order = asyncio.run(svc.place(user_id=..., items=[...]))
assert order.status == "pending"
Conditional implementations
Different environments (production vs. staging vs. local) may need different implementations:
import os
if os.getenv("USE_S3") == "1":
storage = S3Storage(bucket=os.environ["S3_BUCKET"])
else:
storage = LocalStorage(base_path="/tmp/files")
injector.register(key=StorageABC, value=storage)
injector.inject()
inject_into=False
By default, archtool's pass-2 injection also runs on manually registered objects — it will wire their own class-level annotations too. Set inject_into=False when registering third-party objects that don't use archtool annotations:
import httpx
http_client = httpx.AsyncClient(base_url=API_URL)
injector.register(key=httpx.AsyncClient, value=http_client, inject_into=False)
Reading the registry
After inject(), injector.dependencies is a plain dict. You can inspect it or retrieve instances directly: