Package: inject-it
Inject-ItSimple dependency injection facility using python decorators.inject_itaims to:Keep dependencies creation separated from usage;Keep dependencies swappable for test environments;How to useYou can inject dependencies in two main ways:1- Creating it first, in a main.py like style;
2- Using a function to create the dependency.Creating the dependency first aproachSay you have a dependency for a object of typeSomeService. Then before you start your application, you create a instance
of this object and want to inject it to a function. So you can have:# service.pyclassSomeService:# Your service stuffdefdo_stuff(self):print("Doing stuff")# main.pyfromserviceimportSomeServicefrominject_itimportregister_dependencyservice=SomeService()register_dependency(service)# More code later...The code above will register the type ofserviceand bound this to theserviceinstance.
So in another file that requiresSomeServicewe will use therequiresdecorator, like:# worker.pyfromserviceimportSomeServicefrominject_itimportrequires@requires(SomeService)defyour_function(some_argument:str,another_argument:int,s:SomeService):s.do_stuff()# main.pyfromserviceimportSomeServicefrominject_itimportregister_dependencyservice=SomeService()register_dependency(service)fromworkerimportyour_functionyour_function("abc",another_argument=1)By using thereguiresdecorator onyour_functionpassingSomeServiceas a dependency, inject_it will inject theserviceinstance
into the function for you.How injection worksinject_itrequiresdecorator instropects the signature of the decorated function to inject the dependencies. So it does some checks to see if the decorated function is correctly annotated. We also check if you also not given the dependency that will be injected, so we dont override for some reason your call.The requires decoratorTherequiresdecorator also accepts more than one dependency for function. So you can do:frominject_itimportrequires@requires(str,int,float)deftotally_injected_function(a:int,b:str,c:float):pass# In this case you can call the function like thistotally_injected_function()The code above works, but in the snippet above for simplicity we didn't calledregister_dependency, so this snippet as is will raise aninject_it.exceptions.DependencyNotRegistered.Creating the dependency on a provider functionYou can also define a dependencyprovider. That is a function that will return the dependency object. This is useful if you need a different instance everytime. Using the same example from before:# providers.pyfromserviceimportSomeServicefrominject_itimportprovider@provider(SomeService)defsome_service_provider():# On a real example,on this approach you probably would load some env-variables, config, etc.returnSomeService()In this example, everytime a functionrequiresforSomeServicethis function will be called. If it's expensive to create the object, you can cache it. You do it like:# providers.pyfromserviceimportSomeServicefrominject_itimportprovider@provider(SomeService,cache_dependency=True)# <-defsome_service_provider():# On a real example,on this approach you probably would load some env-variables, config, etc.returnSomeService()This will have the same effect as callingregister_dependencyafter creating the object.Your provider can alsorequiresa dependency, but it must be registered before it.Conditional arguments to ProviderSometimes you will want to create a service dinamically, using some attributes for the current context of your application. For example, on a HTTP view passing the current user to the provider, the state of a object on the database.inject-itallows you to give this parameters to the provider on the fly usingadditional_kwargs_to_providercontext manager. That will applyfunctools.partialinto your provider for the given kwargs. Example:First, let's define the provider like usual:# client.pyfrominject_itimportproviderclassClient:def__init__(self,key):self.key@provider(Client)defclient_provider(api_key:str)->Client:returnClient(key=api_key)Notice that if we don't inject theapi_keyargument,inject_itwon't be able to call theclient_providerfunction, since it will be missing theapi_keyparameter. To solve this let's continue the example:# services.pyfromclientimportClientfrominject_itimportrequires@requires(Client)defmake_request(client:Client):print(client.key)So let's say you use the api_key for each user. And you receive an HTTP request into your view. Using django views, migth look like this:# views.pyfromclientimportClientfromservicesimportmake_requestfrominject_itimportadditional_kwargs_to_providerdefsome_view(request):user=request.userwithadditional_kwargs_to_provider(Client,api_key=user.some_service_key):make_request()# client will be injected for the given user.some_service_key...Two things is happening when you call theadditional_kwargs_to_providerfunction:
1- You will be patch theClientprovider function to receive the kwargs you given.
2- The kwargs must match theclient_providerarguments.This helps if you are using some design patterns like the Strategy Pattern, swapping a service implementation for your current application state.Depending on abstract classesinject-itallows you toregister_dependencyto anotherbound_type. This is useful if you don't really care about the concrete implementation, only the abstract one.
Consider this example:# main .pyfrominject_itimportregister_dependencyclassAbcService:defdo_work(self):print("Working")classConcreteService:defdo_work(self):print("I'm really working")service=ConcreteService()register_dependency(service,bound_type=AbcService)# other_file.pyfrommainimportAbcServicefrominject_itimportrequires@requires(AbcService)defyour_function(s:AbcService):print(s)# Calling this function will return:your_function()"I'm really working"The same is true for theproviderdecorator. You can pass to it the abstract class and return from it the concrete one.
Using the same classes from the above example, consider:frominject_itimportproviderfrommainimportAbcService,ConcreteService@provider(AbcService)defprovider_func():returnConcreteService()When one defines aproviderthis provider function requires to be imported so that inject-it is aware of this provider. Usually, you can do this on your application entrypoint, amain.pyfile for example. However, it may feel weird importing a module only for registering purposes, and your IDE will tell you that you imported a module, but never used it. For example:# main.pyfromyour_applicationimportproviders# <- Imported, but unused in this scopedefmain():print("Do stuff")...main()inject-itallows you to register your providers in a more fashion way. It's similar to what's Django does with applications.Depending on Callable'sinject-italso allows you to depend on a callable. This is possible using thetyping.Protocolandtyping.runtime_checkabledecorator. Check out this example:# protocols.pyfromtypingimportProtocol,runtime_checkable@runtime_checkableclassSumTwoNumbersFunc(Protocol):def__call__(self,a,b)->int:...# providers.pyfrominject_itimportproviderfromprotocolsimportSumTwoNumbersFuncdefsum_two(a,b)->int:returna+bdefsum_two_with_additional_fee(a,b)->int:returna+b+10@provider(SumTwoNumberFunc)defget_two_num_sum_func(is_angry=False):ifis_angry:returnsum_two_with_additional_feereturnsum_two# services.pyfromprotocolsimportSumTwoNumberFuncfrominject_itimportrequires@requires(SumTwoNumberFunc)defcharge_order(fnc:SumTwoNumberFunc):print(fnc(1,1))# main.pyimportproviders# importing so it's registered by inject-itfromservicesimportcharge_orderfromprotocolsimportSumTwoNumbersFuncfrominject_itimportadditional_kwargs_to_providerdefmain():charge_order()# prints 2withadditional_kwargs_to_provider(SumTwoNumberFunc,is_angry=True):charge_order()# prints 12if__name__=="__main__":main()Register Providers ModulesSince importing a provider file in runtime just for registering may feel ackward, as mentioned above,inject-itexposes aregister_provider_modulesfunction that one can use to register all its providers on a single call. Using the same example from above, it will look like:frominject_itimportregister_provider_modulesdefmain():print("Do stuff")...register_provider_modules("your_application.providers","your_application.another_module.my_providers",)main()register_provider_modulesany number of providers modules, it will look for any function decorated withproviderin those modules. If no provider is found an exception is raised.LimitationsFor the moment, you can only have one dependency for each type. So you can't have like two differentstrdependencies. When you register the secondstryou will be overriding the first. You can work around this by using specific types, instead of primitive types. Other way around is registering a provider with conditional ways of creating a object.TestingTesting is made easy withinject-it, you just have to register yourmock,fake,stubbefore calling your function. If you are using pytest, use fixtures.