Dependify
Dependify is a library that aims to reduce the effort that comes with the manual handling of dependency injection by automating as much as it can.
The problem
Imagine that you have some class that requires some functions provided by other classes. If you have to instantiate manually all the dependencies in multiple places, it might get messy really quick.
class A:
def __init__(self):
pass
class B:
def __init__(self, a: A):
self.a = A
class C:
def __init__(self, b: B):
self.b = B
def use_a():
a = A()
def use_b():
a = A()
b = B(a)
def use_c():
a = A()
b = B(a)
c = C(b)
When you want to decouple the classes from direct references (like dependency instantiation inside of a constructor) you use arguments to separate the use from the creation (dependency injection principle).
This make your code easier to scale, grow and its a strong step in the direction of using SOLID principles in your code.
The problems comes when each dependency has its own dependencies.
You can instantiate each of them by yourself as seen in the example. But your code will become more complex and so the classes. To use an specific dependency you have to handle its dependencies and this will force you to remember every dependency's dependencies.
Your first approach might be to define some module that contains all dependency creation logic.
from a_module import A, A1
from b_module import B, B1
from c_module import C, C1
from d_module import D, D1
def create_a():
return A()
def create_a1():
return A1()
def create_b():
return B(create_a())
def create_b1():
return B1(create_a1())
def create_c():
return C(create_b())
def create_d():
return D(create_a(), create_b1())
Then you'll have any sort of combinations of dependencies that will be hard to track or modify.
Dependify offers to take this bullet for you by automatically instantiating and wiring up dependencies so you can focus on creating value with your solution.
Usage
Out of the box usage
from dependify import injectable, inject
@injectable
class SomeDependency:
def __init__(self):
pass
class SomeDependantClass:
@inject
def __init__(self, some_dependency: SomeDependency):
self.some_dependency = some_dependency
dependant = SomeDependantClass()
All dependencies are stored globally, meaning they will be accessible through all the code as long the registration happends before usage.
You can register a dependency for a type using the same type or passing a different type/callable using the patch
keyword argument.
from dependify import injectable, inject
class IService(ABC):
@abstractmethod
def method(self):
pass
@injectable(patch=IService)
class ServiceImpl(IService):
def method(self):
@inject
def do_something_with_service(service: IService):
service.method()
You're not limited to classes to define dependencies, callables also can be registered as dependencies for a type.
from dependify import injectable, inject
class DatabaseHandler:
def __init__(self, str_con: str):
...
def get_clients(self) -> list:
...
@injectable(patch=DatabaseHandler, cached=True)
def create_db_handler():
import os
return DatabaseHandler(os.getenv('DB_CONN_STR'))
@inject
def get_clients_from_db(db_handler: DatabaseHandler):
clients = db_handler.get_clients()
In the previous example you were able to use a predefined process to create an specific dependency. Notice that you must use the patch
keyword when decorating functions since all functions have the same type always.
External register
If for some reason you don't want to anotate your classes (you are using a clean architecture for example), then you can register your classes and callables using the register
function.
from core.users.repository import IUserRepository
class ListUsersUseCase:
def __init__(self, repository: IUserRepository):
self.repository = repository
def execute(self) -> list[User]:
return self.repository.find_all()
from dependify import register
from core.users.usecases import ListUsersUseCase
register(ListUsersUseCase)
import config
from flask import Flask
from dependify import inject
from core.users.usecases import ListUsersUseCase
app = Flask(__name__)
@app.get('/users')
@inject
def get_all_users(
use_case: ListUsersUseCase
):
users = use_case.execute()
return serialized_users
Localized dependencies
In the backstage Dependify uses a global Container
object to hold all dependencies. But you can also use your own. The inject
decorator has an optional keyword called container
so you can use localized injection with different dependencies for the same type. It means you can have localized dependencies that doesn't crash with global dependencies.
from dependify import Container, inject, register
class SomeClass:
pass
my_container = Container()
my_container.register(SomeClass)
@inject
def use_some_class(some_class: SomeClass):
pass
@inject(container=my_container)
def use_some_class(some_class: SomeClass):
pass
Flags
Either in Dependency
constructor or in register
method can specify the following flags to modify the injection behaviour for a dependency.
cache
determines whether to store the result of the first call of the dependency. Defaults to False
.autowire
determines whether to autowire the arguments declared in the dependency. This feature allows you to decide how to initialize internal dependencies if set to False
. Defaults to True
.
from dependify import register, inject
class HelloPrinter:
def __init__(self):
self.last = None
def say_hello(self, name: str):
print("Before I said hi to", self.last)
print(f"Hello {name}")
self.last = name
register(HelloPrinter, cache=True)
@inject
def hello_dev(printer: HelloPrinter):
printer.say_hello("Developer")
@inject
def hello_po(printer: HelloPrinter):
printer.say_hello("Product Owner")
hello_dev()
hello_po()
Since we are sharing the HelloPrinter
instance between functions, any change made to it will be accessible by the next function and so on. In this example we are storing the last name that was passed to the say_hello
method.
The output would look similar to this
Before I said hi to None
Hello Developer
Before I said hi to Developer
Hello Product Owner
Even though the dependency was instantiated out of scene, we are using the same instace throughout the program.
This could be useful when we have a dependency that must store its state like a database connection or some api client whose instantiation is resource-costly.
The catch
Sadly, anything in life is perfect. Dependify is not the exception.
If you want to use the decorators you are tied to use injection in callables only if you want to keep your domain clean from any dependency.
from dependify import injectable, inject
@injectable
class A:
pass
@injectable
class B:
def __init__(self, b: B):
self.b = B
def main():
b = B()
@inject
def main(b: B):
If you don't need (or want) to use decorators, you can use the function-based way.
from dependify import register, resolve
class A:
pass
class B:
def __init__(self, b: B):
self.b = B
register(A)
register(B)
def main():
b = resolve(B)
The good news are that you can mix both ways of using the registration/injection logic.
from dependify import injectable, inject, register, resolve
@injectable
class A:
pass
class B:
def __init__(self, b: B):
self.b = B
register(B)
@inject
def main(b: B):
def main2():
a = resolve(A)
If your classes can be decorated then the usage of a dependant class becomes much easier.
from dependify import injectable, inject
@injectable
class A:
pass
@injectable
class B:
pass
class C:
@inject
def __init__(self, a: A, b: B):
pass
c = C()