Merge branch 'release/3.42.0' into master

This commit is contained in:
Roman Mogylatov 2020-09-09 18:02:53 -04:00
commit ac8212a95b
13 changed files with 255 additions and 414 deletions

View File

@ -66,11 +66,11 @@ Coupling and cohesion are about how tough the components are tied.
- **High coupling**. If the coupling is high it's like using a superglue or welding. No easy way - **High coupling**. If the coupling is high it's like using a superglue or welding. No easy way
to disassemble. to disassemble.
- **High cohesion**. High cohesion is like using the screws. Very easy to disassemble and - **High cohesion**. High cohesion is like using the screws. Very easy to disassemble and
assemble back or assemble a different way. It is an alternative to high coupling. assemble back or assemble a different way. It is an opposite to high coupling.
When the cohesion is high the coupling is low. When the cohesion is high the coupling is low.
High cohesion brings the flexibility. Your code becomes easier to change and test. Low coupling brings a flexibility. Your code becomes easier to change and test.
How to implement dependency injection? How to implement dependency injection?
-------------------------------------- --------------------------------------
@ -124,7 +124,6 @@ After:
if __name__ == '__main__': if __name__ == '__main__':
service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT'))) service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))
``ApiClient`` is decoupled from knowing where the options come from. You can read a key and a ``ApiClient`` is decoupled from knowing where the options come from. You can read a key and a
timeout from a configuration file or even get them from a database. timeout from a configuration file or even get them from a database.
@ -133,16 +132,23 @@ stub or other compatible object.
Flexibility comes with a price. Flexibility comes with a price.
Now you need to assemble your objects like this Now you need to assemble the objects like this::
``Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))``. The assembly code might get
duplicated and it'll become harder to change the application structure.
What does Dependency Injector do? service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))
---------------------------------
The assembly code might get duplicated and it'll become harder to change the application structure.
Here comes the ``Dependency Injector``.
What does the Dependency Injector do?
-------------------------------------
With the dependency injection pattern objects lose the responsibility of assembling the
dependencies. The ``Dependency Injector`` absorbs that responsibility.
``Dependency Injector`` helps to assemble the objects. ``Dependency Injector`` helps to assemble the objects.
It provides you the container and the providers that help you describe objects assembly. When you It provides a container and providers that help you with the objects assembly. When you
need an object you get it from the container. The rest of the assembly work is done by the need an object you get it from the container. The rest of the assembly work is done by the
framework: framework:
@ -151,19 +157,6 @@ framework:
from dependency_injector import containers, providers from dependency_injector import containers, providers
class ApiClient:
def __init__(self, api_key: str, timeout: int):
self.api_key = api_key
self.timeout = timeout
class Service:
def __init__(self, api_client: ApiClient):
self.api_client = api_client
class Container(containers.DeclarativeContainer): class Container(containers.DeclarativeContainer):
config = providers.Configuration() config = providers.Configuration()
@ -187,12 +180,14 @@ framework:
service = container.service() service = container.service()
Retrieving of the ``Service`` instance now is done like this ``container.service()``. Retrieving of the ``Service`` instance now is done like this::
service = container.service()
Objects assembling is consolidated in the container. When you need to make a change you do it in Objects assembling is consolidated in the container. When you need to make a change you do it in
one place. one place.
When doing the testing you call the ``container.api_client.override()`` to replace the real API When doing a testing you call the ``container.api_client.override()`` to replace the real API
client with a mock: client with a mock:
.. code-block:: python .. code-block:: python
@ -203,10 +198,10 @@ client with a mock:
with container.api_client.override(mock.Mock()): with container.api_client.override(mock.Mock()):
service = container.service() service = container.service()
It helps in a testing. Also you can use it for configuring project for the different environments: You can override any provider by another provider.
replace an API client with a stub on the dev or stage.
`More examples <https://github.com/ets-labs/python-dependency-injector/tree/master/examples>`_ It also helps you in configuring project for the different environments: replace an API client
with a stub on the dev or stage.
Installation Installation
------------ ------------

View File

