Package: andi
andimakes easy implementing custom dependency injection mechanisms
where dependencies are expressed using type annotations.andiis useful as a building block for frameworks, or as a library
which helps to implement dependency injection (thus the name -
ANnotation-based Dependency Injection).License is BSD 3-clause.Installationpip install andiandi requires Python >= 3.8.1.GoalSee the following classes that represents parts of a car
(and the car itself):classValves:passclassEngine:def__init__(self,valves):self.valves=valvesclassWheels:passclassCar:def__init__(self,engine,wheels):self.engine=engineself.wheels=wheelsThe following would be the usual way of build aCarinstance:valves=Valves()engine=Engine(valves)wheels=Wheels()car=Car(engine,wheels)There are some dependencies between the classes: A car requires
and engine and wheels to be built, as well as the engine requires
valves. These are the car dependencies and sub-dependencies.The question is, could we have an automatic way of building instances?
For example, could we have abuildfunction that
given theCarclass or any other class would return an instance
even if the class itself has some other dependencies?car=build(Car)# Andi helps creating this generic build functionandiinspect the dependency tree and creates a plan making easy creating
such abuildfunction.This is how this plan for theCarclass would looks like:InvokeValveswith empty argumentsInvokeEngineusing the instance created in 1 as the argumentvalvesInvokeWheelswith empty argumentsInvokeCarswith the instance created in 2 as theengineargument and with
the instance created in 3 as thewheelsargumentType annotationsBut there is a missing piece in the Car example before. How canandiknow that the classValvesis required to build the
argumentvalves? A first idea would be to use the argument
name as a hint for the class name
(aspinjectdoes),
butandiopts to rely on arguments’ type annotations instead.The classes forCarshould then be rewritten as:classValves:passclassEngine:def__init__(self,valves:Valves):self.valves=valvesclassWheels:passclassCar:def__init__(self,engine:Engine,wheels:Wheels):self.engine=engineself.wheels=wheelsNote how now there is a explicit annotation stating that thevalvesargument is of typeValves(same forengineandwheels).Theandi.planfunction can now create a plan to build theCarclass (ignore theis_injectableparameter by now):plan=andi.plan(Car,is_injectable={Engine,Wheels,Valves})This is what theplanvariable contains:[(Valves,{}),(Engine,{'valves':Valves}),(Wheels,{}),(Car,{'engine':Engine,'wheels':Wheels})]Note how this plan correspond exactly to the 4-steps plan described
in the previous section.Building from the planCreating a generic function to build the instances from
a plan generated byandiis then very easy:defbuild(plan):instances={}forfn_or_cls,kwargs_specinplan:instances[fn_or_cls]=fn_or_cls(**kwargs_spec.kwargs(instances))returninstancesSo let’s see putting all the pieces together. The following code
creates an instance ofCarusingandi:plan=andi.plan(Car,is_injectable={Engine,Wheels,Valves})instances=build(plan)car=instances[Car]is_injectableIt is not always desired forandito manage every single annotation found.
Instead is usually better to explicitly declare which types
can be handled byandi. The argumentis_injectableallows to customize this feature.andiwill raise an error on the presence of a dependency that cannot be resolved
because it is not injectable.Usually is desirable to declare injectabilty by
creating a base class to inherit from. For example,
we could create a base classInjectableas base
class for the car components:classInjectable(ABC):passclassValves(Injectable):passclassEngine(Injectable):def__init__(self,valves:Valves):self.valves=valvesclassWheels(Injectable):passThe call toandi.planwould then be:is_injectable=lambdacls:issubclass(cls,Injectable)plan=andi.plan(Car,is_injectable=is_injectable)Functions and methodsDependency injection is also very useful when applied to functions.
Imagine that you have a functiondrivethat drives theCarthrough theRoad:classRoad(Injectable):...defdrive(car:Car,road:Road,speed):...# Drive the car through the roadThe dependencies has to be resolved before invoking
thedrivefunction:plan=andi.plan(drive,is_injectable=is_injectable)instances=build(plan.dependencies)Now thedrivefunction can be invoked:drive(instances[Car],instances[Road],100)Note thatspeedargument was not annotated. The resultant plan just won’t include it
because theandi.planfull_final_kwargsparameter isFalseby default. Otherwise, an exception would have been raised (seefull_final_kwargsargument
documentation for more information).An alternative and more generic way to invoke the drive function
would be:drive(speed=100,**plan.final_kwargs(instances))dataclasses and attrsandisupports classes defined usingattrsand alsodataclasses.
For example theCarclass could have been defined as:# attrs class example@attr.s(auto_attribs=True)classCar:engine:Enginewheels:Wheels# dataclass example@dataclassclassCar(Injectable):engine:Enginewheels:WheelsUsingattrsordataclassis handy because they avoid
some boilerplate.Externally provided dependenciesRetaining the control over object instantiation
could be desired in some cases. For example creating
a database connection could require accessing some
credentials registry or getting the connection from a pool
so you might want to control building
such instances outside of the regular
dependency injection mechanism.andi.planallows to specify which types would be
externally provided. Let’s see an example:classDBConnection(ABC):@abstractmethoddefgetConn():pass@dataclassclassUsersDAO:conn:DBConnectiondefgetUsers():returnself.conn.query("SELECT * FROM USERS")UsersDAOrequires a database connection to run queries.
But the connection will be provided externally from a pool, so we
call thenandi.planusing also theexternally_providedparameter:plan=andi.plan(UsersDAO,is_injectable=is_injectable,externally_provided={DBConnection})The build method should then be modified slightly to be able
to inject externally provided instances:defbuild(plan,instances_stock=None):instances_stock=instances_stockor{}instances={}forfn_or_cls,kwargs_specinplan:iffn_or_clsininstances_stock:instances[fn_or_cls]=instances_stock[fn_or_cls]else:instances[fn_or_cls]=fn_or_cls(**kwargs_spec.kwargs(instances))returninstancesNow we are ready to createUserDAOinstances withandi:plan=andi.plan(UsersDAO,is_injectable=is_injectable,externally_provided={DBConnection})dbconnection=DBPool.get_connection()instances=build(plan.dependencies,{DBConnection:dbconnection})users_dao=instances[UsersDAO]users=user_dao.getUsers()Note that being injectable is not required for externally provided
dependencies.OptionalOptionaltype annotations can be used in case of
dependencies that can be optional. For example:@dataclassclassDashboard:conn:Optional[DBConnection]defshowPage():ifself.conn:self.conn.query("INSERT INTO VISITS ...")...# renders a HTML pageIn this example, theDashboardclass generates a HTML page to be served, and
also stores the number of visits into a database. Database
could be absent in some environments, but you might want
the dashboard to work even if it cannot log the visits.When a database connection is possible the plan call would be:plan=andi.plan(UsersDAO,is_injectable=is_injectable,externally_provided={DBConnection})And the following when the connection is absent:plan=andi.plan(UsersDAO,is_injectable=is_injectable,externally_provided={})It is also required to register the type ofNoneas injectable. Otherwiseandi.planwith raise an exception
saying that “NoneType is not injectable”.Injectable.register(type(None))UnionUnioncan also be used to express alternatives. For example:@dataclassclassUsersDAO:conn:Union[ProductionDBConnection,DevelopmentDBConnection]DevelopmentDBConnectionwill be injected in the absence ofProductionDBConnection.AnnotatedOn Python 3.9+Annotatedtype annotations can be used to attach arbitrary
metadata that will be preserved in the plan. Occurrences of the same type
annotated with different metadata will not be considered duplicates. For
example:@dataclassclassDashboard:conn_main:Annotated[DBConnection,"main DB"]conn_stats:Annotated[DBConnection,"stats DB"]The plan will contain both dependencies.Custom buildersSometimes a dependency can’t be created directly but needs some additional code
to be built. And that code can also have its own dependencies:classWheels:passdefwheel_factory(wheel_builder:WheelBuilder)->Wheels:returnwheel_builder.get_wheels()As by defaultandican’t know how to create aWheelsinstance or that
the plan needs to create aWheelBuilderinstance first, it needs to be told
this with acustom_builder_fnargument:custom_builders={Wheels:wheel_factory,}plan=andi.plan(Car,is_injectable={Engine,Wheels,Valves},custom_builder_fn=custom_builders.get,)custom_builder_fnshould be a function that takes a type and returns a factory
for that type.The build code also needs to know how to buildWheelsinstances. A plan step
for an object built with a custom builder uses an instance of theandi.CustomBuilderwrapper that contains the type to be built in theresult_class_or_fnattribute and
the callable for building it in thefactoryattribute:fromandiimportCustomBuilderdefbuild(plan):instances={}forfn_or_cls,kwargs_specinplan:ifisinstance(fn_or_cls,CustomBuilder):instances[fn_or_cls.result_class_or_fn]=fn_or_cls.factory(**kwargs_spec.kwargs(instances))else:instances[fn_or_cls]=fn_or_cls(**kwargs_spec.kwargs(instances))returninstancesFull final kwargs modeBy defaultandi.planwon’t fail if it is not able to provide
some of the direct dependencies for the given input (see thespeedargument in one of the examples above).This behaviour is desired when inspecting functions
for which is already known that some arguments won’t be
injectable but they will be provided by other means
(like thedrivefunction above).But in other cases is better to be sure that all dependencies
are fulfilled and otherwise fail. Such is the case for classes.
So it is recommended to setfull_final_kwargs=Truewhen invokingandi.planfor classes.OverridesLet’s go back to theCarexample. Imagine you want to build a car again.
But this time you want to replace theEnginebecause this is
going to be an electric car!. And of course, an electric engine contains a battery
and have no valves at all. This could be the newEngine:classBattery:passclassElectricEngine(Engine):def__init__(self,battery:Battery):self.battery=valvesAndi offers the possibility to replace dependencies when planning,
and this is what is required to build the electric car: we need
to replace any dependency onEngineby a dependency onElectricEngine.
This is exactly what overrides offers. Let’s see howplanshould
be invoked in this case:plan=andi.plan(Car,is_injectable=is_injectable,overrides={Engine:ElectricEngine}.get)Note that Andi will unroll the new dependencies properly. That is,ValvesandEnginewon’t be in the resultant plan butElectricEngineandBatterywill.In summary, overrides offers a way to override the default
dependencies anywhere in the tree, changing them with an
alternative one.By default overrides are not recursive: overrides aren’t applied
over the children of an already overridden dependency. There
is flag to turn recursion on if this is what is desired.
Checkandi.plandocumentation for more information.Why type annotations?andiuses type annotations to declare dependencies (inputs).
It has several advantages, and some limitations as well.Advantages:Built-in language feature.You’re not lying when specifying a type - these
annotations still work as usual type annotations.In many projects you’d annotate arguments anyways, soandisupport
is “for free”.Limitations:Callable can’t have two arguments of the same type.This feature could possibly conflict with regular type annotation usages.If your callable has two arguments of the same type, consider making them
different types. For example, a callable may receive url and html of
a web page:defparse(html:str,url:str):# ...To make it play well withandi, you may define separate types for url
and for html:classHTML(str):passclassURL(str):passdefparse(html:HTML,url:URL):# ...This is more boilerplate though.Why doesn’t andi handle creation of objects?Currentlyandijust inspects callable and chooses best concrete types
a framework needs to create and pass to a callable, without prescribing how
to create them. This makesandiuseful in various contexts - e.g.creation of some objects may require asynchronous functions, and it
may depend on libraries used (asyncio, twisted, etc.)in streaming architectures (e.g. based on Kafka) inspection may happen
on one machine, while creation of objects may happen on different nodes
in a distributed system, and then actually running a callable may happen on
yet another machine.It is hard to design API with enough flexibility for all such use cases.
That said,andimay provide more helpers in future,
once patterns emerge, even if they’re useful only in certain contexts.Examples: callback based frameworksSpider exampleNothing better than a example to understand howandican be useful.
Let’s imagine you want to implemented a callback based framework
for writing spiders to crawl web pages.The basic idea is that there is framework in which the user
can write spiders. Each spider is a collection of callbacks
that can process data from a page, emit extracted data or request new
pages. Then, there is an engine that takes care of downloading
the web pages
and invoking the user defined callbacks, chaining requests
with its corresponding callback.Let’s see an example of an spider to download recipes
from a cooking page:classMySpider(Spider):start_url="htttp://a_page_with_a_list_of_recipes"defparse(self,response):forurlinrecipes_urls_from_page(response)yieldRequest(url,callback=parse_recipe)defparse_recipe(self,response):yieldextract_recipe(response)It would be handy if the user can define some requirements
just by annotating parameters in the callbacks. Andandimake it
possible.For example, a particular callback could require access to the cookies:defparse(self,response:Response,cookies:CookieJar):# ... Do something with the response and the cookiesIn this case, the engine can useandito inspect theparsemethod, and
detect thatResponseandCookieJarare required.
Then the framework will build them and will invoke the callback.This functionality would serve to inject into the users callbacks
some components only when they are required.It could also serve to encapsulate better the user code. For
example, we could just decouple the recipe extraction into
it’s own class:@dataclassclassRecipeExtractor:response:Responsedefto_item():returnextract_recipe(self.response)The callback could then be defined as:defparse_recipe(extractor:RecipeExtractor):yieldextractor.to_item()Note how handy is that withandithe engine can create
an instance ofRecipesExtractorfeeding it with the
declaredResponsedependency.In definitive, usingandiin such a framework
can provide great flexibility to the user
and reduce boilerplate.Web server exampleandican be useful also for implementing a new
web framework.Let’s imagine a framework where you can declare your sever in a
class like the following:classMyWeb(Server):@route("/products")defproductspage(self,request:Request):...# return the composed page@route("/sales")defsalespage(self,request:Request):...# return the composed pageThe former case is composed of two endpoints, one for serving
a page with a summary of sales, and a second one to serve
the products list.Connection to the database can be required
to sever these pages. This logic could be encapsulated
in some classes:@dataclassclassProducts:conn:DBConnectiondefget_products()returnself.conn.query("SELECT ...")@dataclassclassSales:conn:DBConnectiondefget_sales()returnself.conn.query("SELECT ...")Nowproductspageandsalespagemethods can just declare
that they require these objects:classMyWeb(Server):@route("/products")defproductspage(self,request:Request,products:Products):...# return the composed page@route("/sales")defsalespage(self,request:Request,sales:Sales):...# return the composed pageAnd the framework can then be responsible to fulfill these
dependencies. The flexibility offered would be a great advantage.
As an example, if would be very easy to create a page that requires
both sales and products:@route("/overview")defproductspage(self,request:Request,products:Products,sales:Sales):...# return the composed overview pageContributingSource code:https://github.com/scrapinghub/andiIssue tracker:https://github.com/scrapinghub/andi/issuesUsetoxto run tests with different Python versions:toxThe command above also runs type checks; we use mypy.Changes0.6.0 (2023-12-26)Drop support for Python 3.5-3.7.Add support for dependencies that need to be built using custom callables.0.5.0 (2023-12-12)Add support for dependency metadata viatyping.Annotated(requires
Python 3.9+).Add docs for overrides.Add support for Python 3.10-3.12.CI improvements.0.4.1 (2021-02-11)Overrides support inandi.plan0.4.0 (2020-04-23)andi.inspectcan handle classes now (their__init__method
is inspected)andi.planandandi.inspectcan handle objects which are
callable via__call__method.0.3.0 (2020-04-03)andi.planfunction replacingandi.to_provide.Rewrite README explaining the new approach based inplanmethod.andi.inspectreturn non annotated arguments also.0.2.0 (2020-02-14)Better attrs support (workaround issue with string type annotations).Declare Python 3.8 support.More tests; ensure dataclasses support.0.1 (2019-08-28)Initial release.