API reference

Complete API documentation for the hdmi dependency injection framework.

Core classes

ContainerBuilder

class hdmi.ContainerBuilder[source]

Bases: object

Mutable 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:
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: object

Immutable 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:
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: object

Describes 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: Container

Scoped 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: Protocol

Protocol 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

exception hdmi.HDMIError[source]

Bases: Exception

Base exception for all hdmi errors.

Base exception for all hdmi-related errors.

CircularDependencyError

exception hdmi.CircularDependencyError[source]

Bases: HDMIError

Raised when circular dependencies are detected.

Raised when a circular dependency is detected during container build.

ScopeViolationError

exception hdmi.ScopeViolationError[source]

Bases: HDMIError

Raised 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

exception hdmi.UnresolvableDependencyError[source]

Bases: HDMIError, KeyError

Raised when a required dependency cannot be resolved.

This exception extends both HDMIError and KeyError for compatibility with code that catches KeyError.

Raised when a required dependency cannot be resolved.

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