Async-first dependency injection library based on python type hints

Overview

Dependency Depression

Async-first dependency injection library based on python type hints

Quickstart

First let's create a class we would be injecting:

class Test:
    pass

Then we should create instance of container and register our Test class in it, we would use Callable provider that would simply call our class, since classes are also callables!

from dependency_depression import Depression, Callable

container = Depression()
container.register(Test, Callable(Test))

Then we should create a context and resolve our class from it:

with container.sync_context() as ctx:
    ctx.resolve(Test)
    # < __main__.Test>

Injecting

To mark parameters for injection mark them with typing.Annotated and Inject marker

from typing import Annotated
from dependency_depression import Callable, Depression, Inject


def create_number() -> int:
    return 42


def create_str(number: Annotated[int, Inject]) -> str:
    return str(number)

container = Depression()
container.register(str, Callable(create_str))
container.register(int, Callable(create_number))

with container.sync_context() as ctx:
    string = ctx.resolve(str)
    print(string, type(string))
    # 42 
   

Providers

When creating a provider you should specify the type it returns, but it can be inferred from class type or function return type:

from dependency_depression import Callable

provider = Callable(int)
# Is the same as Callable(factory=int, impl=int)
assert provider.provide_sync() == 0

Example using factory function, impl is inferred from return type hint:

from dependency_depression import Callable


def create_foo() -> str:
    return "foo"


provider = Callable(create_foo)
assert provider.provide_sync() == "foo"
assert provider.impl is str

This all comes into play when you have multiple implementations for base class and want to retrieve individual providers from a container,
let's register two concrete classes under same interface:

from dependency_depression import Depression, Callable


class Base:
    pass


class ConcreteA(Base):
    pass


class ConcreteB(Base):
    pass


container = Depression()
container.register(Base, Callable(ConcreteA))
container.register(Base, Callable(ConcreteB))

with container.sync_context() as ctx:
    a = ctx.resolve(Base, ConcreteA)  # <__main__.ConcreteA>
    b = ctx.resolve(Base, ConcreteB)  # <__main__.ConcreteB>
    
    # This would raise an error since we have two classes registered as `Base`
    ctx.resolve(Base)

If you have multiple classes registered under same interface you can specify concrete class using Impl marker:

from typing import Annotated
from dependency_depression import Inject, Impl


class Injectee:
    def __init__(
        self,
        a: Annotated[Base, Inject, Impl[ConcreteA]],
        b: Annotated[Base, Inject, Impl[ConcreteB]],
    ):
        pass

You can also just register concrete classes instead:

container.register(ConcreteA, Callable(ConcreteA))
container.register(ConcreteB, Callable(ConcreteB))

class Injectee:
    def __init__(
        self,
        a: Annotated[ConcreteA, Inject],
        b: Annotated[ConcreteB, Inject],
    ):
        pass

Generics

Dependency Depression can also be used with Generics:

T: raise NotImplementedError class UserRepository(IRepository[User]): def get(self, identity: int) -> User: return User(id=identity, username="Username") class ItemRepository(IRepository[Item]): def get(self, identity: int) -> Item: return Item(id=identity, title="Title") class Injectee: def __init__( self, user_repository: Annotated[IRepository[User], Inject], item_repository: Annotated[IRepository[Item], Inject], ): self.user_repository = user_repository self.item_repository = item_repository container = Depression() container.register(IRepository[User], Callable(UserRepository)) container.register(IRepository[Item], Callable(ItemRepository)) container.register(Injectee, Callable(Injectee)) with container.sync_context() as ctx: injectee = ctx.resolve(Injectee) injectee.user_repository # < __main__.UserRepository> injectee.item_repository # <__main__.ItemRepository>">
import dataclasses
from typing import Generic, TypeVar, Annotated

from dependency_depression import Inject, Depression, Callable

T = TypeVar("T")


@dataclasses.dataclass
class User:
    id: int
    username: str


@dataclasses.dataclass
class Item:
    id: int
    title: str


class IRepository(Generic[T]):
    def get(self, identity: int) -> T:
        raise NotImplementedError


class UserRepository(IRepository[User]):
    def get(self, identity: int) -> User:
        return User(id=identity, username="Username")

    
class ItemRepository(IRepository[Item]):
    def get(self, identity: int) -> Item:
        return Item(id=identity, title="Title")

    
