Package: autowired
autowiredA minimalistic dependency injection library for Python.InstallationpipinstallautowiredQuick StartThe heart ofautowiredis theContextclass.You can think of it as a declarative dependency injection container.Let's look at an example.We start by defining some components in plain Python:classComponentA:defhello(self,name:str):print(f"Hello,{name}!")classComponentB:defgoodbye(self,name:str):print(f"Goodbye,{name}!")classMainComponent:def__init__(self,component_a:ComponentA,component_b:ComponentB):self.component_a=component_aself.component_b=component_bdefrun(self):self.component_a.hello("World")self.component_b.goodbye("World")Next, we'll define theContextclass.In our application code, we only need to interact with theMainComponent, hence it's the only component we explicitly
define.fromautowiredimportContext,autowiredclassApplicationContext(Context):main_component:MainComponent=autowired()Finally, we can utilize it in our application code:ctx=ApplicationContext()ctx.main_component.run()In this example,autowiredwas able to resolve all dependencies automatically.
However, in most real-world applications, you will need more control over the instantiation process.
The following sections will explain all the necessary concepts and advanced features in more detail.Core PrinciplesDependency InjectionReusability, maintainability, and testability are important aspects of code quality.
One technique commonly used for achieving this is Dependency Injection (DI).In essence, DI is about decoupling the creation of objects from their usage.
It encourages a system where dependencies are not built internally,
but provided (or 'injected') externally.
This approach offers the flexibility to replace dependencies without altering the classes using them.While some might associate DI with complex frameworks, it's primarily a simple but effective design pattern.A simple example:Without DI:classTextWriter:defwrite(self,text:str):print(text)classPoet:def__init__(self):self.writer=TextWriter()defwrite_poem(self):self.writer.write("Roses are red, violets are blue...")Here, thePoetclass is tightly coupled to theTextWriterclass.
If we wanted to use a different writer, we would have to change thePoetclass.With DI:classTextWriter:defwrite(self,text:str):print(text)classPoet:def__init__(self,writer:TextWriter):self.writer=writerdefwrite_poem(self):self.writer.write("Roses are red, violets are blue...")Here, thePoetclass is decoupled from theTextWriterclass. It can now interact with any class that implements
theTextWriterinterface without necessitating changes to thePoetclass itself. This elevates the flexibility and
reusability of thePoetclass. Moreover, it simplifies testing, as thePoetcan now be easily tested in isolation
from theTextWriter.Since Dependency Injection relieves the class from creating its own dependencies, these now need to be provided
from the outside.
This naturally leads to the question: Who takes up this responsibility?In a simple application, this could be the main function. It could be responsible for reading the configuration and
instantiating all the necessary components with the correct dependencies.
This is sufficient for small applications, but it quickly becomes unwieldy as the application grows.
This is especially true if you have multiple entry points and need to reuse the same instantiation logic in different
places such as in a CLI, a web app, or a test suite.To resolve this, one clean approach is to create a central class that takes over this responsibility and allows access
to all the necessary components. These components could be presented as properties of this class, each tied to the
others during instantiation.Typically, it's preferable for multiple components to share the same instance (often called
asingleton) of a specific
dependency. In such cases, Python’s built-incached_propertydecorator is an ideal solution. It functions by saving
the result of a property's initial call and then returns this cached value for any subsequent calls.This effectively provides all that's needed for a simple but elegant and reusable form of Dependency Injection in
Python.Let's look at a more concrete example:fromdataclassesimportdataclass# define some componentsclassMessageService:defsend_message(self,user:str,message:str):print(f"Sending message '{message}' to user '{user}'")classUserService:defget_user(self,user_id:int):returnf"User{user_id}"@dataclassclassNotificationService:message_service:MessageServiceuser_service:UserServiceall_caps:bool=Falsedefsend_notification(self,user_id:int,message:str):user=self.user_service.get_user(user_id)ifself.all_caps:message=message.upper()self.message_service.send_message(user,message)@dataclassclassNotificationController:notification_service:NotificationServicedefnotify(self,user_id:int,message:str):print(f"Sending notification to user{user_id}")self.notification_service.send_notification(user_id,message)And now we create a central class that ties everything together.fromfunctoolsimportcached_propertyclassApplicationContext:@cached_propertydefmessage_service(self)->MessageService:returnMessageService()@cached_propertydefuser_service(self)->UserService:returnUserService()@cached_propertydefnotification_service(self)->NotificationService:returnNotificationService(message_service=self.message_service,user_service=self.user_service)@cached_propertydefnotification_controller(self)->NotificationController:returnNotificationController(notification_service=self.notification_service)ctx=ApplicationContext()ctx.notification_controller.notify(1,"Hello, User!")In this setup, theApplicationContextis responsible for managing the dependencies between components. Using
thecached_propertydecorator ensures that each component is instantiated only once, even if it's accessed multiple
times.This approach is already sufficient for many simple applications.
However, as the application becomes larger and more complex, the context class can quickly become bloated.
You'll have more components, increasing interdependencies, and you'll need to
carefully manage the differing life cycles or scopes of each component (e.g. request scoped components, session-scoped
ones, etc.).
As the complexity grows, so does the amount of boilerplate code needed, making it harder to maintain and
increasing the risk for errors.Autowiredaims to streamline this process, while building on the same simple principles.Using autowiredHere's how the previousApplicationContextcould be rewritten usingautowired:fromautowiredimportContext,autowiredclassApplicationContext(Context):notification_controller:NotificationController=autowired()We have simplified the context class to a single line of code.
As theNotificationControllerwas the only component
that needed to be exposed as a public property, it is the only one we explicitly define.Autowirednow handles the instantiation of all components and their dependencies for us.
Components can be either dataclasses or traditional classes, provided they are appropriately annotated with type hints
forautowiredto automatically resolve their dependencies.ConfigurationAutowired provides several ways to configure the instantiation of components within a context.
Some of them are more convenient, while others offer more flexibility.Leveragingcached_propertyandpropertymethodsUsingcached_propertyandpropertymethods is the most flexible way to configure the instantiation of
components, as it gives you full control over the process.
As mentioned before,autowiredbuilds on the idea of usingcached_propertyto implement the singleton pattern.
That's
whycached_propertyis a first-class citizen inautowired.
Whenautowiredresolves dependencies, it does not only respect otherautowiredfields but alsocached_propertyas well aspropertymethods.Here is an example of how to make use of this to configure theNotificationServicefrom the previous example:# We define a dataclass to represent our application settings@dataclassclassApplicationSettings:all_caps_notifications:bool=FalseclassApplicationContext(Context):notification_controller:NotificationController=autowired()# we add a constructor to the context class to allow passing in the settingsdef__init__(self,settings:ApplicationSettings=ApplicationSettings()):self.settings=settings@cached_propertydef_notification_service(self)->NotificationService:returnself.autowire(NotificationService,all_caps=self.settings.all_caps_notifications)Now, we can use the context class as before, with the added benefit of being able to configure the notification service
via theApplicationSettings.settings=ApplicationSettings(all_caps_notifications=True)ctx=ApplicationContext(settings=settings)ctx.notification_controller.notify(1,"Hello, User!")assertctx.notification_controller.notification_service.all_caps==TrueTheautowiremethod behaves very similarly to theautowiredfield, but it is meant to be used to
directly instantiate components, rather than to define them as fields.
Explicit dependencies can be passed as kwargs, as shown in the example above, while the remaining ones will be resolved
automatically as before.Configuring Autowired Fields with Context AttributesUsingcached_propertyandpropertyallows us to define our own factory functions for components.
However, for simple use cases, this is unnecessarily verbose.
To configure your autowired fields with attributes of the
context-instance, you can also directly reference these attributes in the field definition.Here is how you could rewrite the previous example:classApplicationContext(Context):settings:ApplicationSettings=provided()notification_controller:NotificationController=autowired()_notification_service:NotificationService=autowired(all_caps=settings.all_caps_notifications)def__init__(self,settings:ApplicationSettings=ApplicationSettings()):self.settings=settingsTo make the settings field available in the autowired field definition, we need to define it explicitly.
Note that we useprovided()instead ofautowired()because the field is manually set in the constructor.Which of the two approaches you prefer is a matter of taste or the complexity of evaluating the settings. For simple
settings, the second approach should be preferred.
For more complex rules, thecached_propertyapproach might be more suitable. Both approaches can be mixed freely.Advanced Configuration with Kwargs Factory FunctionFor more complex configuration scenarios, you can use a kwargs factory function with autowired fields. This approach
provides a balance between simplicity and flexibility, allowing you to define custom logic for setting up your autowired
fields directly in the field definition.The factory function receives the context instance as its only argument during the component's instantiation.
This allows you to access any attribute of the context and use it in your configuration logic.
It should return a dictionary of kwargs that will be passed to the component's constructor.
As before, the remaining dependencies will be resolved automatically.Here's how you can apply it:classApplicationContext(Context):notification_controller:NotificationController=autowired()_notification_service:NotificationService=autowired(lambdaself:dict(all_caps=self.settings.all_caps_notifications))def__init__(self,settings:ApplicationSettings=ApplicationSettings()):self.settings=settingsAs always, you can freely mix and match the approaches within a single context class.Recap — The Building BlocksWe already covered the most important building blocks ofautowired.Contextserves as the base class for all classes that manage dependencies between components.autowired()defines autowired fields.@cached_propertyand@propertyoffer more control over the instantiation process.self.autowire()is a helper method for implementing@cached_propertyand@propertymethods on context classes.Advanced FeaturesComponent LifetimeBy default, components function as singletons, meaning the same instance is returned each time they're accessed or
injected from a context. However, situations may arise where a different lifetime for a component is required.Autowiredoffers three specific lifetimes within a context: singleton, transient, and thread-local. These can be
applied to both autowired fields and properties, as shown in the table below:LifetimeDescriptionAutowired SyntaxDecoratorSingletonSingle shared instance across the contextautowired()@cached_propertyTransientA new instance is created whenever accessed or injectedautowired(transient=True)@propertyThreadUnique instance per threadautowired(thread_local=True)@thread_local_cached_propertyWhile component lifetimes dictate the policy for instantiation of components within a particular context, determining
whether new instances are created or existing ones are reused, another essential dimension in component lifetime
management exists: the lifetime of the context itself.
The next sections will describe that in more detail.Scopes and Derived ContextsIn many applications, components can be bound to a specific scope.
A common example is a web application,
where some components are request-scoped, while others are session-scoped or application-scoped.
Often, these scopes follow a hierarchy; for example, a request scope is part of a session scope, which is part of the
application scope.While it's certainly possible to manage all these components within a single context, it can sometimes be beneficial to
break them up into multiple contexts.
Each context can then handle its own component instances, while drawing from the parent context if necessary.The next example demonstrates how this hierarchical structure can be implemented usingautowired.fromautowiredimportContext,autowired,providedimportjsonfromdataclassesimportdataclass# application scopeclassDatabaseService:def__init__(self,connection_string:str):self.connection_string=connection_stringdefget_api_keys(self):print(f"Fetching API keys from the database...")return["123","456",""]defget_user_data(self,user_id:str):print(f"Fetching data for user{user_id}from the database...")return{"name":"John Doe","email":"john.doe@example.com"}@dataclassclassApplicationSettings:db_connection_string:str="db://localhost"classApplicationContext(Context):settings:ApplicationSettings=provided()database_service:DatabaseService=autowired(connection_string=settings.db_connection_string)def__init__(self,settings:ApplicationSettings):self.settings=settings# request scope@dataclassclassHttpRequest:headers:dict[str,str]parameters:dict[str,str]classHttpRequestHandler:def__init__(self,database_service:DatabaseService,http_request:HttpRequest):self.database_service=database_serviceself.http_request=http_requestdefhandle_request(self)->str:api_key=self.http_request.headers.get("Authorization")or""ifapi_keyinself.database_service.get_api_keys():print("User is authorised")user_id=self.http_request.parameters.get("user_id")user_data=self.database_service.get_user_data(user_id)returnjson.dumps(user_data)else:raiseException("Not authorised")classRequestContext(Context):http_request:HttpRequest=provided()http_request_handler:HttpRequestHandler=autowired()def__init__(self,parent_context:Context,http_request:HttpRequest):self.derive_from(parent_context)self.http_request=http_request# example usagesettings=ApplicationSettings(db_connection_string="db://localhost")app_context=ApplicationContext(settings)# Create a dummy HTTP requesthttp_request=HttpRequest(headers={"Authorization":"123"},parameters={"user_id":"1"})# Create a request context for the dummy requestrequest_context=RequestContext(app_context,http_request)# Use the HttpRequestHandler to handle the requestresponse=request_context.http_request_handler.handle_request()print(response)Eager and Lazy InstantiationBy default,autowired()fields behave likecached_propertys and are instantiated lazily,
i.e., the first time they are accessed.
If this is not the desired behavior, you can use theeagerparameter to force eager instantiation of the component.classApplicationContext(Context):notification_controller:NotificationController=autowired(eager=True)The ContainerMost of the time, using theContextclass is sufficient for managing dependencies between components.
However, since it requires knowing upfront which components will be needed, it might not be suitable for all use cases.
Therefore, if you need more flexibility, you can use theContainerclass instead.
You can instantiate a container yourself or access a context's container via thecontainerproperty.fromautowiredimportContainerclassMessageService:defsend_message(self,user:str,message:str):print(f"Sending message '{message}' to user '{user}'")classUserService:defget_user(self,user_id:int):returnf"User{user_id}"classNotificationService:def__init__(self,message_service:MessageService,user_service:UserService):self.message_service=message_serviceself.user_service=user_servicedefsend_notification(self,user_id:int,message:str):user=self.user_service.get_user(user_id)self.message_service.send_message(user,message)container=Container()notification_service=container.resolve(NotificationService)assertisinstance(notification_service,NotificationService)assertnotification_serviceiscontainer.resolve(NotificationService)assertnotification_service.message_serviceiscontainer.resolve(MessageService)For more information on how to use theContainerclass, refer to its code documentation.ProviderA container can contain a list of providers (instances of theProviderclass).
A provider is what actually creates the instances of a component.
Most of the time, especially when using theContextclass, you don't need to worry about providers, as they are
created automatically.
TheProviderclass defines a simple interface that theContainerclass uses to resolve dependencies.classProvider(Generic[T]):defsatisfies(self,dependency:Dependency)->bool:# Checks whether the provider can provide an instances that satisfies the given dependency specification....defget_instance(self,dependency:Dependency,container:Container)->T:# Returns an instance that satisfies the given dependency specification....defget_name(self)->str:# Each provider has a name. The container utilises it to resolve ambiguous dependencies....Most providers are singleton component providers, i.e., they always return the same instance whenget_instance()is
called.
In the above container usage example, when we resolved theNotificationServicefor the first time,
a singleton provider was
created automatically and added to the container.
However, you can also add providers manually.In most cases you use thefrom_supplierorfrom_instancefactory methods to create a provider,
but you can also implement your ownProvidersubclass.
In the following example, we use thefrom_supplierfactory method to create a transient provider for a customMessageServiceclass.fromautowiredimportContainer,Providercontainer=Container()classAllCapsMessageService(MessageService):defsend_message(self,user:str,message:str):super().send_message(user,message.upper())defcreate_message_service()->MessageService:returnAllCapsMessageService()# Using `from_supplier` calls the given supplier function each time# Note that the return type annotation on the supplier function is mandatory# unless you specify the type argument explicitlycontainer.add(Provider.from_supplier(create_message_service))assertisinstance(container.resolve(MessageService),AllCapsMessageService)assertcontainer.resolve(MessageService)isnotcontainer.resolve(MessageService)List InjectionSometimes, you might want to inject a list of all components that implement a specific interface.
This is especially useful when you want to implement a plugin system.fromautowiredimportContext,autowiredfromabcimportABC,abstractmethodclassPlugin(ABC):@abstractmethoddefrun(self):...classPluginA(Plugin):defrun(self):print("Plugin A")classPluginB(Plugin):defrun(self):print("Plugin B")classPluginManager:def__init__(self,plugins:list[Plugin]):self.plugins=pluginsdefrun_all(self):forplugininself.plugins:plugin.run()classApplicationContext(Context):plugin_manager:PluginManager=autowired()# usagectx=ApplicationContext()ctx.container.add(PluginA())ctx.container.add(PluginB())ctx.plugin_manager.run_all()Component ScanIn many applications, you might want to automatically discover all components in a specific package.
Like list injection, this can be useful for implementing a plugin system.
Another common use case is to automatically discover all controllers in a web application to easily set up routing.
You can use the@componentdecorator to mark a class as a component.
When you callcomponent_scan()on a container, it will automatically discover all components in the given package
and add them to the container.# my_module/controllers/controller.pyfromabcimportABC,abstractmethodclassController(ABC):@abstractmethoddefrun(self):...# my_module/controllers/controller1.pyfromautowiredimportcomponentfrom.controllerimportController@componentclassController1(Controller):defrun(self):print("Starting Controller 1")# my_module/controllers/controller2.pyfromautowiredimportcomponentfrom.controllerimportController@componentclassController2(Controller):defrun(self):print("Starting Controller 2")# my_module/main.pyfromautowiredimportContext,autowiredimportmy_module.controllersclassControllerManager:def__init__(self,controllers:list[Controller]):self.controllers=controllersdefstart(self):forcontrollerinself.controllers:controller.run()classApplicationContext(Context):controller_manager:ControllerManager=autowired()def__init__(self):# register all components from the my_module.controllers packageself.container.component_scan(my_module.controllers)# usagectx=ApplicationContext()ctx.controller_manager.start()