yandil


Nameyandil JSON
Version 0.4.0 PyPI version JSON
download
home_pagehttps://github.com/DeejayRevok/yandil
SummaryYet ANother Dependency Injection Library
upload_time2023-07-22 16:18:16
maintainer
docs_urlNone
authorDeejayRevok
requires_python>=3.10.0,<3.11.0
licenseBSD-3-Clause
keywords dependency injection di
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # YANDIL
[![YANDIL CI](https://github.com/DeejayRevok/yandil/actions/workflows/pythonapp.yml/badge.svg?branch=main)](https://github.com/DeejayRevok/yandil/actions/workflows/pythonapp.yml)
[![PyPI version](https://badge.fury.io/py/yandil.svg)](https://pypi.org/project/yandil/)

## What 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 a **configurable 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 requirements
**At least all the __init__ methods 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 way
In 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.py
```
Having 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:

```python
from yandil.configuration.configuration_container import ConfigurationContainer
from yandil.container import Container
from yandil.loaders.self_discover_dependency_loader import SelfDiscoverDependencyLoader

configuration_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:

```python
from yandil.container import default_container
```

In 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 way
In 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.py
```
Having the src folder as the sources root folder (with PYTHONPATH properly configured).
If the first_package_first_module.py file contains the following code:

```python
from yandil.declarative.decorators import dependency


@dependency
class SimpleDependencyClass:
    pass
```
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:

```python
from yandil.loaders.declarative_dependency_loader import DeclarativeDependencyLoader

DeclarativeDependencyLoader(
    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:
```python
class ClassWithConfigurationValues:
    def __init__(self, first_config_var: str, second_config_var: int):
        self.first_config_var = first_config_var
        self.second_config_var = second_config_var
```

The following code will load the configuration values to be injected into the class:

```python
from yandil.configuration.configuration_container import ConfigurationContainer
from yandil.configuration.environment import Environment
from yandil.container import Container

configuration_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:

```python
from yandil.configuration.configuration_container import default_configuration_container
```

## How 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:

```python
from yandil.configuration.configuration_container import ConfigurationContainer
from yandil.container import Container

configuration_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:
```python
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:
```python
class ClassWithDependencies:
    def __init__(
            self,
            first_dependency: Optional[FirstDependency] = None,
            second_dependency: Optional[SecondDependency] = None
    ):
        self.first_dependency = first_dependency
        self.second_dependency = second_dependency
```

The following code allows the class to have the dependencies injected when it is instantiated:

```python
from yandil.configuration.configuration_container import ConfigurationContainer
from yandil.container import Container
from yandil.dependency_filler import DependencyFiller

configuration_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 injected
class_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 abstraction
Giving the following class:
```python
from abc import ABC
from typing import List


class AbstractClass(ABC):
    pass

class FirstImplementation(AbstractClass):
    pass

class SecondImplementation(AbstractClass):
    pass

class ClassWithAllAbstractionDependencies:
    def __init__(self, abstract_class_implementations: List[AbstractClass]):
        self.abstract_class_implementations = abstract_class_implementations
```
When 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 abstraction
In this scenario you need to define which is the primary implementation. Which could be achieved in the following ways:
```python
dependency_container.add(FirstImplementation, primary=True)
```
or
```python
@dependency(primary=True)
class FirstImplementation(AbstractClass):
    pass
```
In this way, when retrieving the ClassWithOneAbstractionDependency dependency, the dependency container will inject
that implementation of AbstractClass into the abstract_class_implementation argument.

## Extra features
- **Possibility 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.

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/DeejayRevok/yandil",
    "name": "yandil",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.10.0,<3.11.0",
    "maintainer_email": "",
    "keywords": "dependency injection,di",
    "author": "DeejayRevok",
    "author_email": "seryi_one@hotmail.com",
    "download_url": "https://files.pythonhosted.org/packages/3a/d5/624bc3744b31ad962f216b6a15ad0d34cb314ededf316d5b9123ea835f2a/yandil-0.4.0.tar.gz",
    "platform": null,
    "description": "# YANDIL\n[![YANDIL CI](https://github.com/DeejayRevok/yandil/actions/workflows/pythonapp.yml/badge.svg?branch=main)](https://github.com/DeejayRevok/yandil/actions/workflows/pythonapp.yml)\n[![PyPI version](https://badge.fury.io/py/yandil.svg)](https://pypi.org/project/yandil/)\n\n## What is YANDIL?\nYANDIL(**Yet ANother Dependency Injection Library**) is a python dependency injection library with two main aims:\n- **Decouple the application source code totally from the dependency injection schema**.\n- **Avoid the need of dependency injection definitions**.\n\n## How does it work?\nIn order to achieve the decoupling between the source code and the dependency injection,\nYANDIL uses two main strategies:\n- **Use the source code type hints to infer the dependency injection definitions**.\n- **Allow dependencies \"client classes\" to get the dependencies without using the dependency injection container itself**.\n\nOn the other hand, for achieving the aim of avoiding the need of dependency injection definitions\nYANDIL provides a **configurable way of scanning the source code searching for candidate components of the application**.\n\nAlternatively, in order to allow more control over the dependency injection it provides simple ways\nto define explicitly the dependency injection definitions.\n\n## Code requirements\n**At least all the __init__ methods of the classes which will be used as dependencies should be properly type hinted\nusing the python typing module**.\n\n## How to load dependencies?\n\n### Non-declarative way\nIn this way, it is assumed that all the classes which meets some conditions should be loaded as dependencies.\n\nGiving the following python project structure:\n```\n\u251c\u2500\u2500 app\n\u2502   \u2502   \u251c\u2500\u2500 dependency_injection_configuration.py\n\u251c\u2500\u2500 src\n\u2502   \u251c\u2500\u2500 first_package\n\u2502   \u2502   \u251c\u2500\u2500 first_package_first_module.py\n\u2502   \u2502   \u251c\u2500\u2500 first_package_second_module.py\n\u2502   \u251c\u2500\u2500 second_package\n\u2502   \u2502   \u251c\u2500\u2500 second_package_first_module.py\n```\nHaving the src folder as the sources root folder (with PYTHONPATH properly configured).\nThe following code placed inside the dependency_injection_configuration.py file\nwill load all the classes defined inside the first_package folder as dependencies\ninto the dependency injection container:\n\n```python\nfrom yandil.configuration.configuration_container import ConfigurationContainer\nfrom yandil.container import Container\nfrom yandil.loaders.self_discover_dependency_loader import SelfDiscoverDependencyLoader\n\nconfiguration_container = ConfigurationContainer()\ndependency_container = Container(\n    configuration_container=configuration_container,\n)\n\nSelfDiscoverDependencyLoader(\n    discovery_base_path=\"src\",\n    sources_root_path=\"src\",\n    should_exclude_classes_without_public_methods=False,\n    should_exclude_dataclasses=False,\n    container=dependency_container,\n).load()\n```\nTypically, you would want to load all the classes defined inside the src folder as dependencies,\nwhich can be achieved just setting the discovery_base_path same as the sources_root_path.\n\nIf you do not want to manage the dependency_container you can just leave it empty and the dependencies will be loaded\ninto the library default_container which can be accessed through the following import:\n\n```python\nfrom yandil.container import default_container\n```\n\nIn order to tune the dependency injection discovery process the loader has the following options:\n- **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.\n- **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.\n- **should_exclude_dataclasses**: If set to True, dataclasses will be excluded from the dependency injection discovery process.\n- **mandatory_modules**: Set of module paths which classes contained should be loaded as dependencies without checking if they meet any condition.\n- **should_exclude_exceptions**: If set to True, exceptions will be excluded from the dependency injection discovery process.\n\n### Declarative way\nIn this way you will need to decorate the classes which you want to be loaded as dependencies.\n\nGiving the following python project structure:\n```\n\u251c\u2500\u2500 app\n\u2502   \u2502   \u251c\u2500\u2500 dependency_injection_configuration.py\n\u251c\u2500\u2500 src\n\u2502   \u251c\u2500\u2500 first_package\n\u2502   \u2502   \u251c\u2500\u2500 first_package_first_module.py\n```\nHaving the src folder as the sources root folder (with PYTHONPATH properly configured).\nIf the first_package_first_module.py file contains the following code:\n\n```python\nfrom yandil.declarative.decorators import dependency\n\n\n@dependency\nclass SimpleDependencyClass:\n    pass\n```\nThe following code placed inside the dependency_injection_configuration.py file\nwill load all the classes defined inside the first_package folder as dependencies\ninto the dependency injection container:\n\n```python\nfrom yandil.loaders.declarative_dependency_loader import DeclarativeDependencyLoader\n\nDeclarativeDependencyLoader(\n    discovery_base_path=\"../src/first_package\",\n    sources_root_path=\"../src\",\n).load()\n```\nTake into account that this way of work will load the decorated class as dependencies in the default dependency container.\n\n## How to load the configuration values?\nIn some scenarios, components of the application will need to depend on some specific literal values\ncoming for example from environment variables.\n\nGiving the following class:\n```python\nclass ClassWithConfigurationValues:\n    def __init__(self, first_config_var: str, second_config_var: int):\n        self.first_config_var = first_config_var\n        self.second_config_var = second_config_var\n```\n\nThe following code will load the configuration values to be injected into the class:\n\n```python\nfrom yandil.configuration.configuration_container import ConfigurationContainer\nfrom yandil.configuration.environment import Environment\nfrom yandil.container import Container\n\nconfiguration_container = ConfigurationContainer()\ndependency_container = Container(\n    configuration_container=configuration_container,\n)\n\ndependency_container.add(ClassWithConfigurationValues)\n\nconfiguration_container[\"first_config_var\"] = \"first_config_var_value\"\nconfiguration_container[\"second_config_var\"] = Environment(\n    \"YANDIL_EXAMPLE_SECOND_CONFIG_ENV_VAR\",\n)\n```\n\nWith the previous setup, when the ClassWithConfigurationValues dependency is retrieved,\nthe first_config_var will be filled with first_config_var_value and the second_config_var\nwill be filled with the value of the environment variable YANDIL_EXAMPLE_SECOND_CONFIG_ENV_VAR.\n\nAgain, if you do not want to manage the configuration container you can also use the default one with the following import:\n\n```python\nfrom yandil.configuration.configuration_container import default_configuration_container\n```\n\n## How to load fixed instances for specific classes?\nBy default, if a class is added to the dependency container using the add method, if will be instantiated lazily when used\nas well as its dependencies. But there could some scenarios where you need a class to have an specific instance,\nlike for example when creating connection objects for communicating with external systems.\n\nFor this scenario the following code will always use the same instance\nwhen the dependency for the class ClassWithConfigurationValues is requested:\n\n```python\nfrom yandil.configuration.configuration_container import ConfigurationContainer\nfrom yandil.container import Container\n\nconfiguration_container = ConfigurationContainer()\ndependency_container = Container(\n    configuration_container=configuration_container,\n)\n\ndependency_container[ClassWithConfigurationValues] = ClassWithConfigurationValues(\n    first_config_var=\"first_config_var_value\",\n    second_config_var=23,\n)\n```\n\n## How to retrieve the dependencies?\nThe following code will return the instance of the ClassWithConfigurationValues dependency:\n```python\ndependency_container[ClassWithConfigurationValues]\n```\nIf it is the first time the dependency is resolved its initialization arguments and recursively\ntheir initialization arguments will be resolved also in order to resolve the requested dependency.\n\n## How to retrieve dependencies without using the container itself?\nIn order to achieve the aim of fully decoupling the source code from the dependency injection library,\nwe are still missing the possibility of retrieving the dependencies without using the container itself.\n\nGiving the following class:\n```python\nclass ClassWithDependencies:\n    def __init__(\n            self,\n            first_dependency: Optional[FirstDependency] = None,\n            second_dependency: Optional[SecondDependency] = None\n    ):\n        self.first_dependency = first_dependency\n        self.second_dependency = second_dependency\n```\n\nThe following code allows the class to have the dependencies injected when it is instantiated:\n\n```python\nfrom yandil.configuration.configuration_container import ConfigurationContainer\nfrom yandil.container import Container\nfrom yandil.dependency_filler import DependencyFiller\n\nconfiguration_container = ConfigurationContainer()\ndependency_container = Container(\n    configuration_container=configuration_container,\n)\n\ndependency_container.add(FirstDependency)\ndependency_container.add(SecondDependency)\n\ndependency_filler = DependencyFiller(dependency_container)\ndependency_filler.fill(ClassWithDependencies)\n\n# At this point the class_with_dependencies instance will have the dependencies injected\nclass_with_dependencies = ClassWithDependencies()\n```\n\n## What about abstractions?\nYANDIL supports the usage of abstractions in the code and it will automatically inject the\nabstraction implementation class if the abstraction only has one implementation defined.\n\nIf the abstraction has multiple implementations defined there could be two scenarios:\n\n### Class depends on all the implementations of the abstraction\nGiving the following class:\n```python\nfrom abc import ABC\nfrom typing import List\n\n\nclass AbstractClass(ABC):\n    pass\n\nclass FirstImplementation(AbstractClass):\n    pass\n\nclass SecondImplementation(AbstractClass):\n    pass\n\nclass ClassWithAllAbstractionDependencies:\n    def __init__(self, abstract_class_implementations: List[AbstractClass]):\n        self.abstract_class_implementations = abstract_class_implementations\n```\nWhen retrieving the ClassWithAllAbstractionDependencies dependency, the dependency container will inject\nall the implementations of AbstractClass into the abstract_class_implementations argument.\n\n### Class depends on one implementation of the abstraction\nIn this scenario you need to define which is the primary implementation. Which could be achieved in the following ways:\n```python\ndependency_container.add(FirstImplementation, primary=True)\n```\nor\n```python\n@dependency(primary=True)\nclass FirstImplementation(AbstractClass):\n    pass\n```\nIn this way, when retrieving the ClassWithOneAbstractionDependency dependency, the dependency container will inject\nthat implementation of AbstractClass into the abstract_class_implementation argument.\n\n## Extra features\n- **Possibility 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.\n- **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.\n",
    "bugtrack_url": null,
    "license": "BSD-3-Clause",
    "summary": "Yet ANother Dependency Injection Library",
    "version": "0.4.0",
    "project_urls": {
        "Homepage": "https://github.com/DeejayRevok/yandil",
        "Repository": "https://github.com/DeejayRevok/yandil"
    },
    "split_keywords": [
        "dependency injection",
        "di"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "aef962627e07f42d2d493d2ab37b1ab569e68dc8fc917ee7c89c113ddc2e3642",
                "md5": "6b12ff2d904631f5d33b901e1613f073",
                "sha256": "208442b5639fa37fd39e241a35e99208e8a0d4650aaf84c2fbf5660b07164650"
            },
            "downloads": -1,
            "filename": "yandil-0.4.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "6b12ff2d904631f5d33b901e1613f073",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10.0,<3.11.0",
            "size": 18922,
            "upload_time": "2023-07-22T16:18:15",
            "upload_time_iso_8601": "2023-07-22T16:18:15.283685Z",
            "url": "https://files.pythonhosted.org/packages/ae/f9/62627e07f42d2d493d2ab37b1ab569e68dc8fc917ee7c89c113ddc2e3642/yandil-0.4.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "3ad5624bc3744b31ad962f216b6a15ad0d34cb314ededf316d5b9123ea835f2a",
                "md5": "a7d1ed6ebc886dc4da6b84245f1fe24f",
                "sha256": "c06ed3d5144e5aca744bdbf0da8768e89124c8149f07f38deed94745cc0da365"
            },
            "downloads": -1,
            "filename": "yandil-0.4.0.tar.gz",
            "has_sig": false,
            "md5_digest": "a7d1ed6ebc886dc4da6b84245f1fe24f",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10.0,<3.11.0",
            "size": 14679,
            "upload_time": "2023-07-22T16:18:16",
            "upload_time_iso_8601": "2023-07-22T16:18:16.985272Z",
            "url": "https://files.pythonhosted.org/packages/3a/d5/624bc3744b31ad962f216b6a15ad0d34cb314ededf316d5b9123ea835f2a/yandil-0.4.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-07-22 16:18:16",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "DeejayRevok",
    "github_project": "yandil",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "tox": true,
    "lcname": "yandil"
}
        
Elapsed time: 0.17234s