Package: django-boot-rhazes
RhazesADependency Injection and IoC containerlibrary for Python.This library was initially written only for Django framework, only to realize it does nothing Django specific!For Django, checkDjango Boot Core Starter.Versions and RequirementsWritten and tested using python 3.9, should work on 3.6+.How it worksRhazes works byscanningfor bean (AKA services) classes available in the project. Ideally to make the scan faster, you shall define the packages you want to scan in a configuration inside settings.py (explained in usage section).Scanning for bean classes works by creating graphs of bean classes and their dependencies and choose random nodes to do DFS traversal in order to find edges and possible dependency cycles. Then from edges back to the top, there will bebuilderfunctions created and registered inApplicationContext(which is a static class or a singleton) for a specific bean class or interface. A builder is a function that acceptsApplicationContextand returns an instance of a bean class (possibly singleton instance). The builder can useApplicationContextto access other dependent beans, and this is only possible since we cover dependency graph edges first and then go up in the graph.Eventually, all bean classes will have a builder registered inApplicationContext. You can directly askApplicationContextfor a bean instance of specific type, or you can use@injectdecorator, so they are automatically injected into your classes/functions.Usage and ExampleLet's assume we havebeanclasses like below in a package namedapp1.services:fromabcimportABC,abstractmethodfromrhazes.decoratorimportbeanclassUserStorage(ABC):@abstractmethoddefget_user(user_id:int):pass@bean(_for=UserStorage,primary=False)# primary is False by default tooclassDatabaseUserStorage(UserStorage):defget_user(user_id:int):returnNone@bean(_for=UserStorage,primary=True)# set as primary implementation of UserStorageclassCacheUserStorage(UserStorage):defget_user(user_id:int):returnNone@bean()classProductManager:def__init__(self,user_storage:UserStorage):self.user_storage=user_storagedefget_user_products(user_id):user=self.user_storage.get_user(user_id)# Do something to find products of user?Now assuming you have the above classes defined user some packages that will be scanned by Rhazes, you can access them like this:fromrhazes.contextimportApplicationContextfromsomepackageimportUserStorage,DatabaseUserStorage,CacheUserStorage,ProductManagerapplication_context=ApplicationContext# scan packages and initialize beansapplication_context.initialize(["app1.services"])# Get ProductManager bean using its classproduct_manager:ProductManager=application_context.get_bean(ProductManager)# Get UserStorage (interface) bean# this will be CacheUserStorage implementation since primary was set to trueuser_storage:UserStorage=application_context.get_bean(UserStorage)# Specifically get beans of classes (not the interface)cache_user_storage:CacheUserStorage=application_context.get_bean(CacheUserStorage)# to directly get CacheUserStoragedatabase_user_storage:DatabaseUserStorage=application_context.get_bean(DatabaseUserStorage)# to directly get DatabaseUserStorageImportant note: in order for Rhazes to understand the dependency of the beans and be able to graph ityou have to use type hints.This means that Rhazes fails to understand the type of dependency (service_a) of such bean:@bean()classServiceB:def__init__(self,service_a):# Rhazes can't guess what should be injected herepassAs a general rule, use type hints anywhere you expect Rhazes to do something for you!Bean factoryBean factories are just classes thatproducea bean. They are beans themselves!fromrhazes.protocolimportBeanFactory@bean()classSomeBeanFactory(BeanFactory):# optional: if you haven't defined "_for" in @bean, you can determine it here@classmethoddefproduces(cls):returnSomeBeandefproduce(self):returnSomeBean()Note: Factory beans don't obeyprimarykeyword. Assure that you have single factory for a class or an interface, or the behaviour may be nondeterministic.You can also use_forkeyword of@beaninstead of implementingproduces(cls)method.
In face the default implementation ofproducesmethod checks for_forkeyword.
Quickest usage however is to implement the method.SingletonYou can define beans as singleton.fromrhazes.scopeimportScope@bean(scope=Scope.SINGLETON)classSomeBean:passAt this point this bean will always be the same instance when being injected into another class (another bean or@inject(read further))Lazy Bean DependenciesIf the bean you are defining is depended on another bean but you don't want to immediately instantiate that other bean you can mark it as lazy.@bean()classDependencyA:pass@bean(lazy_dependencies=[DependencyA])classDependencyB:def__int__(self,dependency_a:DependencyA):self.dependency_a=dependency_aNowdependency_awill not be instantiated (built) until there is a call to it from insideDependencyBinstances.InjectionYou can inject beans intofunctionsorclassesas long as your function (or class__init__function) has good support for**kwargs.These classes or functions need to be called with determined input parameter names. Example:@bean()classSomeBean:pass@inject()deffunction(bean:SomeBean,random_input:str):...# You can call it like this:function(random_input="something")# `bean` will be injected automaticallyExample for classes:@bean()classSomeBean:passclassMyClazz:@inject()def__init__(self,bean:SomeBean,random_input:str):...MyClazz(random_input="something")# `bean` will be injected automaticallyTo explicitly inject some beans and not others:@bean()classSomeBean1:pass@bean()classSomeBean2:pass@inject(injections=[SomeBean1])deffunction(bean1:SomeBean1,bean2:SomeBean2,random_input:str):...# You can call it like this:function(bean2=SomeBean2(),random_input="something")# `bean1` will be injected automaticallyWhen to initializeApplicationContextIt really depends on what sort of application you are writing.For example in Django, Application Context can be initialized either in a.ready()method of an app in your Django project, or in mainurls.py.Dependency CyclesDependency cycles are detected during Application Context initialization and will raise error. So you cant have beans like below:@bean()classServiceA:def__init__(self,service_b:"ServiceB"):pass@bean()classServiceB:def__init__(self,service_a:"ServiceA"):passOverride beans for testsIn case you need to override a bean class in a test case method you can use one of the following ways:fromrhazes.test.contextimportTemporaryContext,TemporaryContextManager# Way 1: using TemporaryContexttemporary_context=TemporaryContext()mock=Mock()temporary_context.register_bean(SomeInterface,mock)# your other code here# Call this in the end:temporary_context.reset()# Way 2withTemporaryContextManager()astemporary_context:mock=Mock()temporary_context.register_bean(SomeInterface,mock)# your other tests hereCustom bean builderIn case you need to override or introduce new bean builders (example, adding a bean builder that creates a new bean per each thread but uses same reference of a bean per each thread) you can do it by implementingBeanBuilderStrategyclass.fromrhazes.protocolimportBeanBuilderStrategyclassCustomBeanBuilderStrategy(BeanBuilderStrategy):defexecute(self)->object:pass# Your implementation hereYou can useself.nodeto see whichNodeis being implemented:DependencyNode class referenceYou can useself.metadatato see metadata information of the node (bean):DependencyNodeMetadata class referenceThe default implementation isDefaultBeanBuilderStrategyavailable atrhazes.bean_builder.DefaultBeanBuilderStrategy.Eventually the new strategy class can be used in the bean:@bean(scope=CustomBeanBuilderStrategy)classSomeBean:passContributionRead thecontribution guidelines.