dishka

repository·develop·Indexed 22 days ago

https://github.com/reagento/dishka

A lightweight and fast Dependency Injection (DI) framework for Python. It features a robust scoping system (APP, REQUEST, ACTION, STEP) to manage object lifecycles, Providers for defining dependencies via classes or decorators, and Containers for dependency retrieval. It supports advanced features such as collecting multiple objects of the same type, isolated components for provider grouping, and integration with ASGI frameworks like FastAPI via LifespanManager.

Tokens
41.3K
Snippets
106
Records
163
Agent score
78%

What's inside dishka

  1. Overview of dishka DI framework

    develop

    dishka is a dependency injection (DI) framework designed to manage object lifespans through a flexible scoping system. It provides an Inversion of Control (IoC) container that focuses exclusively on DI without requiring custom markers or decorators in your business logic.

    Key capabilities include:

    • Custom Scopes: Define object lifespans for the entire application, specific requests, or any arbitrary level in between.
    • Finalization: Built-in support for releasing dependencies (e.g., closing database connections) when their scope ends.
    • Modular Providers: Organize factories into small, reusable classes rather than large monolithic functions.
    • Clean Code: Dependencies are resolved without cluttering code with global variables or library-specific specifiers.
    • Performance: Optimized for speed, often outperforming other DI alternatives.
  2. Compare dishka with other IoC containers

    develop

    Use the following comparison table to evaluate dishka against other Python IoC (Inversion of Control) libraries based on key requirements like scope management, async support, and autowiring.

    LibraryScopes (Request + Additional)Async SupportFinalizationConcurrency-safeAutowiringContext DataZero-globals
    dishka✅✅✅✅
    di✅✅
    FastAPI Depends✅❌
    dependency-injector
    injector✅✅
    svcs
    rodi✅❌
    punq✅❌
    lagom

    Feature Definitions:

    • Scopes: Ability to cache dependencies per-scope (e.g., per-request or custom additional scopes).
    • Async support: Support for async functions as factories.
    • Finalization: Automatic cleanup of dependencies (including sub-dependencies) upon scope exit.
    • Concurrency-safe: Thread-safety and async task-safety for singletons and scope caches.
    • Autowiring: Automatic registration of classes based on __init__ type hints without manual binding. Double tick (✅✅) indicates support for isolated sub-graphs (components).
    • Context data: Passing additional data (like an HTTP request) to factories after container creation.
    • Zero-globals: True DI where dependencies are not retrieved via hidden global state.
  3. What is a Provider and how to use it

    develop

    A Provider is an object used to define how dependencies are constructed. It contains factories and other entities that are then used to create a Container. Providers allow for modularity, as you can combine multiple providers in a single application.

    To use a Provider, you can either:

    1. Create an instance and use its .provide() method.
    2. Inherit from Provider and use decorators or class attributes.

    Once a Provider is configured, pass it to make_container() to create the dependency container.

    from dishka import make_container, Provider, Scope
    
    # Instance-based configuration
    provider = Provider(scope=Scope.APP)
    provider.provide(some_factory_function)
    container = make_container(provider)
  4. What is an IoC-container?

    develop

    An Inversion of Control (IoC) container is a specialized object or framework that automates the creation of dependencies following specific rules and manages their lifetime (scopes).

    While you can perform DI manually (e.g., by writing a custom Container class), an IoC-container is a useful helper for large-scale applications to:

    • Automate complex factory logic.
    • Ensure thread-safety in concurrent applications.
    • Enforce lifetime rules (e.g., ensuring a database connection is unique per request but shared within that request).

    dishka is an implementation of an IoC-container designed to handle these requirements.

  5. Understand Dependency and Scope in dishka

    develop

    In dishka, a Dependency is an object required by another object (the dependant). To follow dependency injection principles, objects should receive their dependencies (e.g., via __init__) rather than requesting them themselves.

    Scope defines the lifespan of a dependency. Dependencies are lazy (created on first request) and are kept alive until their scope is exited. When a scope is exited, dependencies are finalized in reverse creation order.

    Standard Scope Hierarchy

    Scopes follow a nested hierarchy. You must enter them sequentially: APP $\rightarrow$ REQUEST $\rightarrow$ ACTION $\rightarrow$ STEP

    • Scope.APP: Ideal for singletons and lazy initialization of application-wide objects.
    • Scope.REQUEST: Ideal for processing events like HTTP requests or messenger updates.

    Scope Rules

    • Directional Dependency: A dependency can depend on objects from its own scope or any previous (outer) scope. For example, a REQUEST-scoped object can depend on an APP-scoped object, but an APP-scoped object cannot depend on a REQUEST-scoped object.
    • Custom Scopes: You can define your own Scope class if the standard flow does not meet your needs.
    class Service:
        def __init__(self, client: Client):
            self.client = client
  6. Understand Container Modularity and Assembly

    develop

    Dishka supports modular dependency injection through several mechanisms:

    • Reusable Parts: Containers can be assembled from reusable parts at runtime within a local scope.
    • Isolation: Different parts of a container can be isolated so they do not affect each other, while still providing an explicit API to interact if required.
    • Multiple Containers: It is possible to maintain multiple containers within the same codebase for different purposes.
  7. Understand Concurrency and Async Support

    develop

    Dishka is designed to work with both multithreading and asyncio environments.

    • Async Support: If the container is configured to run in an asyncio environment, dependency creation using async functions is supported.
    • Concurrency Models: The type of concurrency model can be configured during container creation. This ensures that concurrent entrance of scopes does not violate the requirement of providing a single instance of a dependency.
    • Performance Tuning: Users can switch synchronization on or off to tune performance based on their specific concurrency needs.
  8. Use RUNTIME and SESSION scopes for specific use cases

    develop

    While you can use standard scopes, two specific scopes are often used for advanced patterns:

    • RUNTIME scope: Useful for dependencies that need to persist between tests that recreate applications. You can enter this scope explicitly by passing start_scope=Scope.RUNTIME to make_container.
    • SESSION scope: Useful for managing connection-related objects in long-lived connections like WebSockets, where the HTTP-request handler would otherwise go straight into the shorter-lived REQUEST scope.
    # Explicitly starting at RUNTIME
    container = make_container(provider, start_scope=Scope.RUNTIME)
    with container() as app_container:
        # RUNTIME -> APP
        pass
  9. Understand Dependency Injection (DI) patterns

    develop

    Dependency Injection is a design pattern where an object receives its dependencies from an external source rather than creating them itself. This improves testability (by allowing mocks), reusability, and configuration management.

    There are three primary ways to implement DI:

    1. Parameter injection: Passing the dependency as an argument to a method.
    2. Constructor injection: Passing the dependency during object instantiation. This is the primary and most recommended way to perform DI.
    3. Attribute injection: Assigning the dependency to an attribute on an already constructed object. This is often used to break circular references or modify existing objects.

    Anti-patterns to avoid:

    • Global variables: Limits you to a single instance and makes lifecycle control difficult.
    • Singletons: Similar to global variables; they add laziness but suffer from the same lifecycle and testing issues.
    • Monkey patching: Replacing behavior via mock.patch() relies on implementation details rather than interfaces, making tests fragile.
    # Constructor injection (Recommended)
    class Service:
        def __init__(self, client: Client):
            self.client = client
    
        def action(self):
            self.client.get_data()
    
    token = os.getenv("TOKEN")
    client = Client(token)
    service = Service(client)
    service.action()
  10. How to use Providers to define dependencies

    develop

    Providers are collections of factories used to set up your objects. You can define dependencies in several ways:

    1. Using the Provider class: You can use .provide() to register classes or specific implementations.
    2. Using the @provide decorator: You can decorate methods within a Provider subclass. The method's type hints determine what it creates and what dependencies it requires. All method parameters are treated as dependencies.
    3. Custom Factory with Finalization: Using a generator (yield) within a @provide method allows you to perform cleanup (e.g., closing a database connection) when the scope is exited.

    Example of overriding a provider's default scope:

    service_provider = Provider(scope=Scope.REQUEST)
    service_provider.provide(Service)
    service_provider.provide(APIClient, scope=Scope.APP)  # APIClient will live for the APP lifetime instead
  11. Understand Components and Provider Isolation

    develop

    Components are isolated groups of providers within a single container, identified by a string name. This allows you to provide the same type with different meanings (e.g., two different database connections) without needing to create unique types like NewType for each.

    Key characteristics:

    • There is always a default component (represented by an empty string "").
    • Providers are isolated: a provider cannot implicitly request an object from a different component.
    • To retrieve an object from a specific component, you must explicitly specify the component name when calling container.get().

    If a type is provided in multiple components, dishka searches only within the same component as the dependent, unless a cross-component link is explicitly declared.

  12. Use Components to isolate groups of providers

    develop

    A Component is an isolated group of providers within a single container, identified by a unique string.

    When a dependency is requested, dishka searches for a provider within the same component as the direct dependant. This allows you to build modular parts of an application that use the same type names without collision.

    To define a component, set the component attribute on your Provider class. To request a dependency from a specific component, use Annotated[Type, FromComponent("ComponentName")] in the provider method signature.

    class MainProvider(Provider):
    
        @provide(scope=Scope.APP)
        def foo(self, a: Annotated[int, FromComponent("X")]) -> float:
            return a/10
    
        @provide(scope=Scope.APP)
        def bar(self, a: int) -> complex:
            return a + 0j
    
    
    class AdditionalProvider(Provider):
        component = "X"
    
        @provide(scope=Scope.APP)
        def foo(self) -> int:
            return 1
    
    
    container = make_container(MainProvider(), AdditionalProvider())
    container.get(float)  # returns 0.1
    container.get(complex)  # raises NoFactoryError