Cute DI framework with scopes and agreeable API for Python dependency injection
—
Hierarchical scope system for managing dependency lifetimes from application-wide to per-request or custom granular levels. Scopes provide automatic cleanup and resource management for dependencies with different lifecycle requirements.
Standard scope hierarchy for common application patterns with predefined lifecycle management.
class Scope(BaseScope):
"""Built-in scope hierarchy for dependency lifecycle management"""
RUNTIME: ClassVar[Scope]
"""Runtime scope - typically skipped in hierarchy traversal"""
APP: ClassVar[Scope]
"""Application scope - dependencies live for entire application lifetime"""
SESSION: ClassVar[Scope]
"""Session scope - typically skipped, for user session lifetime"""
REQUEST: ClassVar[Scope]
"""Request scope - dependencies live for single request/operation"""
ACTION: ClassVar[Scope]
"""Action scope - finer granularity within request processing"""
STEP: ClassVar[Scope]
"""Step scope - finest granularity for individual processing steps"""Usage Example:
from dishka import Provider, Scope
# Use built-in scopes
provider = Provider()
# App-scoped dependencies (created once)
provider.provide(DatabaseConnection, scope=Scope.APP)
provider.provide(ConfigService, scope=Scope.APP)
# Request-scoped dependencies (per request)
provider.provide(UserService, scope=Scope.REQUEST)
provider.provide(RequestLogger, scope=Scope.REQUEST)
# Action-scoped dependencies (finer granularity)
provider.provide(ActionProcessor, scope=Scope.ACTION)Base class for creating custom scopes with configurable behavior.
class BaseScope:
"""Base class for dependency scopes"""
name: str
"""Name identifier for the scope"""
skip: bool
"""Whether this scope should be skipped in hierarchy traversal"""
def __init__(self, name: str, skip: bool = False): ...
def __str__(self) -> str:
"""String representation of the scope"""
def __repr__(self) -> str:
"""Debug representation of the scope"""
def __eq__(self, other: object) -> bool:
"""Compare scopes for equality"""
def __hash__(self) -> int:
"""Hash code for scope (allows use as dict key)"""Usage Example:
# Access scope properties
app_scope = Scope.APP
print(app_scope.name) # "APP"
print(app_scope.skip) # False
request_scope = Scope.REQUEST
print(request_scope.name) # "REQUEST"
print(request_scope.skip) # False
# Session scope is typically skipped
session_scope = Scope.SESSION
print(session_scope.skip) # TrueFunction for creating custom scope values with specific behavior.
def new_scope(value: str, *, skip: bool = False) -> BaseScope:
"""
Create a new custom scope.
Parameters:
- value: Name identifier for the scope
- skip: Whether this scope should be skipped in hierarchy traversal
Returns:
New BaseScope instance with the specified configuration
"""Usage Example:
from dishka import new_scope
# Create custom scopes
BATCH_SCOPE = new_scope("BATCH")
WORKER_SCOPE = new_scope("WORKER")
TEMPORARY_SCOPE = new_scope("TEMP", skip=True)
# Use custom scopes
provider = Provider()
provider.provide(BatchProcessor, scope=BATCH_SCOPE)
provider.provide(WorkerService, scope=WORKER_SCOPE)
# Create custom scope hierarchy
class CustomScope(BaseScope):
SYSTEM = new_scope("SYSTEM")
MODULE = new_scope("MODULE")
COMPONENT = new_scope("COMPONENT")The scope hierarchy determines the order of dependency resolution and cleanup. Dependencies in parent scopes are available to child scopes, and cleanup happens in reverse order.
Standard Hierarchy:
RUNTIME (skip=True) → APP → SESSION (skip=True) → REQUEST → ACTION → STEPLifecycle Management:
Usage Example:
from dishka import make_container, Provider, Scope
# Set up providers with different scopes
provider = Provider()
# APP scope - singleton for entire application
provider.provide(DatabasePool, scope=Scope.APP)
# REQUEST scope - new instance per request
provider.provide(UserService, scope=Scope.REQUEST)
# ACTION scope - new instance per action
provider.provide(ActionLogger, scope=Scope.ACTION)
container = make_container(provider)
# APP-scoped dependencies available immediately
db_pool = container.get(DatabasePool)
# Enter REQUEST scope
with container() as request_container:
# REQUEST and APP scoped dependencies available
user_service = request_container.get(UserService)
db_pool_same = request_container.get(DatabasePool) # Same instance
# Enter ACTION scope
with request_container() as action_container:
# All scopes available
action_logger = action_container.get(ActionLogger)
user_service_same = action_container.get(UserService) # Same instance
# ACTION scope cleaned up here
# REQUEST scope cleaned up here
container.close() # APP scope cleaned up hereScopes support automatic resource cleanup through context managers and finalizers.
Context Manager Support:
from contextlib import contextmanager
from dishka import provide, Scope
@provide(scope=Scope.REQUEST)
@contextmanager
def database_connection():
"""Dependency with automatic cleanup"""
conn = create_connection()
try:
yield conn
finally:
conn.close() # Automatically called when scope exits
@provide(scope=Scope.REQUEST)
def create_service(db_conn):
"""Service using managed connection"""
return ServiceClass(db_conn)Async Context Manager Support:
from contextlib import asynccontextmanager
from dishka import provide, Scope
@provide(scope=Scope.REQUEST)
@asynccontextmanager
async def async_database_connection():
"""Async dependency with automatic cleanup"""
conn = await create_async_connection()
try:
yield conn
finally:
await conn.close() # Automatically called when scope exitsScope validation ensures dependencies are properly organized and accessible.
Rules:
Example of Valid Dependencies:
# Valid: REQUEST scope can depend on APP scope
@provide(scope=Scope.APP)
def config() -> Config: ...
@provide(scope=Scope.REQUEST)
def service(cfg: Config) -> Service: ... # Valid: REQUEST → APP
# Valid: Same scope dependencies
@provide(scope=Scope.REQUEST)
def logger() -> Logger: ...
@provide(scope=Scope.REQUEST)
def processor(log: Logger) -> Processor: ... # Valid: REQUEST → REQUESTExample of Invalid Dependencies:
# Invalid: APP scope cannot depend on REQUEST scope
@provide(scope=Scope.REQUEST)
def user_context() -> UserContext: ...
@provide(scope=Scope.APP)
def global_service(ctx: UserContext) -> GlobalService: ... # Invalid: APP → REQUESTContext variables provide request-specific data that flows through scope hierarchies.
# Register context variables for scopes
from dishka import from_context, Scope
# Request-specific context
from_context(RequestID, scope=Scope.REQUEST)
from_context(UserID, scope=Scope.REQUEST)
from_context(str, scope=Scope.REQUEST) # Generic string from context
# Action-specific context
from_context(ActionID, scope=Scope.ACTION)
from_context(ActionType, scope=Scope.ACTION)Usage with Context:
# Set context when entering scopes
container = make_container(provider)
# Enter REQUEST scope with context
context = {
DependencyKey(RequestID, None): RequestID("req-123"),
DependencyKey(UserID, None): UserID("user-456"),
}
with container(context=context) as request_container:
# Context variables available as dependencies
request_id = request_container.get(RequestID) # "req-123"
user_id = request_container.get(UserID) # "user-456"Install with Tessl CLI
npx tessl i tessl/pypi-dishka