class Injectee:
    def __init__(
        self,
        user_repository: Annotated[IRepository[User], Inject],
        item_repository: Annotated[IRepository[Item], Inject],
    ):
        self.user_repository = user_repository
        self.item_repository = item_repository


container = Depression()
container.register(IRepository[User], Callable(UserRepository))
container.register(IRepository[Item], Callable(ItemRepository))
container.register(Injectee, Callable(Injectee))

with container.sync_context() as ctx:
    injectee = ctx.resolve(Injectee)
    injectee.user_repository
    # < __main__.UserRepository>
    injectee.item_repository
    # <__main__.ItemRepository>

Context

Context as meant to be used within application or request scope, it keeps instances cache and an ExitStack to close all resources.

Cache

Context keeps cache of all instances, so they won't be created again, unless use_cache=False or NoCache is used.

In this example passing use_cache=False would cause context to create instance of Test again, however it wouldn't be cached:

from dependency_depression import Callable, Depression


class Test:
    pass


container = Depression()
container.register(Test, Callable(Test))

with container.sync_context() as ctx:
    first = ctx.resolve(Test)
    
    assert first is not ctx.resolve(Test, use_cache=False)
    # first is still cached in context
    assert first is ctx.resolve(Test)

Closing resources using context managers

Context would also use functions decorated with contextlib.contextmanager or contextlib.asyncontextmanager, but it won't use other instances of ContextManager.
Note that you're not passing impl parameter should specify return type using Iterable, Generator or their async counterparts - AsyncIterableand AsyncGenerator:

import contextlib
from typing import Iterable

from dependency_depression import Depression, Callable


@contextlib.contextmanager
def contextmanager() -> Iterable[int]:
    yield 42


class ContextManager:
    def __enter__(self):
        # This would never be called
        raise ValueError

    def __exit__(self, exc_type, exc_val, exc_tb):
        pass


container = Depression()

# Without return type hint you can specify impl parameter:
# container.register(int, Callable(contextmanager, int))
container.register(int, Callable(contextmanager))
container.register(ContextManager, Callable(ContextManager))

with container.sync_context() as ctx:
    number = ctx.resolve(int)  # 42
    ctx_manager = ctx.resolve(ContextManager) # __enter__ would not be called
    with ctx_manager:
        ...
        # Oops, ValueError raised

In case you need to manage lifecycle of your objects you should wrap them in a context manager:

import contextlib
from typing import AsyncGenerator

from dependency_depression import Callable, Depression
from sqlalchemy.ext.asyncio import AsyncSession


@contextlib.asynccontextmanager
async def get_session() -> AsyncGenerator[AsyncSession, None]:
    session = AsyncSession()
    async with session:
        try:
            yield session
        except Exception:
            await session.rollback()
            raise

container = Depression()
container.register(AsyncSession, Callable(AsyncSession))

@Inject decorator

@inject decorator allows you to automatically inject parameters into functions:

from typing import Annotated

from dependency_depression import Callable, Depression, Inject, inject


@inject
def injectee(number: Annotated[int, Inject]):
    return number


container = Depression()
container.register(int, Callable(int))

with container.sync_context():
    print(injectee())
    # 0

Without active context number parameter would not be injected:

injectee()
# TypeError: injectee() missing 1 required positional argument: 'number'

But you still can use your function just fine

print(injectee(42))

You can pass parameters even if you have an active context:

with container.sync_context():
    print(injectee())  # 0, injected
    print(injectee(42))  # 42, provided by user

Usage with Asyncio

Dependency Depression can be used in async context, just use context instead of sync_context:

import asyncio

from dependency_depression import Callable, Depression


async def get_number() -> int:
    await asyncio.sleep(1)
    return 42


async def main():
    container = Depression()
    container.register(int, Callable(get_number))
    async with container.context() as ctx:
        number = await ctx.resolve(int)
        assert number == 42


if __name__ == '__main__':
    asyncio.run(main())

Async context also supports both sync and async context managers and factory functions.

Owner
Doctor
Doctor
Acesse seus investimentos da NuInvest pelo Python (Experimental)

Acesse seus investimentos da NuInvest pelo Python (Experimental)

André Roggeri Campos 5 Dec 06, 2022
OB_Template is a vault template reference for using Obsidian.

Obsidian Template OB_Template is a vault template reference for using Obsidian. If you've tested out Obsidian. and worked through the "Obsidian Help"

