API reference¶
Complete API documentation for the hdmi dependency injection framework.
Core classes¶
ContainerBuilder¶
- class hdmi.ContainerBuilder[source]¶
Bases:
objectMutable builder for configuring dependency injection services.
The ContainerBuilder is responsible for: - Accumulating service registrations - Validating the dependency graph when build() is called - Producing an immutable, validated Container
- build() Container[source]¶
Build and validate the Container.
This method: 1. Validates the dependency graph 2. Checks for circular dependencies 3. Validates scope hierarchy 4. Produces an immutable Container
- Returns:
An immutable, validated Container ready for runtime use
- Raises:
CircularDependencyError – If circular dependencies are detected
UnresolvableDependencyError – If a dependency cannot be resolved
ScopeViolationError – If scope hierarchy is violated
- register(service_type: Type, /, *, scoped: bool = False, transient: bool = False, name: str | None = None, factory: Callable[[...], Any] | Callable[[...], Awaitable[Any]] | None = None, autowire: bool = True, initializer: Callable[[Any], None] | Callable[[Any], Awaitable[None]] | None = None, finalizer: Callable[[Any], None] | Callable[[Any], Awaitable[None]] | None = None) None[source]¶
Register a service type with the container.
- Parameters:
service_type – The class to register as a service
scoped – False (default) = available from Container, True = requires ScopedContainer
transient – False (default) = cached, True = new instance per request
name – Optional name for the service
factory – Optional factory function to create the service (sync or async)
autowire – Whether to auto-inject this service into optional dependencies (defaults to True)
initializer – Optional initialization function called after service creation (sync or async)
finalizer – Optional cleanup function called when service is disposed (sync or async)
Container¶
- class hdmi.Container(definitions: dict[Type, ServiceDefinition])[source]¶
Bases:
objectImmutable root container for resolving service instances at runtime.
The Container is produced by ContainerBuilder.build() and is: - Immutable: cannot be modified after creation - Pre-validated: all configuration errors caught during build - Lazy: services instantiated only when first requested via get() - Async: all resolution and lifecycle management is async
Implements IContainer protocol to provide a consistent interface with ScopedContainer.
- __init__(definitions: dict[Type, ServiceDefinition])[source]¶
Initialize Container with validated service definitions.
This should only be called by ContainerBuilder.build().
- Parameters:
definitions – Validated service definitions from builder
- async get(service_type: Type[T]) T[source]¶
Resolve a service instance (lazy instantiation).
- Parameters:
service_type – The service type to resolve
- Returns:
An instance of the service type
- Raises:
UnresolvableDependencyError – If the service type is not registered
ScopeViolationError – If trying to resolve a scoped service outside a scope
- scope() ScopedContainer[source]¶
Create a new scoped container for resolving scoped services.
- Returns:
A new ScopedContainer instance
ServiceDefinition¶
- class hdmi.ServiceDefinition(service_type: Type, /, *, scoped: bool = False, transient: bool = False, name: str | None = None, factory: Callable[[...], Any] | Callable[[...], Awaitable[Any]] | None = None, autowire: bool = True, initializer: Callable[[Any], None] | Callable[[Any], Awaitable[None]] | None = None, finalizer: Callable[[Any], None] | Callable[[Any], Awaitable[None]] | None = None)[source]¶
Bases:
objectDescribes everything to know about a service.
Service lifetime is defined by two boolean flags: - scoped: False (default) = available from Container, True = requires ScopedContainer - transient: False (default) = cached, True = new instance per request
Four service types: 1. Singleton (scoped=False, transient=False): cached in Container 2. Scoped (scoped=True, transient=False): cached in ScopedContainer 3. Transient (scoped=False, transient=True): not cached, no scope required 4. Scoped Transient (scoped=True, transient=True): not cached, requires scope
The ServiceDefinition class describes everything needed to know about a service:
service_type: The type/class to be registered (positional only)
scoped: Boolean flag indicating if the service is scoped (default: False)
transient: Boolean flag indicating if the service is transient (default: False)
name: Optional name for named registrations
factory: Optional factory callable for custom instantiation (sync or async)
autowire: Whether to auto-inject this service into optional dependencies (default: True)
initializer: Optional callback called after service instantiation (sync or async)
finalizer: Optional callback called when container/scope exits (sync or async)
The combination of scoped and transient flags creates four service types:
Singleton (scoped=False, transient=False): One instance per container
Scoped (scoped=True, transient=False): One instance per scope
Transient (scoped=False, transient=True): New instance every time
Scoped Transient (scoped=True, transient=True): New instance every time, requires scope
Example usage:
from hdmi import ServiceDefinition, ContainerBuilder # Basic definition (singleton by default) definition = ServiceDefinition(MyService) # Scoped service scoped_definition = ServiceDefinition(MyService, scoped=True) # Transient service transient_definition = ServiceDefinition(MyService, transient=True) # With custom factory def create_service(): return MyService(custom_param="value") definition = ServiceDefinition( MyService, scoped=True, factory=create_service ) # With lifecycle hooks definition_with_hooks = ServiceDefinition( MyService, initializer=lambda s: s.setup(), # Called after instantiation finalizer=lambda s: s.cleanup() # Called when container exits ) # With async hooks async def async_init(service): await service.connect() async_definition = ServiceDefinition( MyService, initializer=async_init ) # Registration: use the service_type directly with kwargs builder = ContainerBuilder() builder.register(MyService, scoped=True, factory=create_service)
ScopedContainer¶
- class hdmi.ScopedContainer(parent: Container)[source]¶
Bases:
ContainerScoped container for resolving scoped services within a scope context.
ScopedContainer extends Container, following the decorator pattern to delegate to its parent Container for non-scoped services while maintaining its own cache for scoped instances.
Implements IContainer protocol to provide a consistent interface with Container.
- __init__(parent: Container)[source]¶
Initialize ScopedContainer with a parent Container.
- Parameters:
parent – The parent Container to delegate to
- async get(service_type: Type[T]) T[source]¶
Resolve a service instance within the scope.
- Parameters:
service_type – The service type to resolve
- Returns:
An instance of the service type
- Raises:
UnresolvableDependencyError – If the service type is not registered
Protocols¶
IContainer¶
- class hdmi.IContainer(*args, **kwargs)[source]¶
Bases:
ProtocolProtocol defining the interface for dependency injection containers.
This protocol is implemented by both Container (root container) and ScopedContainer (scoped container), ensuring they provide a consistent interface for resolving service instances.
Protocol defining the interface for dependency injection containers.
- get(service_type: Type[T]) T[source]¶
Resolve a service instance.
- Parameters:
service_type – The service type to resolve
- Returns:
An instance of the service type
- Raises:
KeyError – If the service type is not registered
ScopeViolationError – If trying to resolve a scoped service outside a scope (for root container only)
Exceptions¶
HDMIError¶
CircularDependencyError¶
ScopeViolationError¶
- exception hdmi.ScopeViolationError[source]¶
Bases:
HDMIErrorRaised when a service depends on a service with incompatible scope.
The only invalid dependency pattern is when a non-scoped service (singleton or transient) attempts to depend on a scoped service.
Valid patterns: - Any service can depend on singleton services - Any service can depend on transient services - Scoped services can depend on any service type
Invalid patterns: - Singleton (scoped=False) depending on Scoped (scoped=True) - Transient (scoped=False) depending on Scoped (scoped=True)
Note: Transient dependencies are created once during their dependent’s construction and live for the dependent’s lifetime, making them safe dependencies for any service type.
Raised when a service depends on another service with a shorter lifetime.
UnresolvableDependencyError¶
Type definitions¶
Service Types¶
Services are configured using two boolean flags that combine to create four distinct types:
# Singleton (default): scoped=False, transient=False
builder.register(MyService)
# Scoped: scoped=True, transient=False
builder.register(MyService, scoped=True)
# Transient: scoped=False, transient=True
builder.register(MyService, transient=True)
# Scoped Transient: scoped=True, transient=True
builder.register(MyService, scoped=True, transient=True)
The four service types:
Singleton: One instance per container (default)
Scoped: One instance per scope
Transient: New instance every time
Scoped Transient: New instance every time, requires scope context
Public API summary¶
The main hdmi package exports the following:
from hdmi import (
# Core classes
ContainerBuilder,
Container,
ScopedContainer,
ServiceDefinition,
# Protocols
IContainer,
# Exceptions
HDMIError,
CircularDependencyError,
ScopeViolationError,
UnresolvableDependencyError,
)
Usage examples¶
Basic registration¶
import asyncio
from hdmi import ContainerBuilder
async def main():
builder = ContainerBuilder()
builder.register(DatabaseService) # singleton (default)
builder.register(UserRepository, scoped=True) # scoped service
async with builder.build() as container:
# Singleton services can be accessed directly
db = await container.get(DatabaseService)
# Scoped services require a scope context
async with container.scope() as scoped:
repo = await scoped.get(UserRepository)
asyncio.run(main())
Using lifecycle hooks¶
import asyncio
from hdmi import ContainerBuilder
class DatabaseService:
def __init__(self):
self.connected = False
def connect(self):
self.connected = True
def disconnect(self):
self.connected = False
async def main():
builder = ContainerBuilder()
builder.register(
DatabaseService,
initializer=lambda db: db.connect(),
finalizer=lambda db: db.disconnect()
)
async with builder.build() as container:
db = await container.get(DatabaseService)
assert db.connected # initializer was called
# After exiting context, finalizer is called
asyncio.run(main())
See also¶
Architecture deep dive for architectural overview
How-to guides for practical guides
Tutorials for step-by-step tutorials