Socket
Socket
Sign inDemoInstall

dependify

Package Overview
Dependencies
0
Maintainers
1
Alerts
File Explorer

Install Socket

Detect and block malicious and high-risk dependencies

Install

    dependify

Dependency injection package


Maintainers
1

Readme

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.

# Your classes with good coding practices ;)
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

# But then, the hell...
def use_a():
    a = A()
    # do something with A

def use_b():
    a = A()
    b = B(a)
    # do something with B

def use_c():
    a = A()
    b = B(a)
    c = C(b)
    # do something with C

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())

# And so on...

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

# Register a class as a dependency with 'injectable' decorator
@injectable
class SomeDependency:
    def __init__(self):
        pass

class SomeDependantClass:
    # Decorate a callable to inject the dependencies
    @inject
    def __init__(self, some_dependency: SomeDependency):
        self.some_dependency = some_dependency

# Instantiation
# No need to pass arguments since they are being handled by dependify
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.

# Register an interface for injection
# interface definition: abc or protocols
from dependify import injectable, inject


class IService(ABC):
    @abstractmethod
    def method(self):
        pass


# Register ServiceImpl to be injected instead of IService
@injectable(patch=IService)
class ServiceImpl(IService):
    # Class implementation
    def method(self):
        # Some implementation


# usage of service
@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

# Some classes dependencies might need a complex setting up process 
# that can't be put as a dependency due some factors 
# (standard types dependencies for example).
class DatabaseHandler:
    # The following init method needs a string but
    # it will be a non-sense to register 'str' type as a
    # dependency
    def __init__(self, str_con: str):
        ...
    
    def get_clients(self) -> list:
        ...


# Here we define a pre initialization process and
# mark the dependency as 'cached' so it will be 
# instantiated once and it will save the object to
# future calls. Same result could be achieved with
# @cached. But the goal of the decorator is to reduce
# redundant decorators related to usage.
@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()
    # Do something else with result

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.

# use case file
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()

# config file
from dependify import register
from core.users.usecases import ListUsersUseCase

register(ListUsersUseCase)

# controller file (flask in this example)
import config # You make sure that registration happends
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()
    # Serialization to json
    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)

# If we declare a function and decorate it with 'inject' 
# it won't work and instead raise an exception. 
# This is because the global 'Container' it's not aware 
# of the SomeClass type.
@inject
def use_some_class(some_class: SomeClass):
    pass

# Now if we use the 'container' keyword, it won't fail
# and continue the normal flow.
@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.
# Cached instance example
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 your class as a cached dependency 
register(HelloPrinter, cache=True)

# inject the dependency in the place you need (has to be a callable)
@inject
def hello_dev(printer: HelloPrinter):
    printer.say_hello("Developer")

# reuse the object
@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

# Bad use
def main():
    b = B() # this will break since it is specting an instance of A

# Working code
@inject
def main(b: B):
    # do something with 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)

# This will work since you are calling the resolve 
# logic insted of direct instantiation, so 
# dependify will handle all registered 
# dependencies
def main():
    b = resolve(B)
    # Do something with B

The good news are that you can mix both ways of using the registration/injection logic.

from dependify import injectable, inject, register, resolve

# Register A with decorator
@injectable
class A:
    pass

class B:
    def __init__(self, b: B):
        self.b = B

# Register B with function
register(B)


# Inject B with decorator
@inject
def main(b: B):
    # Do something with B

# Inject A with function
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

# This will work since the constructor is decorated and 
# its dependencies are automatically resolved.
c = C() 

FAQs


Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc