Package: yandil
YANDILWhat is YANDIL?YANDIL(Yet ANother Dependency Injection Library) is a python dependency injection library with two main aims:Decouple the application source code totally from the dependency injection schema.Avoid the need of dependency injection definitions.How does it work?In order to achieve the decoupling between the source code and the dependency injection,
YANDIL uses two main strategies:Use the source code type hints to infer the dependency injection definitions.Allow dependencies "client classes" to get the dependencies without using the dependency injection container itself.On the other hand, for achieving the aim of avoiding the need of dependency injection definitions
YANDIL provides aconfigurable way of scanning the source code searching for candidate components of the application.Alternatively, in order to allow more control over the dependency injection it provides simple ways
to define explicitly the dependency injection definitions.Code requirementsAt least all theinitmethods of the classes which will be used as dependencies should be properly type hinted
using the python typing module.How to load dependencies?Non-declarative wayIn this way, it is assumed that all the classes which meets some conditions should be loaded as dependencies.Giving the following python project structure:├── app
│   │   ├── dependency_injection_configuration.py
├── src
│   ├── first_package
│   │   ├── first_package_first_module.py
│   │   ├── first_package_second_module.py
│   ├── second_package
│   │   ├── second_package_first_module.pyHaving the src folder as the sources root folder (with PYTHONPATH properly configured).
The following code placed inside the dependency_injection_configuration.py file
will load all the classes defined inside the first_package folder as dependencies
into the dependency injection container:fromyandil.configuration.configuration_containerimportConfigurationContainerfromyandil.containerimportContainerfromyandil.loaders.self_discover_dependency_loaderimportSelfDiscoverDependencyLoaderconfiguration_container=ConfigurationContainer()dependency_container=Container(configuration_container=configuration_container,)SelfDiscoverDependencyLoader(discovery_base_path="src",sources_root_path="src",should_exclude_classes_without_public_methods=False,should_exclude_dataclasses=False,container=dependency_container,).load()Typically, you would want to load all the classes defined inside the src folder as dependencies,
which can be achieved just setting the discovery_base_path same as the sources_root_path.If you do not want to manage the dependency_container you can just leave it empty and the dependencies will be loaded
into the library default_container which can be accessed through the following import:fromyandil.containerimportdefault_containerIn order to tune the dependency injection discovery process the loader has the following options:excluded_modules: Set of module or package names to be excluded from the dependency injection discovery process. For example for excluding the folder which contains the models in a web application.should_exclude_classes_without_public_methods: If set to True, classes which not define any public method will be excluded from the dependency injection discovery process. For example for excluding DTO classes.should_exclude_dataclasses: If set to True, dataclasses will be excluded from the dependency injection discovery process.mandatory_modules: Set of module paths which classes contained should be loaded as dependencies without checking if they meet any condition.should_exclude_exceptions: If set to True, exceptions will be excluded from the dependency injection discovery process.Declarative wayIn this way you will need to decorate the classes which you want to be loaded as dependencies.Giving the following python project structure:├── app
│   │   ├── dependency_injection_configuration.py
├── src
│   ├── first_package
│   │   ├── first_package_first_module.pyHaving the src folder as the sources root folder (with PYTHONPATH properly configured).
If the first_package_first_module.py file contains the following code:fromyandil.declarative.decoratorsimportdependency@dependencyclassSimpleDependencyClass:passThe following code placed inside the dependency_injection_configuration.py file
will load all the classes defined inside the first_package folder as dependencies
into the dependency injection container:fromyandil.loaders.declarative_dependency_loaderimportDeclarativeDependencyLoaderDeclarativeDependencyLoader(discovery_base_path="../src/first_package",sources_root_path="../src",).load()Take into account that this way of work will load the decorated class as dependencies in the default dependency container.How to load the configuration values?In some scenarios, components of the application will need to depend on some specific literal values
coming for example from environment variables.Giving the following class:classClassWithConfigurationValues:def__init__(self,first_config_var:str,second_config_var:int):self.first_config_var=first_config_varself.second_config_var=second_config_varThe following code will load the configuration values to be injected into the class:fromyandil.configuration.configuration_containerimportConfigurationContainerfromyandil.configuration.environmentimportEnvironmentfromyandil.containerimportContainerconfiguration_container=ConfigurationContainer()dependency_container=Container(configuration_container=configuration_container,)dependency_container.add(ClassWithConfigurationValues)configuration_container["first_config_var"]="first_config_var_value"configuration_container["second_config_var"]=Environment("YANDIL_EXAMPLE_SECOND_CONFIG_ENV_VAR",)With the previous setup, when the ClassWithConfigurationValues dependency is retrieved,
the first_config_var will be filled with first_config_var_value and the second_config_var
will be filled with the value of the environment variable YANDIL_EXAMPLE_SECOND_CONFIG_ENV_VAR.Again, if you do not want to manage the configuration container you can also use the default one with the following import:fromyandil.configuration.configuration_containerimportdefault_configuration_containerHow to load fixed instances for specific classes?By default, if a class is added to the dependency container using the add method, if will be instantiated lazily when used
as well as its dependencies. But there could some scenarios where you need a class to have an specific instance,
like for example when creating connection objects for communicating with external systems.For this scenario the following code will always use the same instance
when the dependency for the class ClassWithConfigurationValues is requested:fromyandil.configuration.configuration_containerimportConfigurationContainerfromyandil.containerimportContainerconfiguration_container=ConfigurationContainer()dependency_container=Container(configuration_container=configuration_container,)dependency_container[ClassWithConfigurationValues]=ClassWithConfigurationValues(first_config_var="first_config_var_value",second_config_var=23,)How to retrieve the dependencies?The following code will return the instance of the ClassWithConfigurationValues dependency:dependency_container[ClassWithConfigurationValues]If it is the first time the dependency is resolved its initialization arguments and recursively
their initialization arguments will be resolved also in order to resolve the requested dependency.How to retrieve dependencies without using the container itself?In order to achieve the aim of fully decoupling the source code from the dependency injection library,
we are still missing the possibility of retrieving the dependencies without using the container itself.Giving the following class:classClassWithDependencies:def__init__(self,first_dependency:Optional[FirstDependency]=None,second_dependency:Optional[SecondDependency]=None):self.first_dependency=first_dependencyself.second_dependency=second_dependencyThe following code allows the class to have the dependencies injected when it is instantiated:fromyandil.configuration.configuration_containerimportConfigurationContainerfromyandil.containerimportContainerfromyandil.dependency_fillerimportDependencyFillerconfiguration_container=ConfigurationContainer()dependency_container=Container(configuration_container=configuration_container,)dependency_container.add(FirstDependency)dependency_container.add(SecondDependency)dependency_filler=DependencyFiller(dependency_container)dependency_filler.fill(ClassWithDependencies)# At this point the class_with_dependencies instance will have the dependencies injectedclass_with_dependencies=ClassWithDependencies()What about abstractions?YANDIL supports the usage of abstractions in the code and it will automatically inject the
abstraction implementation class if the abstraction only has one implementation defined.If the abstraction has multiple implementations defined there could be two scenarios:Class depends on all the implementations of the abstractionGiving the following class:fromabcimportABCfromtypingimportListclassAbstractClass(ABC):passclassFirstImplementation(AbstractClass):passclassSecondImplementation(AbstractClass):passclassClassWithAllAbstractionDependencies:def__init__(self,abstract_class_implementations:List[AbstractClass]):self.abstract_class_implementations=abstract_class_implementationsWhen retrieving the ClassWithAllAbstractionDependencies dependency, the dependency container will inject
all the implementations of AbstractClass into the abstract_class_implementations argument.Class depends on one implementation of the abstractionIn this scenario you need to define which is the primary implementation. Which could be achieved in the following ways:dependency_container.add(FirstImplementation,primary=True)or@dependency(primary=True)classFirstImplementation(AbstractClass):passIn this way, when retrieving the ClassWithOneAbstractionDependency dependency, the dependency container will inject
that implementation of AbstractClass into the abstract_class_implementation argument.Extra featuresPossibility to define optional dependencies: In this scenario, if the dependency retrieved depends on an optional dependency not defined, then it will have the None value injected.Allow usage of keyword arguments: In this scenario, if the dependency retrieved depends on a keyword argument, and the argument type dependency is not defined, then it will have the default value of the keyword argument injected.