Decorators API Reference
The decorators module provides the main interface for dependency injection through Python decorators.
Lifecycle Decorators
@singleton
Decorates a class to be registered as a singleton dependency.
Signature:
Parameters:
- cls (Type): The class to register as singleton
Returns: - Type: The decorated class with patched constructor
Example:
from nexusdi import singleton
@singleton
class DatabaseService:
def __init__(self):
self.connection = "database_connection"
def query(self, sql: str):
return f"Result: {sql}"
# Usage - same instance returned every time
service1 = _container.resolve(DatabaseService)
service2 = _container.resolve(DatabaseService)
assert service1 is service2 # True
@transient
Decorates a class to be registered as a transient dependency.
Signature:
Parameters:
- cls (Type): The class to register as transient
Returns: - Type: The decorated class with patched constructor
Example:
from nexusdi import transient
@transient
class EmailSender:
def __init__(self, config: ConfigService):
self.config = config
self.sent_count = 0
def send_email(self, to: str, message: str):
self.sent_count += 1
print(f"Sending email #{self.sent_count}")
# Usage - new instance each time
sender1 = _container.resolve(EmailSender)
sender2 = _container.resolve(EmailSender)
assert sender1 is not sender2 # True
@scoped
Decorates a class to be registered as a scoped dependency.
Signature:
Parameters:
- cls (Type): The class to register as scoped
Returns: - Type: The decorated class with patched constructor
Example:
from nexusdi import scoped, scope_cleanup
@scoped
class RequestContext:
def __init__(self):
self.request_id = None
self.user_data = {}
def set_request_id(self, request_id: str):
self.request_id = request_id
# Usage - same instance within scope
context1 = _container.resolve(RequestContext)
context2 = _container.resolve(RequestContext)
assert context1 is context2 # True - same scope
scope_cleanup() # End scope
context3 = _container.resolve(RequestContext)
assert context1 is not context3 # True - new scope
Binding Decorators
These decorators register classes without modifying their constructors, useful for third-party classes.
@bind_singleton
Binds a class as singleton without patching its constructor.
Signature:
Example:
from nexusdi import bind_singleton
# Third-party class you can't modify
class ExternalService:
def __init__(self, api_key: str):
self.api_key = api_key
# Bind it as singleton
bind_singleton(ExternalService)
# Manual instantiation required
external = ExternalService("my-api-key")
@bind_transient
Binds a class as transient without patching its constructor.
Signature:
@bind_scoped
Binds a class as scoped without patching its constructor.
Signature:
Injection Decorators
@inject
Decorates a function to inject dependencies into its parameters.
Signature:
Parameters:
- func (Callable): The function to decorate
Returns: - Callable: The decorated function with dependency injection
Example:
from nexusdi import inject
@inject
def process_data(
data_processor: DataProcessor,
logger: LoggingService,
data: str
) -> str:
logger.info("Processing data")
return data_processor.process(data)
# Usage - dependencies automatically injected
result = process_data(data="my_data")
Features: - Resolves dependencies based on parameter names and type hints - Preserves function signature and annotations - Handles missing dependencies gracefully - Supports mixed injected and regular parameters
@service
Semantic alias for @transient. Decorates a class as a service.
Signature:
Example:
from nexusdi import service
@service
class UserService:
def __init__(self, user_repo: UserRepository):
self.user_repo = user_repo
def get_user(self, user_id: int):
return self.user_repo.find_by_id(user_id)
# Equivalent to @transient
@controller
Semantic alias for @transient. Decorates a class as a controller.
Signature:
Example:
from nexusdi import controller
@controller
class UserController:
def __init__(self, user_service: UserService):
self.user_service = user_service
def get_user_endpoint(self, user_id: int):
return {"user": self.user_service.get_user(user_id)}
# Equivalent to @transient
Implementation Details
Constructor Patching
The lifecycle decorators (@singleton, @transient, @scoped) patch the class constructor to enable dependency injection:
# Before patching
class MyService:
def __init__(self, dependency: SomeDependency):
self.dependency = dependency
# After patching with @singleton
class MyService:
def __init__(self, dependency: SomeDependency = <LazyProxy>):
# Dependencies are automatically resolved
self.dependency = dependency
# Additional attributes added:
# - _nexusdi_original_init: Original constructor
# - _nexusdi_lifecycle: Lifecycle type
# - _nexusdi_container_resolve_init: Container resolution method
Dependency Resolution Strategy
The decorators use a multi-step resolution strategy:
- Type Hints: Primary method using
__annotations__ - Parameter Names: Fallback matching by parameter name
- Lazy Proxies: For circular dependencies
- Graceful Fallback: Falls back to original constructor if injection fails
Error Handling
from nexusdi.exceptions import DependencyResolutionException, LifecycleException
@inject
def my_function(missing_service: UnregisteredService):
return "This will raise DependencyResolutionException"
try:
result = my_function()
except DependencyResolutionException as e:
print(f"Failed to resolve: {e.dependency_name}")
Advanced Usage
Multiple Decorators
from nexusdi import singleton, inject
@singleton
class CacheService:
def __init__(self):
self.cache = {}
@inject
def cached_operation(cache: CacheService, key: str, value: str):
cache.cache[key] = value
return f"Cached {key}={value}"
Custom Parameter Names
@transient
class OrderService:
def __init__(self, user_service, email_service):
# Resolved by parameter name matching
self.user_service = user_service # Matches UserService
self.email_service = email_service # Matches EmailService
Circular Dependencies
@singleton
class ServiceA:
def __init__(self, service_b: 'ServiceB'):
self.service_b = service_b # LazyProxy initially
@singleton
class ServiceB:
def __init__(self, service_a: ServiceA):
self.service_a = service_a # Circular dependency resolved
# Both services can be resolved successfully
a = _container.resolve(ServiceA)
b = _container.resolve(ServiceB)
Testing Decorators
import pytest
from unittest.mock import Mock
from nexusdi import singleton, inject
@singleton
class TestService:
def __init__(self):
self.value = "original"
@inject
def test_function(service: TestService):
return service.value
def test_dependency_injection():
# Test that injection works
result = test_function()
assert result == "original"
def test_with_mocked_dependency(mocker):
# Mock the dependency
mock_service = Mock()
mock_service.value = "mocked"
# Replace in container for testing
_container.register(TestService, lambda: mock_service)
result = test_function()
assert result == "mocked"
Performance Notes
- Decorator Overhead: Minimal runtime overhead after first resolution
- Signature Preservation: Function signatures and annotations are preserved
- Lazy Resolution: Dependencies resolved only when accessed
- Caching: Singleton instances cached for performance
Best Practices
- Use Type Hints: Always provide type hints for reliable resolution
- Choose Appropriate Lifecycle: Match lifecycle to use case
- Avoid Heavy Constructor Logic: Keep constructors light
- Test with Mocks: Use dependency injection for better testing
- Handle Circular Dependencies: Use forward references when needed