Package: kink
KinkDependency injection container made for pythonFeaturesEasy to use interfaceExtensible with custom dependency resolversAutomatic dependency injection (Autowiring)LightweightSupport for async with asyncioInstallationPippipinstallkinkPoetryIf you don't know poetry, I highly recommend visiting theirwebpagepoetryaddkinkWhy using dependency injection in python?Short storyBecause python is a multi paradigm language and this should encourage
you to use best OOP practices improving your workflow and your code and have more time
for your hobbies and families instead monkey-patching entire world.Long storyDependency happens when one component (component might be a class, or a function)Auses other componentB. We say than thatAdepends onB.Instead hardcoding dependency inside your components and making your code tightly coupled
you are loosing it by providing(injecting) required behaviour either by subclassing or
plugging additional code. This is calledInversion of Controlwhich keeps your code
oriented around behaviour rather than control. There are many benefits coming out of it:increased modularitybetter extensibility and flexibilityit helps you understand higher concepts like event driven programmingThis is where dependency injection comes in place. Dependency injection is a specific
style of inversion of control, which generally says instead hardcoding dependency pass
dependant object as a parameter to a method rather than having method creating it itself.
( who would thought it is so easy :)? ). It can go even further than that; when you pass
a dependency don't rely on a particular implementation rely on an abstraction (Dependency Inversion Principle).So you might ask why do I need it? Here is couple reasons:Relying on the global state is evilCoding is hard enough ( business requirements are changing all the time, deadlines are
shortening, clients wants more, there are so many unknowns you have to figure out),
relying on unpredictable state makes it even harder:it might introduce potential bugsmakes code harder to maintainconcurrency becomes harder to achievebalancing mokey-patching well is a hard taskGreat, but now I have additional work I have to manage now all my dependencies write more code and deadlines are coming even closer!True, that is why you should pick up Dependency Injection Container to do all this work
for you. Kink gives you one decorator and simpledict-likeobject to bootstrap and manipulate
your container.
No need for manual work and manual dependency management. Give it a try and you will love it!UsageTo fully utilise the potential of kink it is recommended to bootstrap your initial dependencies
(config values, or instances of classes that are standalone, requires no other dependencies than themselves).
Some people prefer to keep it in__init__.pyin the top module of your application, other
create separatebootstrap.pyfile for this purpose. Once all is setup the only step left
is to decorate your classes/functions with@injectdecorator.Bootstrapping/Adding services manuallyAddingserviceto di containerDependency container is a dict-like object, adding new service to dependency container is as
simple as the following example:fromkinkimportdifromosimportgetenvdi["db_name"]=getenv("DB_NAME")di["db_password"]=getenv("DB_PASSWORD")Addingon-demand serviceto dependency injection containerKink also supports on-demand service creation. In order to define such a service,
lambda function should be used:fromkinkimportdifromsqlite3importconnectdi["db_connection"]=lambdadi:connect(di["db_name"])In this scenario connection to database will not be established until service is requested.Adding factorised services to dependency injectionFactorised services are services that are instantiated every time they are requested.fromkinkimportdifromsqlite3importconnectdi.factories["db_connection"]=lambdadi:connect(di["db_name"])connection_1=di["db_connection"]connection_2=di["db_connection"]connection_1!=connection_2In the above example we defined factorised servicedb_connection, and below by accessing the service from di we created
two separate connection to database.Requesting services from dependency injection containerTo access given service just reference it insidedilike you would do this with
a normal dictionary, full example below:fromkinkimportdifromsqlite3importconnect# Bootstrappingdi["db_name"]="test_db.db"di["db_connection"]=lambdadi:connect(di["db_name"])# Getting a serviceconnection=di["db_connection"]# will return instance of sqlite3.Connectionassertconnection==di["db_connection"]# TrueAutowiring dependenciesAutowiring is the ability of the container to automatically create and inject dependencies.
It detects dependencies of the component tries to search for references in the container
and if all references are present an instance of requested service is returned.Autowiring system in kink works in two ways:matching argument's namesmatching argument's type annotationHow dependencies are prioritised by autowiring mechanismAutowiring mechanism priorities dependencies automatically, so when multiple
matches are found for the service this is how it works;
Firstly passed arguments are prioritied - if you pass arguments manually to the service
they will take precendence over anything else. Next argument's names are taken into
consideration and last but not least argument's type annotations.Matching argument's namesIf you don't like type annotations or would like to take advantage of autowiring's
precedence mechanism use this style.This is a very simple mechanism we have already seen in previous examples.
Autowiring system checks function argument's names and tries to search for
services with the same names inside the container.Matching argument's type annotationsIf you are like me and like type annotations and use static analysis tools this is
a preferred way working with DI container.In this scenario names are ignored instead argument's type annotations are inspected
and looked up inside di container. This requires aliases when bootstrapping
your services in DI container or simply adding them to container in the way that
its type is the key by which service is accessed. Please consider the following example:fromkinkimportdi,injectfromsqlite3importconnect,Connectiondi["db_name"]="test_db.db"di[Connection]=lambdadi:connect(di["db_name"])# sqlite connection can be accessed by its type@inject# Constructor injection will happen hereclassUserRepository:def__init__(self,db:Connection):# `db` argument will be resolved because `Connection` instance is present in the container.self.db=dbrepo=di[UserRepository]assertrepo.db==di[Connection]# TrueConstructor injectionfromkinkimportinject,diimportMySQLdb# Set dependenciesdi["db_host"]="localhost"di["db_name"]="test"di["db_user"]="user"di["db_password"]="password"di["db_connection"]=lambdadi:MySQLdb.connect(host=di["db_host"],user=di["db_user"],passwd=di["db_password"],db=di["db_name"])@injectclassAbstractRepository:def__init__(self,db_connection):self.connection=db_connectionclassUserRepository(AbstractRepository):...repository=di[UserRepository]# will retrieve instance of UserRepository from di containerrepository.connection# mysql db connection is resolved and available to use.When class is annotated byinjectannotation it will be automatically added to the container for future use (eg autowiring).Services aliasingWhen you register a service with@injectdecorator you can attach your own alias name, please consider the following example:fromkinkimportinjectfromtypingimportProtocolclassIUserRepository(Protocol):...@inject(alias=IUserRepository)classUserRepository:...assertdi[IUserRepository]==di[UserRepository]# returns trueFor more examples checktestsdirectoryRetrieving all instances with the same aliasAliases inkinkdo not have to be unique, but by default when autowiring mechnism is called the service that
was registered first within given alias will be returned. If for some reason you would like to retrieve all
services that alias to the same name (eg implementing strategy pattern),kinkprovides a useful functionality
for doing so. Please consider the following example:fromkinkimportinjectfromtypingimportProtocol,ListclassIUserRepository(Protocol):...@inject(alias=IUserRepository)classMongoUserRepository:...@inject(alias=IUserRepository)classMySQLUserRepository:...@inject()classUserRepository:def__init__(self,repos:List[IUserRepository])->None:# all services that alias to IUserRepository will be passed hereself._repos=reposdefstore_to_mysql(self,user:...):self._repos[1].store(user)defstore_to_mongo(self,user:...):self._repos[0].store(user)Clearing di cacheSometimes it might come handy to clear cached services in di container. Simple way of
doing this is callingdi.clear_cache()method like in the following example.fromkinkimportinject,di...# set and accesss your servicesdi.clear_cache()# this will clear cache of all services inside di container that are not factorised servicesIntegration with FastAPIfromfastapiimportAPIRouter,Depends,statusfromfastapi.responsesimportJSONResponse,Responsefromkinkimportdirouter=APIRouter()# register service in the DI containerdi[ClientService]=ClientService()@router.post("/clients",response_model=ClientDTO,responses={400:{"model":APIErrorMessage},500:{"model":APIErrorMessage}},tags=["clients"],)asyncdefcreate_client(request:CreateClientDTO,service:ClientService=Depends(lambda:di[ClientService]))->JSONResponse:result=service.create(request)returnJSONResponse(content=result.dict(),status_code=status.HTTP_201_CREATED)A complete example, together with tests you can find ithere.Articles on Kinkhttps://www.netguru.com/codestories/dependency-injection-with-python-make-it-easy