@ -1,137 +1,256 @@
Dependency injection and inversion of control in Python Dependency injection and inversion of control in Python
------------------------------------------------------- =======================================================
.. meta:: .. meta::
:keywords: Python,DI,Dependency injection,IoC,Inversion of Control :keywords: Python,DI,Dependency injection,IoC,Inversion of Control,Example
:description: This article describes benefits of dependency injection and :description: This page describes a usage of the dependency injection and inversion of control
inversion of control for Python applications. Also it in Python. It contains Python examples that show how to implement dependency
contains some Python examples that show how dependency injection. It demonstrates a usage of the dependency injection framework
injection and inversion could be implemented. In addition, it Dependency Injector, its container, Factory, Singleton and Configuration
demonstrates usage of dependency injection framework, providers. The example show how to use Dependency Injector providers overriding
IoC container and such popular design pattern as Factory. feature for testing or configuring project in different environments and explains
why it's better then monkey-patching.
History Originally dependency injection pattern got popular in the languages with a static typing,
~~~~~~~ like Java. Dependency injection is a principle that helps to achieve an inversion of control.
Dependency injection framework can significantly improve a flexibility of the language
with a static typing. Implementation of a dependency injection framework for a language
with a static typing is not something that one can do quickly. It will be a quite complex thing
to be done well. And will take time.
Originally, dependency injection pattern got popular in languages with static Python is an interpreted language with a dynamic typing. There is an opinion that dependency
typing, like Java. Dependency injection framework can injection doesn't work for it as well as it does for Java. A lot of the flexibility is already
significantly improve flexibility of the language with static typing. Also, built in. Also there is an opinion that a dependency injection framework is something that
implementation of dependency injection framework for language with static Python developer rarely needs. Python developers say that dependency injection can be implemented
typing is not something that one can do shortly, it could be quite complex easily using language fundamentals.
thing to be done well.
While Python is very flexible interpreted language with dynamic typing, there This page describes the advantages of the dependency injection usage in Python. It
is a meaning that dependency injection doesn't work for it as well, as it does contains Python examples that show how to implement dependency injection. It demonstrates a usage
for Java. Also there is a meaning that dependency injection framework is of the dependency injection framework ``Dependency Injector``, its container, ``Factory``,
something that Python developer would not ever need, cause dependency injection ``Singleton`` and ``Configuration`` providers. The example shows how to use ``Dependency Injector``
could be implemented easily using language fundamentals. providers overriding feature for testing or configuring project in different environments and
explains why it's better then monkey-patching.
Discussion What is dependency injection?
~~~~~~~~~~ -----------------------------
It is true. Let's see what the dependency injection is.
Partly. Dependency injection is a principle that helps to decrease coupling and increase cohesion.
Dependency injection, as a software design pattern, has number of .. image:: images/coupling-cohesion.png
advantages that are common for each language (including Python):
+ Dependency Injection decreases coupling between a class and its dependency. What is coupling and cohesion?
+ Because dependency injection doesn't require any change in code behavior it
can be applied to legacy code as a refactoring. The result is clients that
are more independent and that are easier to unit test in isolation using
stubs or mock objects that simulate other objects not under test. This ease
of testing is often the first benefit noticed when using dependency
injection.
+ Dependency injection can be used to externalize a system's configuration
details into configuration files allowing the system to be reconfigured
without recompilation (rebuilding). Separate configurations can be written
for different situations that require different implementations of
components. This includes, but is not limited to, testing.
+ Reduction of boilerplate code in the application objects since all work to
initialize or set up dependencies is handled by a provider component.
+ Dependency injection allows a client to remove all knowledge of a concrete
implementation that it needs to use. This helps isolate the client from the
impact of design changes and defects. It promotes reusability, testability
and maintainability.
+ Dependency injection allows a client the flexibility of being configurable.
Only the client's behavior is fixed. The client may act on anything that
supports the intrinsic interface the client expects.
.. note:: Coupling and cohesion are about how tough the components are tied.
While improved testability is one the first benefits of using dependency - **High coupling**. If the coupling is high it's like using a superglue or welding. No easy way
injection, it could be easily overwhelmed by monkey-patching technique, to disassemble.
that works absolutely great in Python (you can monkey-patch anything, - **High cohesion**. High cohesion is like using the screws. Very easy to disassemble and
anytime). At the same time, monkey-patching has nothing similar with assemble back or assemble a different way. It is an opposite to high coupling.
other advantages defined above. Also monkey-patching technique is
something that could be considered like too dirty to be used in production.
The complexity of dependency injection pattern implementation in Python is When the cohesion is high the coupling is low.
definitely quite lower than in other languages (even with dynamic typing).
.. note:: Low coupling brings a flexibility. Your code becomes easier to change and test.
Low complexity of dependency injection pattern implementation in Python How to implement the dependency injection?
still means that some code should be written, reviewed, tested and
supported.
Talking about inversion of control, it is a software design principle that Objects do not create each other anymore. They provide a way to inject the dependencies instead.
also works for each programming language, not depending on its typing type.
Inversion of control is used to increase modularity of the program and make Before:
it extensible.
Main design purposes of using inversion of control are: .. code-block:: python
+ To decouple the execution of a task from implementation. import os
+ To focus a module on the task it is designed for.
+ To free modules from assumptions about how other systems do what they do and
instead rely on contracts.
+ To prevent side effects when replacing a module.
Example
~~~~~~~
Let's go through next example: class ApiClient:
.. image:: /images/miniapps/engines_cars/diagram.png def __init__(self):
:width: 100% self.api_key = os.getenv('API_KEY') # <-- the dependency
:align: center self.timeout = os.getenv('TIMEOUT') # <-- the dependency
Listing of ``example.engines`` module:
.. literalinclude:: ../../examples/miniapps/engines_cars/example/engines.py class Service:
:language: python
Listing of ``example.cars`` module: def __init__(self):
self.api_client = ApiClient() # <-- the dependency
.. literalinclude:: ../../examples/miniapps/engines_cars/example/cars.py
:language: python
Next example demonstrates creation of several cars with different engines: if __name__ == '__main__':
service = Service()
.. literalinclude:: ../../examples/miniapps/engines_cars/example_di.py
:language: python
While previous example demonstrates advantages of dependency injection, there After:
is a disadvantage demonstration as well - creation of car requires additional
code for specification of dependencies. Nevertheless, this disadvantage could
be easily avoided by using a dependency injection framework for creation of
inversion of control container (IoC container).
Example of creation of several inversion of control containers (IoC containers) .. code-block:: python
using :doc:`Dependency Injector <../index>`:
.. literalinclude:: ../../examples/miniapps/engines_cars/example_ioc_containers.py import os
:language: python
class ApiClient:
def __init__(self, api_key: str, timeout: int):
self.api_key = api_key # <-- the dependency is injected
self.timeout = timeout # <-- the dependency is injected
class Service:
def __init__(self, api_client: ApiClient):
self.api_client = api_client # <-- the dependency is injected
if __name__ == '__main__':
service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))
``ApiClient`` is decoupled from knowing where the options come from. You can read a key and a
timeout from a configuration file or even get them from a database.
``Service`` is decoupled from the ``ApiClient``. It does not create it anymore. You can provide a
stub or other compatible object.
Flexibility comes with a price.
Now you need to assemble the objects like this::
service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))
The assembly code might get duplicated and it'll become harder to change the application structure.
Here comes the ``Dependency Injector``.
What does the Dependency Injector do?
-------------------------------------
With the dependency injection pattern objects lose the responsibility of assembling the
dependencies. The ``Dependency Injector`` absorbs that responsibility.
``Dependency Injector`` helps to assemble the objects.
It provides a container and providers that help you with the objects assembly. When you
need an object you get it from the container. The rest of the assembly work is done by the
framework:
.. code-block:: python
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
api_client = providers.Singleton(
ApiClient,
api_key=config.api_key,
timeout=config.timeout.as_int(),
)
service = providers.Factory(
Service,
api_client=api_client,
)
if __name__ == '__main__':
container = Container()
container.config.api_key.from_env('API_KEY')
container.config.timeout.from_env('TIMEOUT')
service = container.service()
Retrieving of the ``Service`` instance now is done like this::
service = container.service()
Objects assembling is consolidated in the container. When you need to make a change you do it in
one place.
When doing a testing you call the ``container.api_client.override()`` to replace the real API
client with a mock:
.. code-block:: python
from unittest import mock
with container.api_client.override(mock.Mock()):
service = container.service()
You can override any provider by another provider.
It also helps you in configuring project for the different environments: replace an API client
with a stub on the dev or stage.
Testing, Monkey-patching and dependency injection
-------------------------------------------------
The testability benefit is opposed to a monkey-patching.
In Python you can monkey-patch
anything, anytime. The problem with a monkey-patching is that it's too fragile. The reason is that
when you monkey-patch you do something that wasn't intended to be done. You monkey-patch the
implementation details. When implementation changes the monkey-patching is broken.
With a dependency injection you patch the interface, not an implementation. This is a way more
stable approach.
Also monkey-patching is a way too dirty to be used outside of the testing code for
reconfiguring the project for the different environments.
Conclusion
----------
Dependency injection brings you 3 advantages:
- **Flexibility**. The components are loosely coupled. You can easily extend or change a
functionality of the system by combining the components different way. You even can do it on
the fly.
- **Testability**. Testing is easy because you can easily inject mocks instead of real objects
that use API or database, etc.
- **Clearness and maintainability**. Dependency injection helps you reveal the dependencies.
Implicit becomes explicit. And "Explicit is better than implicit" (PEP20 - The Zen of Python).
You have all the components and dependencies defined explicitly in the container. This
provides an overview and control on the application structure. It is easy to understand and
change it.
Is it worth to use a dependency injection in Python?
It depends on what you build. The advantages above are not too important if you use Python as a
scripting language. The picture is different when you use Python to create an application. The
larger the application the more significant is the benefit.
Is it worth to use a framework for the dependency injection?
The complexity of the dependency injection pattern implementation in Python is
lower than in the other languages but it's still in place. It doesn't mean you have to use a
framework but using a framework is beneficial because the framework is:
- Already implemented
- Tested on all platforms and versions of Python
- Documented
- Supported
- Known to the other engineers
Few advices at last:
- **Give it a try**. Dependency injection is counter-intuitive. Our nature is that
when we need something the first thought that comes to our mind is to go and get it. Dependency
injection is just like "Wait, I need to state a need instead of getting something right now".
It's like a little investment that will pay-off later. The advice is to just give it a try for
two weeks. This time will be enough for getting your own impression. If you don't like it you
won't lose too much.
- **Common sense first**. Use a common sense when apply dependency injection. It is a good
principle, but not a silver bullet. If you do it too much you will reveal too much of the
implementation details. Experience comes with practice and time.
What's next? What's next?
~~~~~~~~~~~~ ------------
Choose one of the following as a next step: Choose one of the following as a next step:
- Look at application examples: - Look at the application examples:
- :ref:`application-single-container` - :ref:`application-single-container`
- :ref:`application-multiple-containers` - :ref:`application-multiple-containers`
- :ref:`decoupled-packages` - :ref:`decoupled-packages`
@ -140,11 +259,12 @@ Choose one of the following as a next step:
- :ref:`aiohttp-tutorial` - :ref:`aiohttp-tutorial`
- :ref:`asyncio-daemon-tutorial` - :ref:`asyncio-daemon-tutorial`
- :ref:`cli-tutorial` - :ref:`cli-tutorial`
- Know more about the ``Dependency Injector`` :ref:`key-features`
- Know more about the :ref:`providers` - Know more about the :ref:`providers`
- Go to the :ref:`contents` - Go to the :ref:`contents`
Useful links Useful links
~~~~~~~~~~~~ ------------
There are some useful links related to dependency injection design pattern There are some useful links related to dependency injection design pattern
that could be used for further reading: that could be used for further reading:

