Cullinan Dependency Injection Quick Reference¶
Version: 0.93a13 Author: Cullinan
Quick lookup page: use this page as a compact DI recipe sheet; use Dependency Injection Guide for fuller guidance and API Reference for symbol lookup.
Basic Usage¶
1. Define a Service¶
from cullinan import service
@service
class UserService:
def __init__(self):
self.name = "UserService"
def get_user(self, user_id):
return {"id": user_id, "name": "John"}
2. Inject Service into Controller¶
from cullinan import Inject, InjectByName, Lazy, Provider
from cullinan import controller
@controller(url='/api')
class UserController:
# Method 1: Constructor injection — bare type annotation, zero boilerplate (Recommended)
user_service: UserService
# Method 2: Explicit name
auth_service = InjectByName('AuthService')
# Method 3: Optional dependency — just set to None
notifier: NotifierService = None
# Method 4: Lazy lookup
report_service = Lazy('ReportService')
3. Use Injected Service¶
from cullinan import controller, get_api, Path
@controller(url='/api')
class UserController:
user_service: UserService # constructor injection
@get_api(url='/users/{user_id}')
async def get_user(self, user_id: int = Path()):
user = self.user_service.get_user(user_id)
return user
Which injection primitive to use¶
| Case | Use |
|---|---|
| Simplest, zero boilerplate (Recommended) | db: DatabaseService — constructor injection |
| Type is importable at runtime | Inject() |
TYPE_CHECKING / forward reference still maps to one unique target |
Inject() |
| Type should not be imported at runtime | InjectByName("Name") |
| Lookup should happen on first access | Lazy("Name") |
| Optional dependency | notifier: NotifierService = None or required=False |
| Deferred provider object | Provider[T] = Inject() |
| Multiple implementations of one contract | list[T] = Inject() / set[T] = Inject() / tuple[T, ...] = Inject() |
TYPE_CHECKING rule¶
Inject() now supports TYPE_CHECKING forward references when the binding result is unique:
from typing import TYPE_CHECKING
from cullinan import Inject, Provider
if TYPE_CHECKING:
from .contracts import Hook
from .providers import DatabaseSessionProvider
class Repo:
session_provider: Provider["DatabaseSessionProvider"] = Inject()
hooks: list["Hook"] = Inject(required=False)
Startup still fails fast when:
- the annotation is ambiguous, such as
Union[A, B]with multiple live candidates - the combination is unsupported, such as
list[Union[A, B]] - the framework would need name guessing instead of exact resolution
Packaging Configuration¶
Using an Entry Method (Recommended)¶
from cullinan import application, configure
# user_packages triggers automatic module discovery — no manual
# component imports needed. The framework scans and registers all
# @service / @controller / @component classes under my_app.
@configure(user_packages=["my_app"])
@application
def main(): ...
main()
Use @module only when the application needs an explicit advanced runtime boundary such as package ownership or hot-pluggable module structure.
PyInstaller Configuration¶
# your_app.spec
hiddenimports=[
'my_app.service.user_service',
'my_app.service.auth_service',
],
datas=[
('my_app', 'my_app'),
],
Nuitka Configuration¶
nuitka --include-package=my_app \
--include-module=my_app.service.user_service \
your_app.py
Troubleshooting¶
| Problem | Solution |
|---|---|
| Dependency is None | Ensure service uses @service decorator |
| Service not found | Check service name matches (case-sensitive) |
| Circular dependency or runtime-only import edge | Use InjectByName() or Lazy("Name") |
DependencyTypeResolutionError |
Annotation is ambiguous, unsupported, or cannot be normalized safely; narrow the type contract or use InjectByName() |
| Injection not working | Ensure the component is registered with @service / @controller and available to ApplicationContext |
See Also¶
- Dependency Injection Guide - Complete DI documentation
- Architecture - System architecture overview
- Migration Guide - Migration from older versions