Rodi Dependency Injection for Python 3

repository·main·Indexed 42 days ago

https://github.com/neoteroi/rodi

A dependency injection library for Python 3 inspired by .NET's Microsoft.Extensions.DependencyInjection. Rodi provides type resolution using type hints and supports singleton, transient, and scoped service lifetimes. It features a Container for service registration, an ActivationScope for lifecycle management, and a decorator pattern introduced in version 2.1.0 to simplify dependency injection.

Tokens
3.6K
Snippets
15
Records
20
Agent score
54%

What's inside Rodi

  1. Rodi API approaches: ContainerProtocol vs Concrete Implementation

    main

    Rodi provides two distinct API approaches depending on your architectural needs:

    • ContainerProtocol (Generic API): Use this when you want to maintain flexibility to swap rodi with other dependency injection implementations. The protocol requires a class to implement three specific methods:

      • register: To register types.
      • resolve: To resolve types.
      • __contains__: To check if a type is configured. If using another library, you can use composition to wrap it in a class that implements this protocol.
    • Concrete Implementation: Use this when you want a direct, specific implementation and do not intend to switch DI libraries.

  2. Understand service lifetimes in rodi

    main

    Rodi supports three service lifetimes that control how objects are instantiated and reused within the container:

    • singleton: The service is instantiated only once per service provider.
    • transient: A new instance of the service is created every time it is requested.
    • scoped: The service is instantiated once per root service resolution call (for example, once per web request in a web framework context).
  3. Best practices for using rodi

    main

    To ensure efficiency and stability, follow these recommended practices:

    1. Configure once at startup: All services should be configured when the application starts. Do not alter the object graph during normal program execution (e.g., do not change container configuration while handling web requests).
    2. Abstract the container: Keep the Container and service graphs abstracted from your application's front-end layer.
    3. Avoid mixing runtime values with configuration: Avoid registering runtime-specific objects, such as an HTTP Request object, directly as services in your container.
  4. Manage service lifecycles with ServiceLifeStyle

    main

    Rodi supports three primary service lifecycles via the ServiceLifeStyle enum:

    • TRANSIENT: A new instance is created every time the service is requested.
    • SCOPED: An instance is created once per ActivationScope. Subsequent requests within the same scope return the same instance.
    • SINGLETON: An instance is created once and shared across the entire application/provider.
    from rodi import ServiceLifeStyle
    
    # Example enum values
    # ServiceLifeStyle.TRANSIENT
    # ServiceLifeStyle.SCOPED
    # ServiceLifeStyle.SINGLETON
  5. Use ActivationScope for scoped services

    main

    To manage services that should only live for a specific duration (e.g., a web request), use ActivationScope. You can create a scope from a Services instance using create_scope(). Scoped services are stored within the scope and are disposed of when the scope exits.

    For nested scopes, you can use TrackingActivationScope by configuring your Container with scope_cls=TrackingActivationScope.

    from rodi import ActivationScope
    
    # Assuming 'services' is your Services instance
    with services.create_scope() as scope:
        instance = scope.get(MyScopedService)
        # instance is unique to this scope
    
    # instance is now disposed
  6. Use the @inject() decorator for factory functions

    main

    When using factory functions to register services, you must decorate them with @inject() if they rely on local or global namespace information (required for PEP 563 compatibility in Python 3.10+). This ensures that get_type_hints can correctly resolve annotations within the function's scope.

    If a factory is not decorated and lacks the necessary context, a FactoryMissingContextException will be raised.

    from rodi import inject
    
    @inject
    def my_factory(context, type):
        return MyService()
  7. Configure a singleton object

    main

    Use the Container to configure objects as singletons, ensuring that the same instance is returned every time the service is requested throughout the application lifecycle.

    # Based on example-03.py
    # Configure a singleton object within the Container.
  8. Use the decorator pattern for dependency injection

    main

    Since version 2.1.0, rodi supports a decorator pattern to simplify dependency injection, allowing you to mark components or methods for automatic resolution.

    # Based on example-04.py
    # Use the decorator pattern (available since 2.1.0) for dependency injection.
  9. Basic usage of Container for transient service registration

    main

    The Container class can be used to register multiple types and achieve automatic resolution via type inspection. By default, services can be registered as "transient", meaning a new instance is created every time the service is requested.

    # Based on example-01.py
    # Register two types as transient services for automatic resolution via type inspection.
  10. Register and activate concrete types by base type

    main

    To decouple business logic from specific implementations (e.g., separating business layers from data access logic), you can register a concrete type using its base type (interface/abstract class). The Container will then resolve the concrete implementation when the base type is requested.

    # Based on example-02.py
    # Register a concrete type by its base type to enable decoupled activation.
  11. Resolve services from the container

    main

    Once the container is configured, use the resolve method to obtain instances of registered services. You can resolve by the type itself or by a string identifier (if an alias or name is registered).

    resolve(obj_type, scope=None, **kwargs)

    • obj_type: The Type[T] or str representing the service to retrieve.
    • scope: An optional scope object (used when resolving scoped services).
    • **kwargs: Additional arguments passed to the resolver.
    # Resolve by type
    service = container.resolve(MyService)
    
    # Resolve by string name/alias
    service = container.resolve("my_service_name")