From f7d3ada92f577dbafc7af6239ce7325c2a09cf89 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 30 Sep 2020 16:47:37 -0400 Subject: [PATCH] Update DI in Python page --- docs/introduction/di_in_python.rst | 68 ++++++++++++++++++++------- examples/demo/after.py | 29 ++++++++++++ examples/demo/before.py | 23 +++++++++ examples/demo/di.py | 28 ----------- examples/demo/example.py | 23 --------- examples/demo/{demo.py => with_di.py} | 2 +- 6 files changed, 105 insertions(+), 68 deletions(-) create mode 100644 examples/demo/after.py create mode 100644 examples/demo/before.py delete mode 100644 examples/demo/di.py delete mode 100644 examples/demo/example.py rename examples/demo/{demo.py => with_di.py} (95%) diff --git a/docs/introduction/di_in_python.rst b/docs/introduction/di_in_python.rst index dfd3ab91..cf5d9bb0 100644 --- a/docs/introduction/di_in_python.rst +++ b/docs/introduction/di_in_python.rst @@ -77,10 +77,14 @@ Before: self.api_client = ApiClient() # <-- the dependency - if __name__ == '__main__': - service = Service() + def main() -> None: + service = Service() # <-- the dependency + ... + if __name__ == '__main__': + main() + After: .. code-block:: python @@ -101,8 +105,19 @@ After: self.api_client = api_client # <-- the dependency is injected + def main(service: Service): # <-- the dependency is injected + ... + + if __name__ == '__main__': - service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT'))) + main( + service=Service( + api_client=ApiClient( + api_key=os.getenv('API_KEY'), + timeout=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. @@ -110,11 +125,22 @@ 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. +Function ``main()`` is decoupled from ``Service``. It receives it as an argument. + Flexibility comes with a price. -Now you need to assemble the objects like this:: +Now you need to assemble and inject the objects like this: - service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT'))) +.. code-block:: python + + main( + service=Service( + api_client=ApiClient( + api_key=os.getenv('API_KEY'), + timeout=os.getenv('TIMEOUT'), + ), + ), + ) The assembly code might get duplicated and it'll become harder to change the application structure. @@ -123,18 +149,20 @@ Here comes the ``Dependency Injector``. What does the Dependency Injector do? ------------------------------------- -With the dependency injection pattern objects loose the responsibility of assembling the -dependencies. The ``Dependency Injector`` absorbs that responsibility. +With the dependency injection pattern objects loose the responsibility of assembling +the dependencies. The ``Dependency Injector`` absorbs that responsibilities. -``Dependency Injector`` helps to assemble the objects. +``Dependency Injector`` helps to assemble and inject the dependencies. -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: +It provides a container and providers that help you with the objects assembly. +When you need an object you place a ``Provide`` marker as a default value of a +function argument. When you call this function framework assembles and injects +the dependency. .. code-block:: python from dependency_injector import containers, providers + from dependency_injector.wiring import Provide class Container(containers.DeclarativeContainer): @@ -153,22 +181,29 @@ framework: ) + def main(service: Service = Provide[Container.service]): + ... + + if __name__ == '__main__': container = Container() + container.config.api_key.from_env('API_KEY') container.config.timeout.from_env('TIMEOUT') - service = container.service() + container.wire(modules=[sys.modules[__name__]]) -Retrieving of the ``Service`` instance now is done like this:: + main() - service = container.service() +When you call ``main()`` function the ``Service`` dependency is assembled and injected:: + + main() 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: +client with a mock. When you call ``main()`` the mock is injected: .. code-block:: python @@ -176,7 +211,7 @@ client with a mock: with container.api_client.override(mock.Mock()): - service = container.service() + main() You can override any provider with another provider. @@ -261,6 +296,7 @@ Choose one of the following as a next step: - :ref:`cli-tutorial` - Know more about the ``Dependency Injector`` :ref:`key-features` - Know more about the :ref:`providers` +- Know more about the :ref:`wiring` - Go to the :ref:`contents` Useful links diff --git a/examples/demo/after.py b/examples/demo/after.py new file mode 100644 index 00000000..213f5a3c --- /dev/null +++ b/examples/demo/after.py @@ -0,0 +1,29 @@ +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 + + +def main(service: Service): # <-- the dependency is injected + ... + + +if __name__ == '__main__': + main( + service=Service( + api_client=ApiClient( + api_key=os.getenv('API_KEY'), + timeout=os.getenv('TIMEOUT'), + ), + ), + ) diff --git a/examples/demo/before.py b/examples/demo/before.py new file mode 100644 index 00000000..895562c5 --- /dev/null +++ b/examples/demo/before.py @@ -0,0 +1,23 @@ +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 + + +def main() -> None: + service = Service() # <-- the dependency + ... + + +if __name__ == '__main__': + main() diff --git a/examples/demo/di.py b/examples/demo/di.py deleted file mode 100644 index 4b34b6a1..00000000 --- a/examples/demo/di.py +++ /dev/null @@ -1,28 +0,0 @@ -import os - - -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 - - -def main() -> None: - service = Service( - api_client=ApiClient( - api_key=os.getenv('API_KEY'), - timeout=os.getenv('TIMEOUT'), - ), - ) - ... - - -if __name__ == '__main__': - main() diff --git a/examples/demo/example.py b/examples/demo/example.py deleted file mode 100644 index 393b04b3..00000000 --- a/examples/demo/example.py +++ /dev/null @@ -1,23 +0,0 @@ -import os - - -class ApiClient: - - def __init__(self): - self.api_key = os.getenv('API_KEY') - self.timeout = os.getenv('TIMEOUT') - - -class Service: - - def __init__(self): - self.api_client = ApiClient() - - -def main() -> None: - service = Service() - ... - - -if __name__ == '__main__': - main() diff --git a/examples/demo/demo.py b/examples/demo/with_di.py similarity index 95% rename from examples/demo/demo.py rename to examples/demo/with_di.py index 12eccb97..c76c1c61 100644 --- a/examples/demo/demo.py +++ b/examples/demo/with_di.py @@ -4,7 +4,7 @@ from unittest import mock from dependency_injector import containers, providers from dependency_injector.wiring import Provide -from di import ApiClient, Service +from after import ApiClient, Service class Container(containers.DeclarativeContainer):