python-injector

repository·master·Indexed 23 days ago

https://github.com/python-injector/injector

A Python dependency injection framework inspired by Google Guice. It automates transitive dependency provision using modules, providers, and scopes to encourage decoupled, modular code. The framework avoids global state, requiring explicit use of an Injector instance via methods like .get() or .create_object(), and is designed for compatibility with static type checkers like mypy.

Tokens
6K
Snippets
18
Records
28
Agent score
81%

What's inside injector

  1. What is a Provider in Injector?

    master

    A Provider is a mechanism for providing an instance of a type. Injector includes several built-in providers:

    • ClassProvider: Creates a new instance from a class.
    • InstanceProvider: Returns an existing instance directly.
    • CallableProvider: Provides an instance by calling a function.

    To create a custom provider, subclass injector.Provider and override the get method.

  2. Static type checking with Injector

    master
    Injector is designed to cooperate with static type checking tools like mypy. The Injector.get() method is typed such that injector.get(SomeType) is statically declared to return an instance of SomeType, allowing for type-safe dependency retrieval.
  3. How @inject and scope decorators affect class instantiation

    master

    Using @inject or scope decorators on your classes does not change how they are constructed manually. These decorators do not modify global state and do not allow dependencies to be injected automatically when you instantiate a class without the Injector instance.

    To ensure that dependencies defined in your bindings are actually provided, you must obtain the instance through the Injector (either by injecting the class into another component or by using Injector.get()).

    class X:
        @inject
        def __init__(self, s: str):
            self.s = s
    
    # This will FAIL with a TypeError because @inject cannot find bindings without an Injector instance
    x = X()
  4. Create Child Injectors to override bindings

    master

    A child injector inherits bindings from a parent injector but can override specific bindings without affecting the parent. This allows for localized configuration.

    Warning: If a binding key is already in a parent injector's scope (like singleton), the provider saved in the parent may take precedence when overriding in the child. This behavior is subject to change.

    from injector import Injector
    
    def configure_parent(binder):
        binder.bind(str, to='asd')
        binder.bind(int, to=42)
    
    def configure_child(binder):
        binder.bind(str, to='qwe')
    
    parent = Injector(configure_parent)
    child = parent.create_child_injector(configure_child)
    
    print(parent.get(str)) # 'asd'
    print(child.get(str))  # 'qwe'
    print(child.get(int))  # 42
  5. Avoid injecting into Module constructors or configure methods

    master

    When defining Module classes, avoid using @inject in their __init__ or configure methods to acquire dependencies. Doing so makes the injection process dependent on the order in which modules are passed to the Injector instance, which is fragile and error-prone.

    If ModuleA requires a dependency provided by ModuleC, but ModuleA is passed to the Injector before ModuleC, an error will occur because the dependency is unbound at the time ModuleA is processed.

    A = Key('A')
    B = Key('B')
    
    class ModuleA(Module):
        @inject(a=A)
        def configure(self, binder, a):
            pass
    
    class ModuleB(Module):
        @inject(b=B)
        def __init__(self, b):
            pass
    
    class ModuleC(Module):
        def configure(self, binder):
            binder.bind(A, to='a')
            binder.bind(B, to='b')
    
    # This will fail if ModuleA or ModuleB appear before ModuleC in the list
    Injector([ModuleA, ModuleC])
  6. How Injector handles dependency injection and state

    master

    Unlike some frameworks, Injector does not use global state. You cannot simply decorate a constructor with @inject and instantiate the class manually if that class requires dependencies; there is no global Injector instance to resolve them automatically.

    To resolve dependencies, you must be explicit by using one of the following methods on an Injector instance:

    • Injector.get()
    • Injector.create_object()
    • Injecting the class into a location that is already managed by an Injector.

    This design ensures that classes remain standard Python classes that can still be instantiated manually without the framework if desired.

  7. Core design principles of Injector

    master

    Injector is designed around several key principles:

    • Simplicity: It provides a Pythonic API and avoids excessive 'magic' like automatic member or method injection.
    • No Global State: There is no global injector. You must explicitly use an Injector instance via injector.get() or injector.create_object(). This allows multiple independent injectors with different configurations to coexist.
    • Static Type Safety: The API is designed to work with tools like mypy. For example, injector.get(SomeType) is typed to return an instance of SomeType.
    • Non-intrusive: Markers like @inject, @Inject, and @NoInject are simple markers. Your classes can often be instantiated manually without an injector if needed.
  8. Avoid performing IO or blocking calls in Modules and Providers

    master

    Injector and its related classes use a lock to ensure thread safety. This means only one thread can perform dependency injection at a time.

    Warning: Performing blocking IO (especially without timeouts) inside Module code or @provider methods can lead to application-wide deadlocks. Always avoid performing IO inside these components.

    class BadModule(Module):
        @provider
        def provide_suba(self) -> SubA:
            # DO NOT DO THIS: Blocking calls/IO inside providers can deadlock the app
            while True:
                sleep(1)
            return SubA()
  9. How Injector works: Modules, Providers, and Singletons

    master

    For complex dependency graphs, Injector uses several key abstractions:

    1. Modules: Classes inheriting from Module used to group bindings and providers.
    2. Providers: Methods within a Module decorated with @provider that define how to construct a specific type.
    3. Singletons: Decorating a provider or a binding with @singleton ensures that the same instance is returned every time the type is requested within that Injector instance.
    4. Binder: Used within a configuration function to manually bind a type to a specific instance or configuration.

    This allows you to decouple configuration (e.g., connection strings) from implementation (e.g., database connection logic).

    import sqlite3
    from injector import Module, provider, Injector, inject, singleton
    
    class Configuration:
        def __init__(self, connection_string):
            self.connection_string = connection_string
    
    # 1. Using a configuration function with a binder
    def configure_for_testing(binder):
        configuration = Configuration(':memory:')
        binder.bind(Configuration, to=configuration, scope=singleton)
    
    # 2. Using a Module with @provider and @singleton
    class DatabaseModule(Module):
        @singleton
        @provider
        def provide_sqlite_connection(self, configuration: Configuration) -> sqlite3.Connection:
            conn = sqlite3.connect(configuration.connection_string)
            return conn
    
    class RequestHandler:
        @inject
        def __init__(self, db: sqlite3.Connection):
            self._db = db
    
    # 3. Initializing the Injector with modules
    injector = Injector([configure_for_testing, DatabaseModule()])
    handler = injector.get(RequestHandler)
  10. How Scopes control instance lifecycle

    master

    By default, providers are executed every time an instance is required (NoScope). Scopes allow you to customize this behavior:

    • SingletonScope: (Typically used via the @singleton decorator) ensures the same instance is always provided.
    • Other examples include threading scopes (per-thread) or request scopes (per-HTTP-request).

    Note: Default scopes are bound only to the root injector. Manually binding them to child injectors can result in unexpected behavior.

  11. How to handle transient lifetimes like HTTP requests in custom scopes

    master

    For scopes that require a transient lifetime tied to a specific context (such as an HTTP request), implement a thread-local or greenlet-local cache inside your Scope implementation.

    To manage the lifecycle, call a method on your scope instance in low-level code to "enter" the scope (creating the cache) and call another method to "leave" the scope (clearing the cache) once the request is complete.

  12. Thread safety in Injector

    master

    When using Injector, be aware of the following thread safety guarantees:

    • Thread-safe operations:

      • Calling Injector.get to retrieve an instance.
      • Injection performed via the @inject decorator.
    • Non-thread-safe operations:

      • Most other instance methods on an Injector instance are not thread safe unless explicitly stated otherwise.
      • Note that while the @inject decorator itself is thread safe for performing injection, it does not guarantee the thread safety of the function it decorates.