Skip to content

Commit

Permalink
Aggregate provider (#544)
Browse files Browse the repository at this point in the history
* Add implementation and typing stubs

* Add tests

* Add typing tests

* Refactor FactoryAggregate

* Update changelog

* Add Aggregate provider docs and example

* Update cross links between Aggregate, Selector, and FactoryAggregate docs

* Add wording improvements to the docs
  • Loading branch information
rmk135 committed Jan 10, 2022
1 parent cfadd8c commit 742e73a
Show file tree
Hide file tree
Showing 13 changed files with 12,785 additions and 11,330 deletions.
6 changes: 6 additions & 0 deletions docs/main/changelog.rst
Expand Up @@ -9,8 +9,14 @@ follows `Semantic versioning`_

Development version
-------------------
- Add new provider ``Aggregate``. It is a generalized version of ``FactoryAggregate`` that
can contain providers of any type, not only ``Factory``. See issue
`#530 <https://github.com/ets-labs/python-dependency-injector/issues/530>`_. Thanks to
`@zerlok (Danil Troshnev) <https://github.com/zerlok>`_ for suggesting the feature.
- Add argument ``as_`` to the ``config.from_env()`` method for the explicit type casting
of an environment variable value, e.g.: ``config.timeout.from_env("TIMEOUT", as_=int)``.
See issue `#533 <https://github.com/ets-labs/python-dependency-injector/issues/533>`_. Thanks to
`@gtors (Andrey Torsunov) <https://github.com/gtors>`_ for suggesting the feature.
- Add ``.providers`` attribute to the ``FactoryAggregate`` provider. It is an alias for
``FactoryAggregate.factories`` attribute.
- Add ``.set_providers()`` method to the ``FactoryAggregate`` provider. It is an alias for
Expand Down
72 changes: 72 additions & 0 deletions docs/providers/aggregate.rst
@@ -0,0 +1,72 @@
.. _aggregate-provider:

Aggregate provider
==================

.. meta::
:keywords: Python,DI,Dependency injection,IoC,Inversion of Control,Configuration,Injection,
Aggregate,Polymorphism,Environment Variable,Flexibility
:description: Aggregate provider aggregates other providers.
This page demonstrates how to implement the polymorphism and increase the
flexibility of your application using the Aggregate provider.

:py:class:`Aggregate` provider aggregates a group of other providers.

.. currentmodule:: dependency_injector.providers

.. literalinclude:: ../../examples/providers/aggregate.py
:language: python
:lines: 3-
:emphasize-lines: 24-27

Each provider in the ``Aggregate`` is associated with a key. You can call aggregated providers by providing
their key as a first argument. All positional and keyword arguments following the key will be forwarded to
the called provider:

.. code-block:: python
yaml_reader = container.config_readers("yaml", "./config.yml", foo=...)
You can also retrieve an aggregated provider by providing its key as an attribute name:

.. code-block:: python
yaml_reader = container.config_readers.yaml("./config.yml", foo=...)
To retrieve a dictionary of aggregated providers, use ``.providers`` attribute:

.. code-block:: python
container.config_readers.providers == {
"yaml": <YAML provider>,
"json": <JSON provider>,
}
.. note::
You can not override the ``Aggregate`` provider.

.. note::
When you inject the ``Aggregate`` provider, it is passed "as is".

To use non-string keys or string keys with ``.`` and ``-``, provide a dictionary as a positional argument:

.. code-block:: python
aggregate = providers.Aggregate({
SomeClass: providers.Factory(...),
"key.with.periods": providers.Factory(...),
"key-with-dashes": providers.Factory(...),
})
.. seealso::
:ref:`selector-provider` to make injections based on a configuration value, environment variable, or a result of a callable.

``Aggregate`` provider is different from the :ref:`selector-provider`. ``Aggregate`` provider doesn't select which provider
to inject and doesn't have a selector. It is a group of providers and is always injected "as is". The rest of the interface
of both providers is similar.

.. note::
``Aggregate`` provider is a successor of :ref:`factory-aggregate-provider` provider. ``Aggregate`` provider doesn't have
a restriction on the provider type, while ``FactoryAggregate`` aggregates only ``Factory`` providers.

.. disqus::
6 changes: 6 additions & 0 deletions docs/providers/factory.rst
Expand Up @@ -145,11 +145,17 @@ provider with two peculiarities:
:lines: 3-
:emphasize-lines: 34

.. _factory-aggregate-provider:

Factory aggregate
-----------------

:py:class:`FactoryAggregate` provider aggregates multiple factories.

.. seealso::
:ref:`aggregate-provider` – it's a successor of ``FactoryAggregate`` provider that can aggregate
any type of provider, not only ``Factory``.

The aggregated factories are associated with the string keys. When you call the
``FactoryAggregate`` you have to provide one of the these keys as a first argument.
``FactoryAggregate`` looks for the factory with a matching key and calls it with the rest of the arguments.
Expand Down
1 change: 1 addition & 0 deletions docs/providers/index.rst
Expand Up @@ -46,6 +46,7 @@ Providers module API docs - :py:mod:`dependency_injector.providers`
dict
configuration
resource
aggregate
selector
dependency
overriding
Expand Down
3 changes: 3 additions & 0 deletions docs/providers/selector.rst
Expand Up @@ -30,4 +30,7 @@ When a ``Selector`` provider is called, it gets a ``selector`` value and delegat
the provider with a matching name. The ``selector`` callable works as a switch: when the returned
value is changed the ``Selector`` provider will delegate the work to another provider.

.. seealso::
:ref:`aggregate-provider` to inject a group of providers.

.. disqus::
39 changes: 39 additions & 0 deletions examples/providers/aggregate.py
@@ -0,0 +1,39 @@
"""`Aggregate` provider example."""

from dependency_injector import containers, providers


class ConfigReader:

def __init__(self, path):
self._path = path

def read(self):
print(f"Parsing {self._path} with {self.__class__.__name__}")
...


class YamlReader(ConfigReader):
...


class JsonReader(ConfigReader):
...


class Container(containers.DeclarativeContainer):

config_readers = providers.Aggregate(
yaml=providers.Factory(YamlReader),
json=providers.Factory(JsonReader),
)


if __name__ == "__main__":
container = Container()

yaml_reader = container.config_readers("yaml", "./config.yml")
yaml_reader.read() # Parsing ./config.yml with YamlReader

json_reader = container.config_readers("json", "./config.json")
json_reader.read() # Parsing ./config.json with JsonReader

0 comments on commit 742e73a

Please sign in to comment.