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
Created a Python Keylogger script.

Python Script Simple Keylogger Script WHAT IS IT? Created a Python Keylogger script. HOW IT WORKS Once the script has been executed, it will automatic

AC 0 Dec 12, 2021
A notebook explaining the principle of adversarial attacks and their defences

TL;DR: A notebook explaining the principle of adversarial attacks and their defences Abstract: Deep neural networks models have been wildly successful

1 Jan 22, 2022
Small exercises to get you used to reading and writing Python code!

Pythonlings Welcome to Pythonlings, an automated Python tutorial program (inspired by Rustlings and Haskellings). WIP This program is still working in

鹤翔万里 5 Sep 23, 2022
AMTIO aka All My Tools in One

AMTIO AMTIO aka All My Tools In One. I plan to put a bunch of my tools in this one repo since im too lazy to make one big tool. Installation git clone

osintcat 3 Jul 29, 2021
Simple project to assist in tracking/logging my working hours

Fill working hours Basic script to assist in the logging/tracking of my working hours How it works Create a file called projects.json in this director

Robin Kennedy-Reid 2 Oct 31, 2022
Nmap script to detect a Microsoft Exchange instance version with OWA enabled.

Nmap script to detect a Microsoft Exchange instance version with OWA enabled.

Luciano Righetti 27 Nov 17, 2022
Cross-platform config and manager for click console utilities.

climan Help the project financially: Donate: https://smartlegion.github.io/donate/ Yandex Money: https://yoomoney.ru/to/4100115206129186 PayPal: https

3 Aug 31, 2021
3D Printed Flip Clock Design and Code

Smart Flip Clock 3D printed smart clock that puts a new twist on old technology. Making The Smart Flip Clock The first thing that must be done for thi

Thomas 105 Oct 17, 2022
PBN Obfuscator: A overpowered obfuscator for python, which will help you protect your source code

PBN Obfuscator PBN Obfuscator is a overpowered obfuscator for python, which will

Karim 6 Dec 22, 2022
📽 Streamlit application powered by a PyScaffold project setup

streamlit-demo Streamlit application powered by a PyScaffold project setup. Work in progress: The idea of this repo is to demonstrate how to package a

PyScaffold 2 Oct 10, 2022
A Python library for inspecting JVM class files (.class)

lawu Lawu is a human-friendly library for assembling, disassembling, and exploring JVM class files. It's highly suitable for automation tasks. Documen

Tyler Kennedy 45 Oct 23, 2022
Structured Exceptions for Python

XC: Structured exceptions for Python XC encourages a structured, disciplined approach to use of exceptions: it reduces the overhead of declaring excep

Bob Gautier 2 May 28, 2021
mrcal is a generic toolkit to solve calibration and SFM-like problems originating at NASA/JPL

mrcal is a generic toolkit to solve calibration and SFM-like problems originating at NASA/JPL. Functionality related to these problems is exposed as a set of C and Python libraries and some commandli

Dima Kogan 102 Dec 23, 2022
PyDy, short for Python Dynamics, is a tool kit written in the Python

PyDy, short for Python Dynamics, is a tool kit written in the Python programming language that utilizes an array of scientific programs to enable the study of multibody dynamics. The goal is to have

PyDy 307 Jan 01, 2023
Validate UC alumni identifier numbers with Python 3.

UC number validator Validate UC alumni identifier numbers with Python 3. Getting started Install the library with: pip install -U ucnumber Usage from

Open Source eUC 1 Jul 07, 2021
Hook and simulate global keyboard events on Windows and Linux.

keyboard Take full control of your keyboard with this small Python library. Hook global events, register hotkeys, simulate key presses and much more.

BoppreH 3.2k Jan 01, 2023
Render your templates using .txt files

PizzaX About Run Run tests To run the tests, open your terminal and type python tests.py (WIN) or python3 tests.py (UNX) Using the function To use the

Marcello Belanda 2 Nov 24, 2021
A refresher for PowerBI Desktop documents

PowerBI_Refresher-NPP Informació Per executar el programa s'ha de tenir instalat el python versio 3 o mes. Requeriments a requirements.txt. El fitxer

Nil Pujol 1 May 02, 2022
frida-based ceserver. iOS analysis is possible with Cheat Engine.

frida-ceserver frida-based ceserver. iOS analysis is possible with Cheat Engine. Original by Dark Byte. Usage Install frida on iOS. python main.py Cyd

KenjiroIchise 89 Jan 08, 2023
jonny is a stack based programming language

jonny-lang jonny is a stack based programming language also compiling jonny files currently doesnt work on windows you can probably compile jonny file

1 Nov 24, 2021