323 Dec 27, 2022
Notebook researcher - Notebook researcher with python

notebook_researcher To run the server, you must follow these instructions: At th

4 Sep 02, 2022
Control System Packer is a lightweight, low-level program to transform energy equations into the compact libraries for control systems.

Control System Packer is a lightweight, low-level program to transform energy equations into the compact libraries for control systems. Packer supports Python 🐍 , C 💻 and C++ 💻 libraries.

mirnanoukari 31 Sep 15, 2022
A module to prevent invites and joins to Matrix rooms by checking the involved server(s)' domain.

Synapse Domain Rule Checker A module to prevent invites and joins to Matrix rooms by checking the involved server(s)' domain. Installation From the vi

matrix.org 4 Oct 24, 2022
A Bot Which Can generate Random Account Based On Your Hits.

AccountGenBot This Bot Can Generate Account With Hits You Save (Randomly) Keyfeatures Join To Use Support Limit Account Generation Using Sql Customiza

DevsExpo 30 Oct 21, 2022
A normal phoneNumber tracker made with python.

A normal phoneNumber tracker made with python.

CLAYZANE 2 Dec 30, 2021
Beginner Projects A couple of beginner projects here

Beginner Projects A couple of beginner projects here, listed from easiest to hardest :) selector.py: simply a random selector to tell me who to faceti

Kylie 272 Jan 07, 2023
A collection of Python library code for building Python applications.

Abseil Python Common Libraries This repository is a collection of Python library code for building Python applications. The code is collected from Goo

Abseil 2k Jan 07, 2023
Blender 2.80+ Timelapse Capture Tool Addon

SimpleTimelapser Blender 2.80+ Timelapse Capture Tool Addon Developed for Blender 3.0.0, tested working on 2.80.0 It's no ZBrush undo history but it's

4 Jan 19, 2022
This is a method to build your own qgis configuration packages using osgeo4W.

This is a method to build your own qgis configuration packages using osgeo4W. Then you can automate deployment in your organization with a controled and trusted environnement.

Régis Haubourg 26 Dec 05, 2022
Shell scripts made simple 🐚

zxpy Shell scripts made simple 🐚 Inspired by Google's zx, but made much simpler and more accessible using Python. Rationale Bash is cool, and it's ex

Tushar Sadhwani 492 Dec 27, 2022
Functions to analyze Cell-ID single-cell cytometry data using python language.

PyCellID (building...) Functions to analyze Cell-ID single-cell cytometry data using python language. Dependecies for this project. attrs(=21.1.0) fo

0 Dec 22, 2021
Functional collections extension functions for Python

pyfuncol pyfuncol Installation Usage API Documentation Compatibility Contributing License A Python functional collections library. It extends collecti

Andrea Veneziano 32 Nov 16, 2022
A module to develop and apply old-style links

Old-Linkage-Dev (OLD) Old Linkage Development is a module to develop and apply old-style links. Old-style links stand for some traditional or conventi

Tarcadia 2 Dec 04, 2021
Easy, clean, reliable Python 2/3 compatibility

Overview: Easy, clean, reliable Python 2/3 compatibility python-future is the missing compatibility layer between Python 2 and Python 3. It allows you

Python Charmers 1.2k Jan 08, 2023
データサイエンスチャレンジ2021 サンプル

データサイエンスチャレンジ2021 サンプル 概要 線形補間と Catmull–Rom Spline 補間のサンプル Python スクリプトです。 データサイエンスチャレンジ2021の出題意図としましては、訓練用データ(train.csv)から機械学習モデルを作成して、そのモデルに推論させてモーシ

Bandai Namco Research Inc. 5 Oct 17, 2022
Template (v0) do Sistema Chatbot - atividade síncrona - INE5404

ine-5404-sistema-chatbot-template Template (v0) do Sistema Chatbot - atividade síncrona - INE5404 Veja abaixo um exemplo de funcionamento do sistema:

0 Dec 07, 2021
MiniJVM is simple java virtual machine written by python language, it can load class file from file system and run it.

MiniJVM MiniJVM是一款使用python编写的简易JVM,能够从本地加载class文件并且执行绝大多数指令。 支持的功能 1.从本地磁盘加载class并解析 2.支持绝大多数指令集的执行 3.支持虚拟机内存分区以及对象的创建 4.支持方法的调用和参数传递 5.支持静态代码块的初始化 不支

keguoyu 60 Apr 01, 2022