kink

repository·master·Indexed 19 days ago

https://github.com/kodemore/kink

A lightweight dependency injection container for Python (v0.9.0) designed for Inversion of Control and OOP best practices. It features autowiring via names or type annotations, lazy loading, and factorised services. The library provides a Container class for managing service lifecycles and an @inject decorator for constructor and function injection, supporting aliases, manual bindings, and integration with FastAPI.

Tokens
4.5K
Snippets
21
Records
23
Agent score
65%

What's inside kink

  1. How Autowiring works in Kink

    master

    Autowiring allows the container to automatically resolve and inject dependencies into classes or functions. Kink uses two primary mechanisms to match dependencies:

    1. Argument Name Matching: The container looks for a service in di that matches the name of the function/method argument.
    2. Type Annotation Matching: The container looks for a service in di that matches the type hint of the argument. This is the preferred method for static analysis. To use this, you should register services using their type as the key.

    Precedence Order:

    1. Manually passed arguments (highest priority).
    2. Argument names.
    3. Type annotations (lowest priority).
    from kink import di, inject
    from sqlite3 import connect, Connection
    
    # Registering by type for autowiring
    di[Connection] = lambda di: connect(di["db_name"])
    
    @inject
    class UserRepository:
        # 'db' will be resolved via the Connection type annotation
        def __init__(self, db: Connection):
            self.db = db
  2. Integrate Kink with FastAPI

    master

    To use Kink services within FastAPI routes, use fastapi.Depends with a lambda that accesses the di container.

    from fastapi import APIRouter, Depends
    from kink import di
    
    router = APIRouter()
    # Register service
    di[ClientService] = ClientService()
    
    @router.post("/clients")
    async def create_client(
        request: CreateClientDTO, 
        service: ClientService = Depends(lambda: di[ClientService])
    ):
        result = service.create(request)
        return result
  3. Use Kink for Dependency Injection

    master

    Kink provides a dependency injection container and an injection mechanism for Python. The package exports all functionality from .container and .inject at the top level.

    To use Kink, you typically interact with the Container class to manage service lifecycles and the @inject decorator to request dependencies in your functions or classes.

    from kink import Container, inject
    
    # Example usage pattern
    container = Container()
    
    @inject(container)
    def my_function(dependency):
        return dependency
  4. How `inject` resolves arguments

    master

    When an @inject decorated function or class method is called, kink attempts to resolve its arguments using the following priority order:

    1. Explicitly passed arguments: Any *args or **kwargs passed directly to the function call take highest precedence.
    2. Manual Bindings: If the bind parameter was used in the @inject decorator, it maps argument names to container keys.
    3. Argument Name: If the argument name exists as a key in the container, it is resolved by name.
    4. Argument Type: If the argument name is not found, kink looks for a service in the container that matches the argument's type annotation.
    5. Default Values: If no match is found in the container, the function's defined default value is used.

    If a required parameter cannot be resolved through any of these steps, an ExecutionError is raised.

  5. Alias services using @inject

    master

    You can attach an alias to a service using @inject(alias=...). This allows you to request a service via an abstraction (like a Protocol) while the implementation is a concrete class. If multiple services share the same alias, Kink can inject all of them if the receiving argument is typed as a List of that alias.

    from kink import inject, di
    from typing import Protocol, List
    
    class IUserRepository(Protocol):
        def store(self, user):
            ...
    
    @inject(alias=IUserRepository)
    class MongoUserRepository:
        def store(self, user): ...
    
    @inject(alias=IUserRepository)
    class MySQLUserRepository:
        def store(self, user): ...
    
    @inject()
    class UserRepository:
        # Kink injects all services aliasing IUserRepository into this list
        def __init__(self, repos: List[IUserRepository]) -> None:
            self._repos = repos
  6. Request services from the DI container

    master

    Access services from the container using standard dictionary bracket notation di[key].

    from kink import di
    from sqlite3 import connect
    
    # Bootstrapping
    di["db_name"] = "test_db.db"
    di["db_connection"] = lambda di: connect(di["db_name"])
    
    # Getting a service
    connection = di["db_connection"]
  7. Add services to the DI container

    master

    The di object is a dict-like container used to bootstrap your application. You can add services in three ways:

    1. Static Services: Direct assignment of values or instances.
    2. On-demand Services: Use a lambda function that accepts di as an argument. The service is only instantiated when first requested.
    3. Factorised Services: Use the di.factories dictionary. These services are instantiated fresh every time they are requested.
    from kink import di
    from os import getenv
    from sqlite3 import connect
    
    # 1. Static service
    di["db_name"] = getenv("DB_NAME")
    
    # 2. On-demand service (lazy instantiation)
    di["db_connection"] = lambda di: connect(di["db_name"])
    
    # 3. Factorised service (new instance every time)
    di.factories["db_connection"] = lambda di: connect(di["db_name"])
  8. Use the @inject decorator for Constructor Injection

    master

    Annotating a class with @inject tells Kink to automatically add the class to the container and resolve its dependencies via its constructor during instantiation.

    from kink import inject, di
    import MySQLdb
    
    di["db_host"] = "localhost"
    di["db_connection"] = lambda di: MySQLdb.connect(host=di["db_host"])
    
    @inject
    class AbstractRepository:
        def __init__(self, db_connection):
            self.connection = db_connection
    
    # Resolving the class from the container
    repository = di[AbstractRepository]
  9. Clear the DI container cache

    master

    To clear all cached (singleton) services in the container, call di.clear_cache(). Note that factorised services are not cleared as they are not cached by design.

    from kink import di
    
    di.clear_cache()
  10. Remove services from the Container

    master

    Use the del keyword to remove a service, factory, or alias from the container.

    When you delete a key, the container automatically:

    • Removes it from _services, _factories, and _memoized_services.
    • Cleans up any aliases that were targeting that key.
    • Removes the alias itself if the key was an alias.
    • Clears any List[key] memoized services to ensure consistency.

    If the key does not exist in any of these registries, a KeyError is raised.

    from kink.container import Container
    
    di = Container()
    di["service"] = "value"
    
    del di["service"]