CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-dishka

Cute DI framework with scopes and agreeable API for Python dependency injection

Pending
Overview
Eval results
Files

scope-lifecycle.mddocs/

Scope and Lifecycle Management

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.

Capabilities

Built-in Scopes

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 Scope Class

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)  # True

Custom Scope Creation

Function 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")

Scope Hierarchy

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 → STEP

Lifecycle Management:

  1. APP Scope: Created when container starts, closed when container closes
  2. REQUEST Scope: Created per request/operation, automatically closed
  3. ACTION Scope: Created for specific actions within requests
  4. STEP Scope: Created for individual processing steps

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 here

Lifecycle Events

Scopes 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 exits

Scope Validation

Scope validation ensures dependencies are properly organized and accessible.

Rules:

  • Dependencies can only depend on same-scope or parent-scope dependencies
  • Child scopes have access to parent scope dependencies
  • Parent scopes cannot access child scope dependencies

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 → REQUEST

Example 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 → REQUEST

Scope Context Variables

Context 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

docs

component-system.md

container-management.md

framework-integrations.md

index.md

provider-system.md

scope-lifecycle.md

type-markers.md

validation-configuration.md

tile.json