Implementation of dependency injection for Python 3
npx @tessl/cli install tessl/pypi-rodi@2.0.0Implementation of dependency injection for Python 3 that enables automatic resolution of service dependencies through type annotations and class signatures. Rodi provides flexible service registration patterns including singleton, transient, and scoped lifestyles with minimal runtime overhead through code inspection and dynamic function generation.
pip install rodifrom rodi import Container, Services, ActivationScopeProtocol-based interface:
from rodi import ContainerProtocolfrom rodi import Container
# Basic type registration and resolution
class DatabaseService:
def get_data(self):
return "data from database"
class UserService:
def __init__(self, db: DatabaseService):
self.db = db
def get_user(self, user_id: int):
return f"User {user_id}: {self.db.get_data()}"
# Create container and register services
container = Container()
container.register(DatabaseService)
container.register(UserService)
# Resolve services - dependencies are automatically injected
user_service = container.resolve(UserService)
print(user_service.get_user(123))Rodi uses a two-phase approach for efficient dependency injection:
The architecture supports different service lifestyles:
Key components:
Core service registration and container configuration functionality. The Container class provides methods for registering services with different lifestyles and building the service provider.
class Container:
def __init__(self, *, strict: bool = False, scope_cls: Optional[Type[ActivationScope]] = None): ...
def register(self, obj_type, sub_type=None, instance=None, *args, **kwargs) -> Container: ...
def add_singleton(self, base_type: Type, concrete_type: Optional[Type] = None) -> Container: ...
def add_transient(self, base_type: Type, concrete_type: Optional[Type] = None) -> Container: ...
def add_scoped(self, base_type: Type, concrete_type: Optional[Type] = None) -> Container: ...
def build_provider(self) -> Services: ...Service activation and dependency resolution through the Services provider. Provides efficient service lookup and instantiation with support for scoped resolution.
class Services:
def __init__(self, services_map=None, scope_cls: Optional[Type[ActivationScope]] = None): ...
def get(self, desired_type: Union[Type[T], str], scope: Optional[ActivationScope] = None, *, default: Optional[Any] = ...) -> T: ...
def create_scope(self, scoped: Optional[Dict[Union[Type, str], Any]] = None) -> ActivationScope: ...
def exec(self, method: Callable, scoped: Optional[Dict[Type, Any]] = None) -> Any: ...Scoped service management through ActivationScope context managers. Enables per-request or per-operation service scoping with automatic cleanup.
class ActivationScope:
def __init__(self, provider: Optional[Services] = None, scoped_services: Optional[Dict[Union[Type[T], str], T]] = None): ...
def get(self, desired_type: Union[Type[T], str], scope: Optional[ActivationScope] = None, *, default: Optional[Any] = ...) -> T: ...
def dispose(self): ...
def __enter__(self): ...
def __exit__(self, exc_type, exc_val, exc_tb): ...Advanced service registration using factory functions for complex instantiation scenarios. Supports different factory signatures and service lifestyles.
def add_singleton_by_factory(self, factory: FactoryCallableType, return_type: Optional[Type] = None) -> Container: ...
def add_transient_by_factory(self, factory: FactoryCallableType, return_type: Optional[Type] = None) -> Container: ...
def add_scoped_by_factory(self, factory: FactoryCallableType, return_type: Optional[Type] = None) -> Container: ...Convention-based service registration and resolution using string names and aliases instead of type annotations.
def add_alias(self, name: str, desired_type: Type) -> Container: ...
def add_aliases(self, values: AliasesTypeHint) -> Container: ...
def set_alias(self, name: str, desired_type: Type, override: bool = False) -> Container: ...class ContainerProtocol(Protocol):
def register(self, obj_type: Union[Type, str], *args, **kwargs): ...
def resolve(self, obj_type: Union[Type[T], str], *args, **kwargs) -> T: ...
def __contains__(self, item) -> bool: ...
class ServiceLifeStyle(Enum):
TRANSIENT = 1
SCOPED = 2
SINGLETON = 3
AliasesTypeHint = Dict[str, Type]
FactoryCallableNoArguments = Callable[[], Any]
FactoryCallableSingleArgument = Callable[[ActivationScope], Any]
FactoryCallableTwoArguments = Callable[[ActivationScope, Type], Any]
FactoryCallableType = Union[FactoryCallableNoArguments, FactoryCallableSingleArgument, FactoryCallableTwoArguments]def inject(globalsns=None, localns=None) -> Callable[..., Any]:
"""
Marks a class or function as injected. Required for Python >= 3.10 with locals.
Returns:
Decorator function that binds locals/globals to the target
"""
def class_name(input_type) -> str:
"""
Gets the name of a type, handling generic types.
Args:
input_type: Type to get name for
Returns:
String name of the type
"""
def to_standard_param_name(name) -> str:
"""
Convert class names to standard parameter names following Python conventions.
Args:
name: Class name to convert
Returns:
Converted parameter name (e.g., "UserService" -> "user_service")
"""Rodi provides specific exceptions for different error scenarios:
class DIException(Exception): ...
class CannotResolveTypeException(DIException): ...
class CannotResolveParameterException(DIException): ...
class CircularDependencyException(DIException): ...
class OverridingServiceException(DIException): ...
class InvalidOperationInStrictMode(DIException): ...
class AliasAlreadyDefined(DIException): ...
class AliasConfigurationError(DIException): ...
class MissingTypeException(DIException): ...
class InvalidFactory(DIException): ...
class FactoryMissingContextException(DIException): ...