Package: taskiq-dependencies
Taskiq dependenciesThis project is used to add FastAPI-like dependency injection to projects.This project is a part of the taskiq, but it doesn't have any dependencies,
and you can easily integrate it in any project.Installationpipinstalltaskiq-dependenciesUsageLet's imagine you want to add DI in your project. What should you do?
At first we need to create a dependency graph, check if there any cycles
and compute the order of dependencies. This can be done with DependencyGraph.
It does all of those actions on create. So we can remember all graphs at the start of
our program for later use. Or we can do it when needed, but it's less optimal.fromtaskiq_dependenciesimportDependsdefdep1()->int:return1deftarget_func(some_int:int=Depends(dep1)):print(some_int)returnsome_int+1In this example we have a function calledtarget_funcand as you can see, it depends ondep1dependency.To create a dependnecy graph have to write this:fromtaskiq_dependenciesimportDependencyGraphgraph=DependencyGraph(target_func)That's it. Now we want to resolve all dependencies and call a function. It's simple as this:withgraph.sync_ctx()asctx:graph.target(**ctx.resolve_kwargs())Voila! We resolved all dependencies and called a function with no arguments.
Theresolve_kwargsfunction will return a dict, where keys are parameter names, and values are resolved dependencies.Async usageIf your lib is asynchronous, you should use async context, it's similar to sync context, but instead ofwithyou should useasync with. But this way your users can use async dependencies and async generators. It's not possible in sync context.asyncwithgraph.async_ctx()asctx:kwargs=awaitctx.resolve_kwargs()Q&AWhy should I usewithorasync withstatements?Becuase users can use generator functions as dependencies.
Everything beforeyieldhappens before injecting the dependency, and everything afteryieldis executed after thewithstatement is over.How to provide default dependencies?It maybe useful to have default dependencies for your project.
For example, taskiq hasContextandStateclasses that can be used as dependencies.sync_contextandasync_contextmethods have a parameter, where you can pass a dict with precalculated dependencies.fromtaskiq_dependenciesimportDepends,DependencyGraphclassDefaultDep:...deftarget_func(dd:DefaultDep=Depends()):print(dd)return1graph=DependencyGraph(target_func)withgraph.sync_ctx({DefaultDep:DefaultDep()})asctx:print(ctx.resolve_kwargs())You can run this code. It will resolve dd dependency into aDefaultDepvariable you provide.Getting parameters informationIf you want to get the information about how this dependency was specified,
you can use special classParamInfofor that.fromtaskiq_dependenciesimportDepends,DependencyGraph,ParamInfodefdependency(info:ParamInfo=Depends())->str:assertinfo.name=="dd"returninfo.namedeftarget_func(dd:str=Depends(dependency)):print(dd)return1graph=DependencyGraph(target_func)withgraph.sync_ctx()asctx:print(ctx.resolve_kwargs())The ParamInfo has the information about name and parameters signature. It's useful if you want to create a dependency that changes based on parameter name, or signature.Exception propagationBy default if error happens within the context, we send this error to the dependency,
so you can close it properly. You can disable this functionality by settingexception_propagationparameter toFalse.Let's imagine that you want to get a database session from pool and commit after the function is done.asyncdefget_session():session=sessionmaker()yieldsessionawaitsession.commit()But what if the error happened when the dependant function was called? In this case you want to rollback, instead of commit.
To solve this problem, you can just wrap theyieldstatement intry exceptto handle the error.asyncdefget_session():session=sessionmaker()try:yieldsessionexceptException:awaitsession.rollback()returnawaitsession.commit()Also, as a library developer, you can disable exception propagation. If you do so, then no exception will ever be propagated to dependencies and no suchtry exceptexpression will ever work.Example of disabled propogation.graph=DependencyGraph(target_func)withgraph.sync_ctx(exception_propagation=False)asctx:print(ctx.resolve_kwargs())Generics supportWe support generics substitution for class-based dependencies.
For example, let's define an interface and a class. This class can be
parameterized with some type and we consider this type a dependency.importabcfromtypingimportAny,Generic,TypeVarclassMyInterface(abc.ABC):@abc.abstractmethoddefgetval(self)->Any:..._T=TypeVar("_T",bound=MyInterface)classMyClass(Generic[_T]):# We don't know exact type, but we assume# that it can be used as a dependency.def__init__(self,resource:_T=Depends()):self.resource=resource@propertydefmy_value(self)->Any:returnself.resource.getval()Now let's create several implementation of defined interface:defgetstr()->str:return"strstr"defgetint()->int:return100classMyDep1(MyInterface):def__init__(self,s:str=Depends(getstr))->None:self.s=sdefgetval(self)->str:returnself.sclassMyDep2(MyInterface):def__init__(self,i:int=Depends(getint))->None:self.i=idefgetval(self)->int:returnself.iNow you can use these dependencies by just setting proper type hints.defmy_target(d1:MyClass[MyDep1]=Depends(),d2:MyClass[MyDep2]=Depends(),)->None:print(d1.my_value)print(d2.my_value)withDependencyGraph(my_target).sync_ctx()asctx:my_target(**ctx.resolve_kwargs())This code will is going to print:strstr
100Dependencies replacementYou can replace dependencies in runtime, it will recalculate graph
and will execute your function with updated dependencies.!!! This functionality tremendously slows down dependency resolution.Use this functionality only for tests. Otherwise, you will end up building dependency graphs on every resolution request. Which is very slow.But for tests it may be a game changer, since you don't want to change your code, but some dependencies instead.Here's an example. Imagine you have a built graph for a specific function, like this:fromtaskiq_dependenciesimportDependencyGraph,Dependsdefdependency()->int:return1deftarget(dep_value:int=Depends(dependency))->None:assertdep_value==1graph=DependencyGraph(target)Normally, you would call the target, by writing something like this:withgraph.sync_ctx()asctx:target(**ctx.resolve_kwargs())But what if you want to replace dependency in runtime, just
before resolving kwargs? The solution is to addreplaced_depsparameter to the context method. For example:defreplaced()->int:return2withgraph.sync_ctx(replaced_deps={dependency:replaced})asctx:target(**ctx.resolve_kwargs())Furthermore, the new dependency can depend on other dependencies. Or you can change type of your dependency, like generator instead of plain return. Everything should work as you would expect it.Annotated typesTaskiq dependenices also support dependency injection through Annotated types.fromtypingimportAnnotatedasyncdefmy_function(dependency:Annotated[int,Depends(my_func)]):passOr you can specify classesfromtypingimportAnnotatedclassMyClass:passasyncdefmy_function(dependency:Annotated[MyClass,Depends(my_func)]):passAnd, of course you can easily save such type aliases in variables.fromtypingimportAnnotatedDepType=Annotated[int,Depends(my_func)]defmy_function(dependency:DepType):passAlso we support overrides for annotated types.For example:fromtypingimportAnnotatedDepType=Annotated[int,Depends(my_func)]defmy_function(dependency:DepType,no_cache_dep:Annotated[DepType,Depends(my_func,use_cache=False)],)->None:passAlso, please note that if you're usingfrom __future__ import annotationsit won't work for python <= 3.9. Because theinspect.signaturefunction doesn't support it. In all future versions it will work as expected.