python-inject Documentation

repository·master·Indexed 21 days ago

https://github.com/ivankorobkov/python-inject

A fast, thread-safe dependency injection framework for Python that leverages type annotations and descriptors. It provides multiple ways to map dependencies via the Binder object, including instance, constructor, and provider bindings. Key features include the @inject.params and @inject.autoparams decorators for function injection, inject.attr for class attribute descriptors, and inject.instance for runtime requests. It supports context manager lifecycle management and provides utilities for managing injector state during unit testing.

Tokens
3.6K
Snippets
16
Records
16
Agent score
23%

What's inside python-inject

  1. Use context managers with dependencies

    master

    If you bind a class or a function to a context manager (using bind_to_provider or bind), python-inject will use the object as-is. If the object is a context manager, it will be managed by the caller's lifecycle (e.g., when used in an @inject.autoparams function), ensuring resources like connections or files are properly destroyed.

    import contextlib
    import inject
    
    @contextlib.contextmanager
    def get_file_sync():
        obj = MockFile()
        yield obj
        obj.destroy()
    
    def config(binder):
        # Binding a provider that returns a context manager
        binder.bind_to_provider(MockFile, get_file_sync)
    
    inject.configure(config)
    
    @inject.autoparams()
    def example(file: MockFile):
        # 'file' will be automatically destroyed when 'example' exits
        pass
  2. Configure the injector and bind dependencies

    master

    To use python-inject, you must first configure it by providing a configuration function that accepts a binder object. Use the binder to map keys (types or hashable objects) to specific instances, constructors, or providers.

    Binding Types:

    • Instance: binder.bind(Key, instance) returns the exact same instance every time.
    • Constructor: binder.bind_to_constructor(Key, lambda: Constructor()) creates a singleton on the first injection.
    • Provider: binder.bind_to_provider(Key, provider_func) calls the provider function on every injection.
    • Runtime: If a class uses inject.attr(Key) but Key is not explicitly bound, inject will attempt to implicitly instantiate it (Runtime binding).
    import inject
    
    def my_config(binder):
        # Instance binding
        binder.bind(Cache, RedisCache('localhost:1234'))
        # Constructor binding (singleton)
        binder.bind_to_constructor(Db, lambda: DbConnection())
        # Provider binding (called every time)
        binder.bind_to_provider(str, lambda: "Hello")
        # Binding using a hashable key instead of a type
        binder.bind('host', 'localhost')
    
    inject.configure(my_config)
  3. Configure the injector for Django or with overrides

    master

    When using python-inject in environments like Django where modules might be loaded multiple times, use once=True to ensure configuration only runs if the injector is absent.

    To create composable configurations (e.g., for testing), use binder.install(base_config) to reuse existing bindings, and set allow_override=True in inject.configure to permit overriding previously registered dependencies.

    # Django usage
    inject.configure(my_config, once=True)
    
    # Composable/Override usage
    def base_config(binder):
        binder.bind(Validator, RealValidator())
    
    def tests_config(binder):
        binder.install(base_config)
        binder.bind(Validator, TestValidator()) # Overrides RealValidator
    
    inject.configure(tests_config, allow_override=True, clear=True)
  4. Set up and tear down injector for testing

    master

    In unit tests, it is best practice to create a fresh injector for each test using inject.configure(callable, clear=True) in the setUp method, and clean up the injector in the tearDown method using inject.clear().

    import unittest
    import inject
    
    class MyTest(unittest.TestCase):
        def setUp(self):
            # Create a new injector for this test
            inject.configure(lambda binder:
                binder.bind(Cache, MockCache()),
                clear=True)
        
        def tearDown(self):
            # Clean up after the test
            inject.clear()
  5. How binding types work in python-inject

    master

    The Binder object provides several ways to map a key (usually a class) to a dependency:

    1. Instance bindings: Use bind(cls, instance) to always return the exact same instance.
    2. Constructor bindings: Use bind_to_constructor(cls, callable) to create a singleton that is instantiated on its first access.
    3. Provider bindings: Use bind_to_provider(cls, callable) to call the provider function for every injection. Providers can be normal functions or context managers (sync or async).
    4. Runtime bindings: If a class is not explicitly bound, python-inject can automatically create a singleton for it on first access, provided the class can be instantiated without arguments.
    def my_config(binder):
        # Instance binding
        binder.bind(Config, load_config_file())
        
        # Constructor binding (singleton)
        binder.bind_to_constructor(Database, DatabaseConnector)
        
        # Provider binding (called every time)
        binder.bind_to_provider(Session, get_new_session)
  6. Disable runtime binding to prevent accidental instantiations

    master

    By default, python-inject uses runtime binding to implicitly instantiate classes that haven't been explicitly bound. To prevent unexpected behavior and ensure that only explicitly configured dependencies are injected, pass bind_in_runtime=False to inject.configure. This will cause inject to raise an InjectorException if an unbound instance is requested.

    inject.configure(my_config, bind_in_runtime=False)
  7. Configure the injector

    master

    To use python-inject, you must first configure a shared injector. This is typically done by defining a configuration function that accepts a Binder object and then calling inject.configure(your_config_func).

    Once configured, the injector is thread-safe and can be reused across multiple threads.

    def my_config(binder):
        binder.bind(Cache, RedisCache('localhost:1234'))
        binder.bind_to_provider(CurrentUser, get_current_user)
    
    import inject
    inject.configure(my_config)
  8. Manage injector lifecycle in unit tests

    master

    When writing tests, you often need a clean state for each test case. Use inject.clear_and_configure(config_func) to wipe the existing injector and set up a new one, or inject.clear() to remove the injector entirely during teardown.

    import inject
    
    def test_my_feature():
        # Setup a fresh injector for this test
        inject.clear_and_configure(my_test_config)
        
        try:
            # ... run test ...
            pass
        finally:
            # Clean up after the test
            inject.clear()
  9. Inject dependencies using inject.instance, inject.params, and inject.attr

    master

    There are several ways to request dependencies from the injector:

    1. inject.instance(Type): Requests a dependency directly from the injector inside a function body.
    2. @inject.params(...): A decorator that injects dependencies as keyword or positional arguments. (Note: inject.param is deprecated; use inject.params instead).
    3. @inject.attr: A descriptor used within a class to create properties that request dependencies upon access.
    import inject
    
    # 1. Using inject.instance
    def foo(bar):
        cache = inject.instance(Cache)
        cache.save('bar', bar)
    
    # 2. Using @inject.params
    @inject.params(cache=Cache, user=CurrentUser)
    def baz(foo, cache=None, user=None):
        cache.save('foo', foo, user)
    
    # 3. Using @inject.attr
    class User:
        cache = inject.attr(Cache)
                
        def __init__(self, id):
            self.id = id
    
        def save(self):
            self.cache.save('users', self)
  10. Use @inject.autoparams for automatic dependency injection

    master

    The @inject.autoparams decorator automatically injects arguments into a function based on their type annotations. This requires Python >= 3.5.

    To avoid attempting to inject every argument, you can specify exactly which arguments should be injected by passing their names as strings to the decorator.

    You can also use the empty parentheses notation @inject.autoparams() for non-parameterized decorations.

    @inject.autoparams
    def refresh_cache(cache: RedisCache, db: DbInterface):
        pass
    
    # Specify specific arguments to inject
    @inject.autoparams('cache', 'db')
    def sign_up(name, email, cache: RedisCache, db: DbInterface):
        pass
    
    # Non-parameterized usage
    @inject.autoparams()
    def example(conn: MockConnection, file: MockFile):
        pass
  11. Inject arguments based on type hints using `@inject.autoparams`

    master

    The @inject.autoparams decorator automatically injects arguments into a function based on their type hints.

    • Full injection: Use @inject.autoparams to inject all hinted arguments.
    • Selective injection: Use @inject.autoparams('arg1', 'arg2') to only attempt to inject specific arguments by name, even if others are hinted.

    Note: It unwraps Union types (e.g., Union[A, None] becomes A) to find the underlying type for injection.

    import inject
    
    @inject.autoparams
    def refresh_cache(cache: RedisCache, db: DbInterface):
        cache.refresh()
        db.sync()
    
    @inject.autoparams('cache')
    def sign_up(name: str, email: str, cache: RedisCache):
        # Only 'cache' is injected; 'name' and 'email' must be provided by the caller
        cache.save(name)