Lagom Dependency Injection

repository·master·Indexed 43 days ago

https://github.com/meadsteve/lagom

A type-based dependency injection container for Python designed to minimize boilerplate and decouple application code from the DI framework. Lagom uses Python type hints for zero-configuration auto-wiring, supports async functions, and provides tools like ContextContainer for lifecycle management and cleanup, as well as the Env class for environment-based dependency loading.

Tokens
21.5K
Snippets
71
Records
80
Agent score
54%

What's inside Lagom

  1. Compare Lagom with other DI frameworks

    master

    Lagom is a dependency injection framework designed primarily for type-based auto-wiring. Compared to other popular Python DI frameworks, Lagom distinguishes itself by supporting both type-based and name-based auto-wiring, async support, and providing type information usable by mypy without requiring significant alterations to domain code.

    FeatureLagomPinjectInjectorDependency Injector
    Name-based auto-wiring✘✔✘✘
    Type-based auto-wiring✔✘✘✘
    Supports Async✔✘✘✔
    Avoids alteration of domain code✔✔(✔) requires subtle alteration✔
    Exports useful types✔✘✔✔
  2. Use context singletons for shared resources

    master

    By default, every time a dependency is referenced within a ContextContainer invocation, a new instance is created and a separate cleanup is scheduled.

    If you need multiple dependencies to share the same instance during a single invocation (for example, a single database transaction shared by multiple classes), define that type as a context_singleton when initializing the ContextContainer.

    Use the context_singletons argument in ContextContainer to ensure only one instance is created per invocation and cleanup happens only once.

    # Define a transaction that needs to be shared
    @context_dependency_definition(container)
    def _get_a_transaction() -> Iterator[Transaction]:
        transaction = Transaction()
        try:
            yield transaction
        finally:
            transaction.commit()
    
    # Configure the ContextContainer with context_singletons
    context_container = ContextContainer(
        container, 
        context_types=[], 
        context_singletons=[Transaction]
    )
    
    # Both 'a' and 'b' will receive the same Transaction instance
    @bind_to_container(context_container)
    def do_something(a: A = injectable, b: B = injectable):
        print("do something")
  3. Register specific values using NewType

    master

    When a class requires a specific primitive type (like a str for a URL) rather than a full object, use typing.NewType to create a unique type identity. This allows you to register a specific value in the Container that only matches that specific type, preventing collisions with other strings.

    from typing import NewType
    from lagom import Container
    
    DiceApiUrl = NewType("DiceApiUrl", str)
    
    container = Container()
    container[DiceApiUrl] = DiceApiUrl("https://roll.diceapi.com")
  4. Handle multiple instances of the same type

    master

    When you need multiple instances of a single type (e.g., a primary database and a read replica), you can control which instance is injected using either Explicit Definitions or Subtyping.

    Approach 1: Explicit Definitions

    This is the simplest method and requires minimal changes to your existing codebase. You manually tell the Container how to construct each class by providing a lambda or factory function that specifies the correct arguments.

    Approach 2: Subtyping

    This approach involves creating subclasses for each specific instance type (e.g., PrimaryDb and ReadReplica inheriting from Database). You then update your domain code to type hint the specific subclass instead of the base class. This method is more explicit, follows standard Python patterns, and allows IDEs and static analysis tools to correctly identify dependencies.

    # Approach 1: Explicit Definitions
    from lagom import Container
    container = Container()
    container[QueryService] = lambda: QueryService(Database("read-replica-connection-string"))
    container[DataWriter] = lambda: DataWriter(Database("primary-connection-string"))
    
    # Approach 2: Subtyping
    class PrimaryDb(Database):
        pass
    
    class ReadReplica(Database):
        pass
    
    container[ReadReplica] = lambda: ReadReplica("read-replica-connection-string")
    container[PrimaryDb] = lambda: PrimaryDb("primary-connection-string")
    
    class QueryService:
        def __init__(self, db: ReadReplica):
            pass
  5. Map interfaces to implementations in the Container

    master

    To implement the Dependency Inversion Principle, define an abstract base class (interface) and register a concrete implementation class in the Container using the interface as the key. When any other class requests the interface in its __init__, Lagom will provide the registered implementation.

    from abc import ABC
    from lagom import Container
    
    class DiceClient(ABC):
        pass
    
    class HttpDiceClient(DiceClient):
        def __init__(self, url: DiceApiUrl, limiting: RateLimitingConfig):
            pass
    
    container = Container()
    # Map the abstract DiceClient to the concrete HttpDiceClient
    container[DiceClient] = HttpDiceClient
  6. Inject the current HttpRequest

    master

    If any argument in a view or service is type-hinted as HttpRequest, the DjangoIntegration will automatically inject the current request object. This allows services to be constructed with request-specific data.

    # some_services.py
    from django.http import HttpRequest
    
    class DataStoreForRequest:
        def __init__(self, request: HttpRequest, db: SomeDb):
            # The request object is automatically injected
            pass
    
    # views.py
    @dependencies.bind_view
    def question_count(request, store: DataStoreForRequest = injectable):
        return HttpResponse("Done")
  7. Understand Lagom's core comparison features

    master

    When evaluating Lagom for your project, consider these key technical capabilities:

    • Type-based auto-wiring: Automatically injects dependencies based on their type hints. If a constructor requires an instance of MyDep, Lagom can automatically construct or provide it without manual configuration.
    • Name-based auto-wiring: (Note: Lagom focuses on type-based, whereas frameworks like Pinject use name-based matching, e.g., matching an argument named some_dep to a class SomeDep).
    • Async Support: Full compatibility with async def functions.
    • Non-intrusive configuration: Lagom avoids the need to apply special decorators or markers to class constructors, allowing you to keep your domain code clean and decoupled from the DI framework.
    • Type Safety: The framework exports useful types that allow tools like mypy to perform effective static type checking.
  8. How Lagom works: Type-based dependency injection

    master

    Lagom is a dependency injection container where everything is based on types. Instead of requesting objects by name, you pass the class type to the container to retrieve an instance. The design goal is to keep your application code decoupled from Lagom, using it only at the top level to assemble dependencies.

    container = Container()
    some_thing = container[SomeClass]
  9. Mock dependencies in FastAPI tests

    master

    When using a TestClient with the FastAPI integration, dependencies are constructed via the Lagom container. To swap real dependencies for mocks during testing, use deps.override_for_test(). This returns a ContextManager that provides a temporary container where you can register mock objects.

    def test_something():
        client = TestClient(app)
        with deps.override_for_test() as test_container:
            # FooService is an external API so mock it during test
            test_container[FooService] = Mock(FooService)
            response = client.get("/")
            
        assert response.status_code == 200
  10. Integrate Lagom with FastAPI

    master

    While FastAPI has its own dependency injection, you can use Lagom by using the FastApiIntegration.depends method. This method converts a Lagom dependency into the format FastAPI expects.

    Additionally, the FastAPI integration automatically binds the active Request object to the container, allowing you to inject it into classes or functions that require it.

    from fastapi import FastAPI
    from starlette.responses import PlainTextResponse
    from lagom import Container, FastApiIntegration
    
    container = Container()
    container[DBConnection] = DB("DSN_CONNECTION_GOES_HERE")
    
    app = FastAPI()
    deps = FastApiIntegration(container)
    
    @app.get("/")
    async def homepage(request, db = deps.depends(DBConnection)):
        user = db.fetch_data_for_user(request.user)
        return PlainTextResponse(f"Hello {user.name}")
  11. Use Lagom for Dependency Injection with Auto-wiring

    master

    Lagom uses Python type hints to automatically resolve and inject dependencies. By registering types (or classes) into a Container, you can request high-level objects, and Lagom will recursively instantiate all required dependencies based on their type annotations.

    To use Lagom:

    1. Define your application classes and their dependencies using type hints.
    2. Create a Container instance.
    3. Register specific values (like configuration strings or URLs) using their type as the key.
    4. Register implementation classes for abstract interfaces.
    5. Retrieve the top-level object from the container.
    from typing import NewType
    from abc import ABC
    from lagom import Container
    
    # 1. Define types and classes
    DiceApiUrl = NewType("DiceApiUrl", str)
    
    class DiceClient(ABC):
        pass
    
    class HttpDiceClient(DiceClient):
        def __init__(self, url: DiceApiUrl, limiting: RateLimitingConfig):
            ...
    
    class Game:
        def __init__(self, dice_roller: DiceClient):
            ...
    
    # 2. Setup Container
    container = Container()
    
    # 3. Register specific values/types
    container[DiceApiUrl] = DiceApiUrl("https://roll.diceapi.com")
    
    # 4. Register implementations for interfaces
    container[DiceClient] = HttpDiceClient
    
    # 5. Resolve the target object
    game = container[Game]
  12. Configure Request-level Context Singletons with cleanup in FastAPI

    master

    To define dependencies that are singletons for the lifetime of a request and require cleanup (using Python context managers), pass a list of types to the request_context_singletons argument in FastApiIntegration.

    Lagom looks for a ContextManager[T] in the container to satisfy a dependency of type T. You can define these in the container in two ways:

    1. Using a generator with @context_dependency_definition
    2. Using an existing Python Context Manager class
    # Option 1: Generator with decorator
    @context_dependency_definition(container)
    def my_constructor() -> Iterator[SomeResource]:
        try:
            yield SomeResource()
        finally
            # Cleanup logic here
            pass
    
    # Option 2: Existing Context Manager class
    class SomeResourceManager:
        def __enter__(self):
            return SomeResource()
        def __exit__(self, exc_type, exc_val, exc_tb):
            # Cleanup logic here
            pass
    
    container[ContextManager[SomeResource]] = SomeResourceManager
    
    # Setup integration
    deps = FastApiIntegration(container, request_context_singletons=[SomeClass])