View File

@ -3,18 +3,16 @@ Introduction
.. meta:: .. meta::
:keywords: Python,DI,Dependency injection,IoC,Inversion of Control :keywords: Python,DI,Dependency injection,IoC,Inversion of Control
:description: Current section of documentation is designed to give some :description: Current section of the documentation is provides an
overview about dependency injection pattern, inversion of overview of the dependency injection, inversion of
control principle and "Dependency Injector" framework. control and Dependency Injector framework.
Current section of documentation is designed to give some overview about Current section of the documentation provides an overview of the
dependency injection pattern, inversion of control principle and dependency injection, inversion of control and the ``Dependency Injector`` framework.
*Dependency Injector* framework.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
what_is_di
di_in_python di_in_python
key_features key_features
installation installation

View File

@ -1,3 +1,5 @@
.. _key-features:
Key features Key features
------------ ------------

View File

@ -1,195 +0,0 @@
What is dependency injection?
-----------------------------
.. meta::
:keywords: Python,DI,Dependency injection,Low coupling,High cohesion
:description: This page provides a Python example of what is dependency injection. It tells
about benefits of coupling and high cohesion.
Dependency injection is a principle that helps to decrease coupling and increase cohesion.
.. image:: images/coupling-cohesion.png
What is coupling and cohesion?
Coupling and cohesion are about how tough the components are tied.
- **High coupling**. If the coupling is high it's like using a superglue or welding. No easy way
to disassemble.
- **High cohesion**. High cohesion is like using the screws. Very easy to disassemble and
assemble back or assemble a different way. It is an alternative to high coupling.
When the cohesion is high the coupling is low.
High cohesion brings the flexibility. Your code becomes easier to change and test.
The example
~~~~~~~~~~~
How does dependency injection helps to achieve high cohesion?
Objects do not create each other anymore. They provide a way to inject the dependencies instead.
Before:
.. code-block:: python
import os
class ApiClient:
def __init__(self):
self.api_key = os.getenv('API_KEY') # <-- the dependency
self.timeout = os.getenv('TIMEOUT') # <-- the dependency
class Service:
def __init__(self):
self.api_client = ApiClient() # <-- the dependency
if __name__ == '__main__':
service = Service()
After:
.. code-block:: python
import os
class ApiClient:
def __init__(self, api_key: str, timeout: int):
self.api_key = api_key # <-- the dependency is injected
self.timeout = timeout # <-- the dependency is injected
class Service:
def __init__(self, api_client: ApiClient):
self.api_client = api_client # <-- the dependency is injected
if __name__ == '__main__':
service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))
``ApiClient`` is decoupled from knowing where the options come from. You can read a key and a
timeout from a configuration file or even get them from a database.
``Service`` is decoupled from the ``ApiClient``. It does not create it anymore. You can provide a
stub or other compatible object.
Flexibility comes with a price.
Now you need to assemble your objects like this
``Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))``. The assembly code might get
duplicated and it'll become harder to change the application structure.
Here comes the ``Dependency Injector``.
``Dependency Injector`` helps to assemble the objects.
It provides you the container and the providers that help you describe objects assembly. When you
need an object you get it from the container. The rest of the assembly work is done by the
framework:
.. code-block:: python
from dependency_injector import containers, providers
class ApiClient:
def __init__(self, api_key: str, timeout: int):
self.api_key = api_key
self.timeout = timeout
class Service:
def __init__(self, api_client: ApiClient):
self.api_client = api_client
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
api_client = providers.Singleton(
ApiClient,
api_key=config.api_key,
timeout=config.timeout.as_int(),
)
service = providers.Factory(
Service,
api_client=api_client,
)
if __name__ == '__main__':
container = Container()
container.config.api_key.from_env('API_KEY')
container.config.timeout.from_env('TIMEOUT')
service = container.service()
Retrieving of the ``Service`` instance now is done like this ``container.service()``.
Objects assembling is consolidated in the container. When you need to make a change you do it in
one place.
When doing the testing you call the ``container.api_client.override()`` to replace the real API
client with a mock:
.. code-block:: python
from unittest import mock
with container.api_client.override(mock.Mock()):
service = container.service()
How to explain dependency injection to a 5-year-old?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Some time ago `user198313`_ posted this `question`_ on the `StackOverflow`_.
`John Munsch`_ provided a great answer:
*When you go and get things out of the refrigerator for yourself, you can
cause problems. You might leave the door open, you might get something
Mommy or Daddy doesn't want you to have. You might even be looking for
something we don't even have or which has expired.*
*What you should be doing is stating a need, "I need something to drink
with lunch," and then we will make sure you have something when you sit
down to eat.*
What's next?
~~~~~~~~~~~~
Choose one of the following as a next step:
- Look at application examples:
- :ref:`application-single-container`
- :ref:`application-multiple-containers`
- :ref:`decoupled-packages`
- Pass the tutorials:
- :ref:`flask-tutorial`
- :ref:`aiohttp-tutorial`
- :ref:`asyncio-daemon-tutorial`
- :ref:`cli-tutorial`
- Know more about the :ref:`providers`
- Go to the :ref:`contents`
.. disqus::
.. _StackOverflow: http://stackoverflow.com/
.. _question: http://stackoverflow.com/questions/1638919/how-to-explain-dependency-injection-to-a-5-year-old/1639186
.. _user198313: http://stackoverflow.com/users/198313/user198313
.. _John Munsch: http://stackoverflow.com/users/31899/john-munsch

View File

@ -7,6 +7,13 @@ that were made in every particular version.
From version 0.7.6 *Dependency Injector* framework strictly From version 0.7.6 *Dependency Injector* framework strictly
follows `Semantic versioning`_ follows `Semantic versioning`_
3.42.0
------
- Update "DI in Python" documentation page.
- Delete "What is DI?" documentation page.
- Delete "engines cars" example mini app.
- Update README.
3.41.0 3.41.0
------ ------
- Refactor "use cases" example. - Refactor "use cases" example.

View File

@ -1,9 +0,0 @@
Engines & Cars Dependency Injection Example
===========================================
Instructions for running:
.. code-block:: bash
python example_di.py
python example_ioc_containers.py

View File

@ -1 +0,0 @@
"""Example top-level package."""

View File

@ -1,9 +0,0 @@
"""Dependency injection example, cars module."""
class Car:
"""Example car."""
def __init__(self, engine):
"""Initialize instance."""
self._engine = engine # Engine is injected

View File

@ -1,21 +0,0 @@
"""Dependency injection example, engines module."""
class Engine:
"""Example engine base class.
Engine is a heart of every car. Engine is a very common term and
could be implemented in very different ways.
"""
class GasolineEngine(Engine):
"""Gasoline engine."""
class DieselEngine(Engine):
"""Diesel engine."""
class ElectricEngine(Engine):
"""Electric engine."""

View File

@ -1,10 +0,0 @@
"""Dependency injection example, Cars & Engines."""
import example.cars
import example.engines
if __name__ == '__main__':
gasoline_car = example.cars.Car(example.engines.GasolineEngine())
diesel_car = example.cars.Car(example.engines.DieselEngine())
electric_car = example.cars.Car(example.engines.ElectricEngine())

View File

@ -1,36 +0,0 @@
"""Dependency injection example, Cars & Engines IoC containers."""
import example.cars
import example.engines
import dependency_injector.containers as containers
import dependency_injector.providers as providers
class Engines(containers.DeclarativeContainer):
"""IoC container of engine providers."""
gasoline = providers.Factory(example.engines.GasolineEngine)
diesel = providers.Factory(example.engines.DieselEngine)
electric = providers.Factory(example.engines.ElectricEngine)
class Cars(containers.DeclarativeContainer):
"""IoC container of car providers."""
gasoline = providers.Factory(example.cars.Car,
engine=Engines.gasoline)
diesel = providers.Factory(example.cars.Car,
engine=Engines.diesel)
electric = providers.Factory(example.cars.Car,
engine=Engines.electric)
if __name__ == '__main__':
gasoline_car = Cars.gasoline()
diesel_car = Cars.diesel()
electric_car = Cars.electric()

View File

@ -1,6 +1,6 @@
"""Dependency injector top-level package.""" """Dependency injector top-level package."""
__version__ = '3.41.0' __version__ = '3.42.0'
"""Version number that follows semantic versioning. """Version number that follows semantic versioning.
:type: str :type: str