IoC & DI¶
Cullinan's current dependency model is unified around ApplicationContext and the public cullinan.core facade.
For the hard contract behind discovery, typed binding, refresh(), and compatibility APIs, read Framework Semantics together with this page.
Preferred programming model¶
- register business types with
@serviceand@controller - inject dependencies with bare type annotations — the recommended default:
No
class MyClass: dependency: SomeType # ← bare annotation, no = Inject() optional: OptService = None # ← None means optional__init__is needed. The framework callscls()thensetattrfor each field. Inject()is still available as a fallback / backward-compatibility option- use
InjectByName()when runtime type imports are undesirable - use
Lazy("Name")when lookup should be deferred until first access - use
ApplicationContextdirectly for explicit integration or custom definitions
Example¶
Constructor injection (recommended)¶
from cullinan.web.controller import controller, get_api
from cullinan.core.services import service
@service
class UserService:
def get_user(self, user_id: int):
return {"id": user_id}
@controller(url="/users")
class UserController:
user_service: UserService # ← bare annotation — framework injects
@get_api(url="/{user_id}")
async def get_user(self, user_id: int):
return self.user_service.get_user(user_id)
Using Inject() (backward-compatible)¶
from cullinan.core import Inject
@controller(url="/users")
class UserController:
user_service: UserService = Inject()
# ...
Runtime model¶
- decorators produce container definitions
ApplicationContext.refresh()materializes eager parts of the graph- lifecycle hooks run on managed components
- request-scoped resolution is tied to the active request context
- after injection, all DI-populated attributes are enforced read-only — reassignment raises
AttributeError. UseTestContext.mock()fromcullinan.testingfor testing.
Runtime type resolution rule¶
Inject() is still strict, but it now understands a wider set of typed contracts:
- direct runtime types
TYPE_CHECKINGforward references when they map to one unique targetOptional[T],Annotated[T, ...],Final[T]Provider[T]list[T],set[T],tuple[T, ...]Union[A, B]when exactly one branch is bindable
Cullinan still rejects attribute-name guesses and ambiguous combinations. If the type contract cannot be normalized safely, startup fails with DependencyTypeResolutionError.
Use InjectByName("Name") or Lazy("Name") when you want explicit, name-based control instead.
Compatibility layer¶
Inject() remains available for backward compatibility and special cases, but bare type annotations are the recommended default for new code.
Older constructor-injection helpers still exist, but only as compatibility shims:
injectable— no-op compatibility decoratorinject_constructor— no-op compatibility decoratorget_injection_registry()— returnsNonereset_injection_registry()— safe no-op
New code should not build on those APIs.