Develop 4.0 (#298)

* Add wiring (#294)

* Add wiring module

* Fix code style

* Fix package test

* Add version fix

* Try spike for 3.6

* Try another fix with metaclass

* Downsample required version to 3.6

* Introduce concept with annotations

* Fix bugs

* Add debug message

* Add extra tests

* Add extra debugging

* Update config resolving

* Remove 3.6 generic meta fix

* Fix Flake8

* Add spike for 3.6

* Add Python 3.6 spike

* Add unwire functionality

* Add support of corouting functions

* Bump version to 4.0

* Updaet demo example

* Add pydocstyle ignore for demo

* Add flake8 ignore for demo

* Update aiohttp example

* Update flask example

* Rename aiohttp example directory

* Rename views module to handlers in aiohttp example

* Add sanic example

* Remove not needed images

* Update demo

* Implement wiring for Provide[foo.provider]

* Implement Provide[foo.provided.bar.baz.call()]

* Make flake8 happy

* Wiring refactoring (#296)

* Refactor wiring

* Add todos to wiring

* Implement wiring of config invariant

* Implement sub containers wiring + add tests

* Add test for wiring config invariant

* Add container.unwire() typing stub

* Deprecate ext package modules and remove types module

* Deprecate provider.delegate() method

* Add __all__ for wiring module

* Add protection for wiring only declarative container instances

* Bump version to 4.0.0a2

* Add wiring docs

* Add wiring of class methods

* Remove unused import

* Add a note on individuals import to wiring docs

* Add minor improvement to wiring doc

* Update DI in Python page

* Update key features

* Update README concep and FAQ

* Add files via upload

* Update README.rst

* Update README.rst

* Update README.rst

* Update docs index page

* Update README

* Remove API docs for flask and aiohttp ext

* Add wiring API docs

* Update docs index

* Update README

* Update readme and docs index

* Change wording in README

* Django example (#297)

* Add rough django example

* Remove sqlite db

* Add gitignore

* Fix flake8 and pydocstyle errors

* Add tests

* Refactor settings

* Move web app to to the root of the project

* Add bootstrap 4

* Add doc blocks for web app

* Add coverage

* Fix typo in flask

* Remove not needed newlines

* Add screenshot

* Update django app naming

* Add django example to the docs

* Update changelog

* Update Aiohttp example

* Add sanic example to the docs

* Make a little fix in django example docs page

* Add flask example to the docs

* Add aiohttp example to the docs

* Update installation docs page

* Fix .delegate() deprecation

* Refactor movie lister to use wiring

* Make micro cosmetic changes to flask, aiohttp & sanic examples

* Refactor single container example to use wiring

* Refactor multiple container example to use wiring

* Add return type to main() in application examples

* Refactor decoupled packages example to use wiring

* Refactor code layout for DI demo example

* Update wiring feature message

* Add more links to the examples

* Change code layout in miniapps

* Update sanic example

* Update miniapp READMEs

* Update wiring docs

* Refactor part of cli tutorial

* Refactor CLI app tutorial

* Update test coverage results in movie lister example and tutorial

* Make some minor updates to aiohttp and cli tutorials

* Refactor flask tutorial

* Make cosmetic fix in flask example

* Refactor Flask tutorial: Connect to the GitHub

* Refactor Flask tutorial: Search service

* Refactor Flask tutorial: Inject search service into view

* Refactor Flask tutorial: Make some refactoring

* Finish flask tutorial refactoring

* Update tutorials

* Refactor  asyncio monitoring daemon example application

* Fix tutorial links

* Rename asyncio miniapp

* Rename tutorial image dirs

* Rename api docs tol-level page

* Refactor initial sections of asyncio daemon tutorial

* Refactor asyncio tutorial till Example.com monitor section

* Refactor asyncio tutorial example.com monitor section

* Refactor asyncio tutorial httpbin.org monitor tutorial

* Refactor tests section of asyncio daemon tutorial

* Update conclusion of asyncio daemon tutorial

* Rename tutorial images

* Make cosmetic update to flask tutorial

* Refactor aiohttp tutorial: Minimal application section

* Refactor aiohttp tutorial: Giphy API client secion

* Refactor aiohttp tutorial secion: Make the search work

* Refactor aiohttp tutorial tests section

* Refactor aiohttp tutorial conclusion

* Upgrade  Cython to 0.29.21

* Update changelog

* Update demo example

* Update wording on index pages

* Update changelog

* Update code layout for main demo
This commit is contained in:
Roman Mogylatov 2020-10-09 15:16:27 -04:00 committed by GitHub
parent 53b7ad0275
commit 07d4f7e74f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
142 changed files with 13658 additions and 8293 deletions

View File

@ -67,14 +67,18 @@ Key features of the ``Dependency Injector``:
See `Configuration provider <http://python-dependency-injector.ets-labs.org/providers/configuration.html>`_.
- **Containers**. Provides declarative and dynamic containers.
See `Containers <http://python-dependency-injector.ets-labs.org/containers/index.html>`_.
- **Performance**. Fast. Written in ``Cython``.
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with
other frameworks: Django, Flask, Aiohttp, etc.
See `Wiring <http://python-dependency-injector.ets-labs.org/wiring.html>`_.
- **Typing**. Provides typing stubs, ``mypy``-friendly.
See `Typing and mypy <http://python-dependency-injector.ets-labs.org/providers/typing_mypy.html>`_.
- **Performance**. Fast. Written in ``Cython``.
- **Maturity**. Mature and production-ready. Well-tested, documented and supported.
.. code-block:: python
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide
class Container(containers.DeclarativeContainer):
@ -93,23 +97,38 @@ Key features of the ``Dependency Injector``:
)
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')
container.wire(modules=[sys.modules[__name__]])
service = container.service()
main() # <-- dependency is injected automatically
With the ``Dependency Injector`` you keep **application structure in one place**.
This place is called **the container**. You use the container to manage all the components of the
application. All the component dependencies are defined explicitly. This provides the control on
the application structure. It is **easy to understand and change** it.
with container.api_client.override(mock.Mock()):
main() # <-- overridden dependency is injected automatically
.. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/di-map.svg
When you call ``main()`` function the ``Service`` dependency is assembled and injected automatically.
When doing a testing you call the ``container.api_client.override()`` to replace the real API
client with a mock. When you call ``main()`` the mock is injected.
You can override any provider with 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.
With the ``Dependency Injector`` objects assembling is consolidated in the container.
Dependency injections are defined explicitly.
This makes easier to understand and change how application works.
.. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/di-readme.svg
:target: https://github.com/ets-labs/python-dependency-injector
*The container is like a map of your application. You always know what depends on what.*
Visit the docs to know more about the
`Dependency injection and inversion of control in Python <http://python-dependency-injector.ets-labs.org/introduction/di_in_python.html>`_.
@ -133,6 +152,10 @@ Choose one of the following:
- `Application example (single container) <http://python-dependency-injector.ets-labs.org/examples/application-single-container.html>`_
- `Application example (multiple containers) <http://python-dependency-injector.ets-labs.org/examples/application-multiple-containers.html>`_
- `Decoupled packages example (multiple containers) <http://python-dependency-injector.ets-labs.org/examples/decoupled-packages.html>`_
- `Django example <http://python-dependency-injector.ets-labs.org/examples/django.html>`_
- `Flask example <http://python-dependency-injector.ets-labs.org/examples/flask.html>`_
- `Aiohttp example <http://python-dependency-injector.ets-labs.org/examples/aiohttp.html>`_
- `Sanic example <http://python-dependency-injector.ets-labs.org/examples/sanic.html>`_
Tutorials
---------
@ -147,22 +170,16 @@ Choose one of the following:
Concept
-------
``Dependency Injector`` stands on two principles:
The framework stands on the `PEP20 (The Zen of Python) <https://www.python.org/dev/peps/pep-0020/>`_ principle:
- Explicit is better than implicit (PEP20).
- Do no magic to your code.
.. code-block:: plain
How is it different from the other frameworks?
Explicit is better than implicit
- **No autowiring.** The framework does NOT do any autowiring / autoresolving of the dependencies. You need to specify everything explicitly. Because *"Explicit is better than implicit" (PEP20)*.
- **Does not pollute your code.** Your application does NOT know and does NOT depend on the framework. No ``@inject`` decorators, annotations, patching or any other magic tricks.
You need to specify how to assemble and where to inject the dependencies explicitly.
``Dependency Injector`` makes a simple contract with you:
- You tell the framework how to assemble your objects
- The framework does it for you
The power of the ``Dependency Injector`` is in its simplicity and straightforwardness. It is a simple tool for the powerful concept.
The power of the framework is in a simplicity.
``Dependency Injector`` is a simple tool for the powerful concept.
Frequently asked questions
--------------------------
@ -171,35 +188,17 @@ What is the dependency injection?
- dependency injection is a principle that decreases coupling and increases cohesion
Why should I do the dependency injection?
- your code becomes more flexible, testable and clear
- you have no problems when you need to understand how it works or change it 😎
- your code becomes more flexible, testable and clear 😎
How do I start doing the dependency injection?
- you start writing the code following the dependency injection principle
- you register all of your application components and their dependencies in the container
- when you need a component, you get it from the container
Why do I need a framework for this?
- you need the framework for this to not create it by your own
- this framework gives you the container and the providers
- the container is like a dictionary with the batteries 🔋
- the providers manage the lifetime of your components, you will need factories, singletons, smart config object etc
- when you need a component, you specify where to inject it or get it from the container
What price do I pay and what do I get?
- you need to explicitly specify the dependencies in the container
- you need to explicitly specify the dependencies
- it will be extra work in the beginning
- it will payoff when project grows or in two weeks 😊 (when you forget what project was about)
What features does the framework have?
- building objects graph
- smart configuration object
- providers: factory, singleton, thread locals registers, etc
- positional and keyword context injections
- overriding of the objects in any part of the graph
What features the framework does NOT have?
- autowiring / autoresolving of the dependencies
- the annotations and ``@inject``-like decorators
- it will payoff as the project grows
Have a question?
- Open a `Github Issue <https://github.com/ets-labs/python-dependency-injector/issues>`_

View File

@ -1,9 +0,0 @@
dependency_injector.ext.aiohttp
===============================
.. automodule:: dependency_injector.ext.aiohttp
:members:
:show-inheritance:
.. disqus::

View File

@ -1,9 +0,0 @@
dependency_injector.ext.flask
=============================
.. automodule:: dependency_injector.ext.flask
:members:
:show-inheritance:
.. disqus::

View File

@ -4,9 +4,8 @@ API Documentation
.. toctree::
:maxdepth: 2
top_level
providers
containers
errors
aiohttpext
flaskext
top-level
providers
containers
wiring
errors

7
docs/api/wiring.rst Normal file
View File

@ -0,0 +1,7 @@
dependency_injector.wiring
=============================
.. automodule:: dependency_injector.wiring
:members:
.. disqus::

81
docs/examples/aiohttp.rst Normal file
View File

@ -0,0 +1,81 @@
.. _aiohttp-example:
Aiohttp example
===============
.. meta::
:keywords: Python,Dependency Injection,Aiohttp,Example
:description: This example demonstrates a usage of the Aiohttp and Dependency Injector.
This example shows how to use ``Dependency Injector`` with `Aiohttp <https://docs.aiohttp.org/>`_.
The example application is a REST API that searches for funny GIFs on the `Giphy <https://giphy.com/>`_.
The source code is available on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/aiohttp>`_.
:ref:`aiohttp-tutorial` demonstrates how to build this application step-by-step.
Application structure
---------------------
Application has next structure:
.. code-block:: bash
./
├── giphynavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── giphy.py
│ ├── handlers.py
│ ├── services.py
│ └── tests.py
├── config.yml
└── requirements.txt
Container
---------
Declarative container is defined in ``giphynavigator/containers.py``:
.. literalinclude:: ../../examples/miniapps/aiohttp/giphynavigator/containers.py
:language: python
Handlers
--------
Handler has dependencies on search service and some config options. The dependencies are injected
using :ref:`wiring` feature.
Listing of ``giphynavigator/handlers.py``:
.. literalinclude:: ../../examples/miniapps/aiohttp/giphynavigator/handlers.py
:language: python
Application factory
-------------------
Application factory creates container, wires it with the ``handlers`` module, creates
``Aiohttp`` app and setup routes.
Listing of ``giphynavigator/application.py``:
.. literalinclude:: ../../examples/miniapps/aiohttp/giphynavigator/application.py
:language: python
Tests
-----
Tests use :ref:`provider-overriding` feature to replace giphy client with a mock ``giphynavigator/tests.py``:
.. literalinclude:: ../../examples/miniapps/aiohttp/giphynavigator/tests.py
:language: python
:emphasize-lines: 32,59,73
Sources
-------
Explore the sources on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/aiohttp>`_.
.. disqus::

97
docs/examples/django.rst Normal file
View File

@ -0,0 +1,97 @@
.. _django-example:
Django example
==============
.. meta::
:keywords: Python,Dependency Injection,Django,Example
:description: This example demonstrates a usage of the Django and Dependency Injector.
This example shows how to use ``Dependency Injector`` with `Django <https://www.djangoproject.com/>`_.
The example application helps to search for repositories on the Github.
.. image:: images/django.png
:width: 100%
:align: center
The source code is available on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/django>`_.
Application structure
---------------------
Application has standard Django project structure. It consists of ``githubnavigator`` project package and
``web`` application package:
.. code-block:: bash
./
├── githubnavigator/
│ ├── __init__.py
│ ├── asgi.py
│ ├── containers.py
│ ├── services.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── web/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── apps.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── manage.py
└── requirements.txt
Container
---------
Declarative container is defined in ``githubnavigator/containers.py``:
.. literalinclude:: ../../examples/miniapps/django/githubnavigator/containers.py
:language: python
Container instance is created in ``githubnavigator/__init__.py``:
.. literalinclude:: ../../examples/miniapps/django/githubnavigator/__init__.py
:language: python
Views
-----
View has dependencies on search service and some config options. The dependencies are injected
using :ref:`wiring` feature.
Listing of ``web/views.py``:
.. literalinclude:: ../../examples/miniapps/django/web/views.py
:language: python
App config
----------
Container is wired to the ``views`` module in the app config ``web/apps.py``:
.. literalinclude:: ../../examples/miniapps/django/web/apps.py
:language: python
:emphasize-lines: 13
Tests
-----
Tests use :ref:`provider-overriding` feature to replace github client with a mock ``web/tests.py``:
.. literalinclude:: ../../examples/miniapps/django/web/tests.py
:language: python
:emphasize-lines: 39,60
Sources
-------
Explore the sources on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/django>`_.
.. disqus::

87
docs/examples/flask.rst Normal file
View File

@ -0,0 +1,87 @@
.. _flask-example:
Flask example
=============
.. meta::
:keywords: Python,Dependency Injection,Flask,Example
:description: This example demonstrates a usage of the Flask and Dependency Injector.
This example shows how to use ``Dependency Injector`` with `Flask <https://flask.palletsprojects.com/en/1.1.x/>`_.
The example application helps to search for repositories on the Github.
.. image:: images/flask.png
:width: 100%
:align: center
The source code is available on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/flask>`_.
:ref:`flask-tutorial` demonstrates how to build this application step-by-step.
Application structure
---------------------
Application has next structure:
.. code-block:: bash
./
├── githubnavigator/
│ ├── templates
│ │ ├── base.html
│ │ └── index.py
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── services.py
│ ├── tests.py
│ └── views.py
├── config.yml
└── requirements.txt
Container
---------
Declarative container is defined in ``githubnavigator/containers.py``:
.. literalinclude:: ../../examples/miniapps/flask/githubnavigator/containers.py
:language: python
Views
-----
View has dependencies on search service and some config options. The dependencies are injected
using :ref:`wiring` feature.
Listing of ``githubnavigator/views.py``:
.. literalinclude:: ../../examples/miniapps/flask/githubnavigator/views.py
:language: python
Application factory
-------------------
Application factory creates container, wires it with the ``views`` module, creates
``Flask`` app and setup routes.
Listing of ``githubnavigator/application.py``:
.. literalinclude:: ../../examples/miniapps/flask/githubnavigator/application.py
:language: python
Tests
-----
Tests use :ref:`provider-overriding` feature to replace github client with a mock ``githubnavigator/tests.py``:
.. literalinclude:: ../../examples/miniapps/flask/githubnavigator/tests.py
:language: python
:emphasize-lines: 44,67
Sources
-------
Explore the sources on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/flask>`_.
.. disqus::

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

View File

Before

Width:  |  Height:  |  Size: 647 KiB

After

Width:  |  Height:  |  Size: 647 KiB

View File

@ -13,5 +13,9 @@ Explore the examples to see the ``Dependency Injector`` in action.
application-single-container
application-multiple-containers
decoupled-packages
django
flask
aiohttp
sanic
.. disqus::

80
docs/examples/sanic.rst Normal file
View File

@ -0,0 +1,80 @@
.. _sanic-example:
Sanic example
==============
.. meta::
:keywords: Python,Dependency Injection,Sanic,Example
:description: This example demonstrates a usage of the Sanic and Dependency Injector.
This example shows how to use ``Dependency Injector`` with `Sanic <https://sanic.readthedocs.io/en/latest/>`_.
The example application is a REST API that searches for funny GIFs on the `Giphy <https://giphy.com/>`_.
The source code is available on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/sanic>`_.
Application structure
---------------------
Application has next structure:
.. code-block:: bash
./
├── giphynavigator/
│ ├── __init__.py
│ ├── __main__.py
│ ├── application.py
│ ├── containers.py
│ ├── giphy.py
│ ├── handlers.py
│ ├── services.py
│ └── tests.py
├── config.yml
└── requirements.txt
Container
---------
Declarative container is defined in ``giphynavigator/containers.py``:
.. literalinclude:: ../../examples/miniapps/sanic/giphynavigator/containers.py
:language: python
Handlers
--------
Handler has dependencies on search service and some config options. The dependencies are injected
using :ref:`wiring` feature.
Listing of ``giphynavigator/handlers.py``:
.. literalinclude:: ../../examples/miniapps/sanic/giphynavigator/handlers.py
:language: python
Application factory
-------------------
Application factory creates container, wires it with the ``handlers`` module, creates
``Sanic`` app and setup routes.
Listing of ``giphynavigator/application.py``:
.. literalinclude:: ../../examples/miniapps/sanic/giphynavigator/application.py
:language: python
Tests
-----
Tests use :ref:`provider-overriding` feature to replace giphy client with a mock ``giphynavigator/tests.py``:
.. literalinclude:: ../../examples/miniapps/sanic/giphynavigator/tests.py
:language: python
:emphasize-lines: 27,54,68
Sources
-------
Explore the sources on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/sanic>`_.
.. disqus::

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -77,13 +77,16 @@ Key features of the ``Dependency Injector``:
- **Configuration**. Read configuration from ``yaml`` & ``ini`` files, environment variables
and dictionaries. See :ref:`configuration-provider`.
- **Containers**. Provides declarative and dynamic containers. See :ref:`containers`.
- **Performance**. Fast. Written in ``Cython``.
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with
other frameworks: Django, Flask, Aiohttp, etc. See :ref:`wiring`.
- **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`.
- **Performance**. Fast. Written in ``Cython``.
- **Maturity**. Mature and production-ready. Well-tested, documented and supported.
.. code-block:: python
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide
class Container(containers.DeclarativeContainer):
@ -102,23 +105,28 @@ Key features of the ``Dependency Injector``:
)
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')
container.wire(modules=[sys.modules[__name__]])
service = container.service()
main() # <-- dependency is injected automatically
With the ``Dependency Injector`` you keep **application structure in one place**.
This place is called **the container**. You use the container to manage all the components of the
application. All the component dependencies are defined explicitly. This provides the control on
the application structure. It is **easy to understand and change** it.
with container.api_client.override(mock.Mock()):
main() # <-- overridden dependency is injected automatically
.. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/di-map.svg
With the ``Dependency Injector`` objects assembling is consolidated in the container.
Dependency injections are defined explicitly.
This makes easier to understand and change how application works.
.. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/di-readme.svg
:target: https://github.com/ets-labs/python-dependency-injector
*The container is like a map of your application. You always know what depends on what.*
Explore the documentation to know more about the ``Dependency Injector``.
.. _contents:
@ -126,15 +134,16 @@ Explore the documentation to know more about the ``Dependency Injector``.
Contents
--------
.. toctree::
:maxdepth: 2
.. toctree::
:maxdepth: 2
introduction/index
examples/index
tutorials/index
providers/index
containers/index
examples-other/index
api/index
main/feedback
main/changelog
introduction/index
examples/index
tutorials/index
providers/index
containers/index
wiring
examples-other/index
api/index
main/feedback
main/changelog

View File

@ -67,19 +67,23 @@ Before:
class ApiClient:
def __init__(self):
self.api_key = os.getenv('API_KEY') # <-- the dependency
self.timeout = os.getenv('TIMEOUT') # <-- the dependency
self.api_key = os.getenv('API_KEY') # <-- dependency
self.timeout = os.getenv('TIMEOUT') # <-- dependency
class Service:
def __init__(self):
self.api_client = ApiClient() # <-- the dependency
self.api_client = ApiClient() # <-- dependency
def main() -> None:
service = Service() # <-- dependency
...
if __name__ == '__main__':
service = Service()
main()
After:
@ -91,18 +95,29 @@ After:
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
self.api_key = api_key # <-- dependency is injected
self.timeout = timeout # <-- dependency is injected
class Service:
def __init__(self, api_client: ApiClient):
self.api_client = api_client # <-- the dependency is injected
self.api_client = api_client # <-- dependency is injected
def main(service: Service): # <-- 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,36 +181,34 @@ 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')
container.wire(modules=[sys.modules[__name__]])
service = container.service()
main() # <-- dependency is injected automatically
Retrieving of the ``Service`` instance now is done like this::
with container.api_client.override(mock.Mock()):
main() # <-- overridden dependency is injected automatically
service = container.service()
Objects assembling is consolidated in the container. When you need to make a change you do it in
one place.
When you call ``main()`` function the ``Service`` dependency is assembled and injected automatically.
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()
client with a mock. When you call ``main()`` the mock is injected.
You can override any provider with 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.
Objects assembling is consolidated in the container. Dependency injections are defined explicitly.
This makes easier to understand and change how application works.
Testing, Monkey-patching and dependency injection
-------------------------------------------------
@ -254,6 +280,10 @@ Choose one of the following as a next step:
- :ref:`application-single-container`
- :ref:`application-multiple-containers`
- :ref:`decoupled-packages`
- :ref:`django-example`
- :ref:`flask-example`
- :ref:`aiohttp-example`
- :ref:`sanic-example`
- Pass the tutorials:
- :ref:`flask-tutorial`
- :ref:`aiohttp-tutorial`
@ -261,6 +291,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

View File

@ -1,41 +1,42 @@
Installation
============
*Dependency Injector* framework is distributed by PyPi_.
Latest stable version (and all previous versions) of *Dependency Injector*
framework can be installed from PyPi_:
``Dependency Injector`` is available on `PyPI <https://pypi.org/project/dependency-injector/>`_.
To install latest version you can use ``pip``:
.. code-block:: bash
pip install dependency-injector
.. note::
Some components of *Dependency Injector* are implemented as C extension types.
*Dependency Injector* is distributed as an archive with a source code, so
C compiler and Python header files are required for the installation.
Some modules of the ``Dependency Injector`` are implemented as C extensions.
``Dependency Injector`` is distributed as a pre-compiled wheels. Wheels are
available for all supported Python versions on Linux, Windows and MacOS.
Linux distribution uses `manylinux <https://github.com/pypa/manylinux>`_.
Sources can be cloned from GitHub_:
If there is no appropriate wheel for your environment (Python version and OS)
installer will compile the package from sources on your machine. You'll need
a C compiler and Python header files.
.. code-block:: bash
git clone https://github.com/ets-labs/python-dependency-injector.git
Also all *Dependency Injector* releases can be downloaded from
`GitHub releases page`_.
Verification of currently installed version could be done using
:py:obj:`dependency_injector.VERSION` constant:
To verify the installed version:
.. code-block:: bash
>>> import dependency_injector
>>> dependency_injector.__version__
'3.43.0'
'4.0.0'
.. _PyPi: https://pypi.org/project/dependency-injector/
.. _GitHub: https://github.com/ets-labs/python-dependency-injector
.. _GitHub releases page: https://github.com/ets-labs/python-dependency-injector/releases
.. note::
When add ``Dependency Injector`` to the ``requirements.txt`` don't forget to pin version
to the current major:
.. code-block:: bash
dependency-injector>=4.0,<5.0
*Next major version can be incompatible.*
All releases are available on `PyPI release history page <https://pypi.org/project/dependency-injector/#history>`_.
Each release has appropriate tag. The tags are available on
`GitHub releases page <https://github.com/ets-labs/python-dependency-injector/releases>`_.
.. disqus::

View File

@ -19,20 +19,21 @@ Key features of the ``Dependency Injector``:
- **Configuration**. Read configuration from ``yaml`` & ``ini`` files, environment variables
and dictionaries. See :ref:`configuration-provider`.
- **Containers**. Provides declarative and dynamic containers. See :ref:`containers`.
- **Performance**. Fast. Written in ``Cython``.
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with
other frameworks: Django, Flask, Aiohttp, etc. See :ref:`wiring`.
- **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`.
- **Performance**. Fast. Written in ``Cython``.
- **Maturity**. Mature and production-ready. Well-tested, documented and supported.
The framework stands on two principles:
The framework stands on the `PEP20 (The Zen of Python) <https://www.python.org/dev/peps/pep-0020/>`_ principle:
- **Explicit is better than implicit (PEP20)**.
- **Do not do any magic to your code**.
.. code-block:: plain
How is that different from the other frameworks?
Explicit is better than implicit
- **No autowiring.** The framework does NOT do any autowiring / autoresolving of the dependencies. You need to specify everything explicitly. Because *"Explicit is better than implicit" (PEP20)*.
- **Does not pollute your code.** Your application does NOT know and does NOT depend on the framework. No ``@inject`` decorators, annotations, patching or any other magic tricks.
You need to specify how to assemble and where to inject the dependencies explicitly.
The power of the framework is in a simplicity. ``Dependency Injector`` is a simple tool for the powerful concept.
The power of the framework is in a simplicity.
``Dependency Injector`` is a simple tool for the powerful concept.
.. disqus::

View File

@ -7,6 +7,44 @@ that were made in every particular version.
From version 0.7.6 *Dependency Injector* framework strictly
follows `Semantic versioning`_
4.0.0
-----
New features:
- Add ``wiring`` feature.
Deprecations:
- Deprecate ``ext.aiohttp`` module in favor of ``wiring`` feature.
- Deprecate ``ext.flask`` module in favor of ``wiring`` feature.
- Deprecate ``.delegate()`` provider method in favor of ``.provider`` attribute.
Removals:
- Remove deprecated ``types`` module.
Tutorials:
- Update ``flask`` tutorial.
- Update ``aiohttp`` tutorial.
- Update ``asyncio`` daemon tutorial.
- Update CLI application tutorial.
Examples:
- Add ``django`` example.
- Add ``sanic`` example.
- Update ``aiohttp`` example.
- Update ``flask`` example.
- Update ``asyncio`` daemon example.
- Update ``movie-lister`` example.
- Update CLI application example.
Misc:
- Regenerate C sources using Cython 0.29.21.
- Improve documentation and README (typos removal, rewording, etc).
3.44.0
------
- Add native support of the generics to the providers: ``some_provider = providers.Provider[SomeClass]``.

View File

@ -21,7 +21,7 @@ Start from the scratch or jump to the section:
:backlinks: none
You can find complete project on the
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/giphynav-aiohttp>`_.
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/aiohttp>`_.
What are we going to build?
---------------------------
@ -88,18 +88,18 @@ Prepare the environment
Let's create the environment for the project.
First we need to create a project folder and the virtual environment:
First we need to create a project folder:
.. code-block:: bash
mkdir giphynav-aiohttp-tutorial
cd giphynav-aiohttp-tutorial
python3 -m venv venv
Now let's activate the virtual environment:
Now let's create and activate virtual environment:
.. code-block:: bash
python3 -m venv venv
. venv/bin/activate
Environment is ready and now we're going to create the layout of the project.
@ -116,7 +116,7 @@ Initial project layout::
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
│ └── handlers.py
├── venv/
└── requirements.txt
@ -164,14 +164,14 @@ The requirements are setup. Now we will build a minimal application.
Minimal application
-------------------
In this section we will build a minimal application. It will have an endpoint that we can call.
The endpoint will answer in the right format and will have no data.
In this section we will build a minimal application. It will have an endpoint that
will answer our requests in json format. There will be no payload for now.
Edit ``views.py``:
Edit ``handlers.py``:
.. code-block:: python
"""Views module."""
"""Handlers module."""
from aiohttp import web
@ -190,34 +190,25 @@ Edit ``views.py``:
},
)
Now let's create the main part of our application - the container. Container will keep all of the
application components and their dependencies. First two providers we need to add are
the ``aiohttp`` application provider and the view provider.
Now let's create a container. Container will keep all of the application components and their dependencies.
Put next into the ``containers.py``:
Edit ``containers.py``:
.. code-block:: python
"""Application containers module."""
"""Containers module."""
from dependency_injector import containers
from dependency_injector.ext import aiohttp
from aiohttp import web
from . import views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
class Container(containers.DeclarativeContainer):
...
app = aiohttp.Application(web.Application)
Container is empty for now. We will add the providers in the following sections.
index_view = aiohttp.View(views.index)
At the last we need to create the ``aiohttp`` application factory. It is traditionally called
``create_app()``. It will create the container. Then it will use the container to create
the ``aiohttp`` application. Last step is to configure the routing - we will assign
``index_view`` from the container to handle the requests to the root ``/`` of our REST API server.
Finally we need to create ``aiohttp`` application factory. It will create and configure container
and ``web.Application``. It is traditionally called ``create_app()``.
We will assign ``index`` handler to handle user requests to the root ``/`` of our web application.
Put next into the ``application.py``:
@ -227,28 +218,20 @@ Put next into the ``application.py``:
from aiohttp import web
from .containers import ApplicationContainer
from .containers import Container
from . import handlers
def create_app():
"""Create and return aiohttp application."""
container = ApplicationContainer()
def create_app() -> web.Application:
container = Container()
app: web.Application = container.app()
app = web.Application()
app.container = container
app.add_routes([
web.get('/', container.index_view.as_view()),
web.get('/', handlers.index),
])
return app
.. note::
Container is the first object in the application.
The container is used to create all other objects.
Now we're ready to run our application
Do next in the terminal:
@ -264,7 +247,7 @@ The output should be something like:
[18:52:59] Starting aux server at http://localhost:8001 ◆
[18:52:59] Starting dev server at http://localhost:8000 ●
Let's use ``httpie`` to check that it works:
Let's check that it works. Open another terminal session and use ``httpie``:
.. code-block:: bash
@ -306,7 +289,7 @@ Create ``giphy.py`` module in the ``giphynavigator`` package:
│ ├── application.py
│ ├── containers.py
│ ├── giphy.py
│ └── views.py
│ └── handlers.py
├── venv/
└── requirements.txt
@ -351,21 +334,16 @@ providers from the ``dependency_injector.providers`` module:
Edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 3,7,15,17-21
:emphasize-lines: 3-5,10-16
"""Application containers module."""
"""Containers module."""
from dependency_injector import containers, providers
from dependency_injector.ext import aiohttp
from aiohttp import web
from . import giphy, views
from . import giphy
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = aiohttp.Application(web.Application)
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
@ -375,8 +353,6 @@ Edit ``containers.py``:
timeout=config.giphy.request_timeout,
)
index_view = aiohttp.View(views.index)
.. note::
We have used the configuration value before it was defined. That's the principle how the
@ -399,7 +375,7 @@ Create an empty file ``config.yml`` in the root root of the project:
│ ├── application.py
│ ├── containers.py
│ ├── giphy.py
│ └── views.py
│ └── handlers.py
├── venv/
├── config.yml
└── requirements.txt
@ -427,24 +403,23 @@ Edit ``application.py``:
from aiohttp import web
from .containers import ApplicationContainer
from .containers import Container
from . import handlers
def create_app():
"""Create and return aiohttp application."""
container = ApplicationContainer()
def create_app() -> web.Application:
container = Container()
container.config.from_yaml('config.yml')
container.config.giphy.api_key.from_env('GIPHY_API_KEY')
app: web.Application = container.app()
app = web.Application()
app.container = container
app.add_routes([
web.get('/', container.index_view.as_view()),
web.get('/', handlers.index),
])
return app
Now we need to create an API key and set it to the environment variable.
As for now, dont worry, just take this one:
@ -473,7 +448,7 @@ Now it's time to add the ``SearchService``. It will:
Create ``services.py`` module in the ``giphynavigator`` package:
.. code-block:: bash
:emphasize-lines: 7
:emphasize-lines: 8
./
├── giphynavigator/
@ -481,9 +456,10 @@ Create ``services.py`` module in the ``giphynavigator`` package:
│ ├── application.py
│ ├── containers.py
│ ├── giphy.py
│ ├── services.py
│ └── views.py
│ ├── handlers.py
│ └── services.py
├── venv/
├── config.yml
└── requirements.txt
and put next into it:
@ -509,27 +485,22 @@ and put next into it:
return [{'url': gif['url']} for gif in result['data']]
The ``SearchService`` has a dependency on the ``GiphyClient``. This dependency will be injected.
Let's add ``SearchService`` to the container.
The ``SearchService`` has a dependency on the ``GiphyClient``. This dependency will be
injected when we add ``SearchService`` to the container.
Edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 7,23-26
:emphasize-lines: 5,18-21
"""Application containers module."""
"""Containers module."""
from dependency_injector import containers, providers
from dependency_injector.ext import aiohttp
from aiohttp import web
from . import giphy, services, views
from . import giphy, services
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = aiohttp.Application(web.Application)
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
@ -544,31 +515,31 @@ Edit ``containers.py``:
giphy_client=giphy_client,
)
index_view = aiohttp.View(views.index)
The search service is ready. In the next section we're going to make it work.
The search service is ready. In next section we're going to put it to work.
Make the search work
--------------------
Now we are ready to make the search work. Let's use the ``SearchService`` in the ``index`` view.
Now we are ready to put the search into work. Let's inject ``SearchService`` into
the ``index`` handler. We will use :ref:`wiring` feature.
Edit ``views.py``:
Edit ``handlers.py``:
.. code-block:: python
:emphasize-lines: 5,8-11,15
:emphasize-lines: 4-7,10-13,17
"""Views module."""
"""Handlers module."""
from aiohttp import web
from dependency_injector.wiring import Provide
from .services import SearchService
from .containers import Container
async def index(
request: web.Request,
search_service: SearchService,
search_service: SearchService = Provide[Container.search_service],
) -> web.Response:
query = request.query.get('query', 'Dependency Injector')
limit = int(request.query.get('limit', 10))
@ -583,44 +554,35 @@ Edit ``views.py``:
},
)
Now let's inject the ``SearchService`` dependency into the ``index`` view.
To make the injection work we need to wire the container instance with the ``handlers`` module.
This needs to be done once. After it's done we can use ``Provide`` markers to specify as many
injections as needed for any handler.
Edit ``containers.py``:
Edit ``application.py``:
.. code-block:: python
:emphasize-lines: 28-31
:emphasize-lines: 13
"""Application containers module."""
"""Application module."""
from dependency_injector import containers, providers
from dependency_injector.ext import aiohttp
from aiohttp import web
from . import giphy, services, views
from .containers import Container
from . import handlers
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
def create_app() -> web.Application:
container = Container()
container.config.from_yaml('config.yml')
container.config.giphy.api_key.from_env('GIPHY_API_KEY')
container.wire(modules=[handlers])
app = aiohttp.Application(web.Application)
config = providers.Configuration()
giphy_client = providers.Factory(
giphy.GiphyClient,
api_key=config.giphy.api_key,
timeout=config.giphy.request_timeout,
)
search_service = providers.Factory(
services.SearchService,
giphy_client=giphy_client,
)
index_view = aiohttp.View(
views.index,
search_service=search_service,
)
app = web.Application()
app.container = container
app.add_routes([
web.get('/', handlers.index),
])
return app
Make sure the app is running or use:
@ -639,30 +601,30 @@ You should see:
.. code-block:: json
HTTP/1.1 200 OK
Content-Length: 850
Content-Length: 492
Content-Type: application/json; charset=utf-8
Date: Wed, 29 Jul 2020 22:22:55 GMT
Date: Fri, 09 Oct 2020 01:35:48 GMT
Server: Python/3.8 aiohttp/3.6.2
{
"gifs": [
{
"url": "https://giphy.com/gifs/dollyparton-3xIVVMnZfG3KQ9v4Ye"
},
{
"url": "https://giphy.com/gifs/tennistv-unbelievable-disbelief-cant-believe-UWWJnhHHbpGvZOapEh"
},
{
"url": "https://giphy.com/gifs/discoverychannel-nugget-gold-rush-rick-ness-KGGPIlnC4hr4u2s3pY"
},
{
"url": "https://giphy.com/gifs/primevideoin-ll1hyBS2IrUPLE0E71"
"url": "https://giphy.com/gifs/soulpancake-wow-work-xUe4HVXTPi0wQ2OAJC"
},
{
"url": "https://giphy.com/gifs/jackman-works-jackmanworks-l4pTgQoCrmXq8Txlu"
},
{
"url": "https://giphy.com/gifs/cat-massage-at-work-l46CzMaOlJXAFuO3u"
},
{
"url": "https://giphy.com/gifs/everwhatproductions-fun-christmas-3oxHQCI8tKXoeW4IBq"
},
"url": "https://giphy.com/gifs/readingrainbow-teamwork-levar-burton-reading-rainbow-3o7qE1EaTWLQGDSabK"
}
],
"limit": 10,
"limit": 5,
"query": "wow,it works"
}
@ -673,30 +635,32 @@ The search works!
Make some refactoring
---------------------
Our ``index`` view has two hardcoded config values:
Our ``index`` handler has two hardcoded config values:
- Default search query
- Default results limit
Let's make some refactoring. We will move these values to the config.
Edit ``views.py``:
Edit ``handlers.py``:
.. code-block:: python
:emphasize-lines: 11-12,14-15
:emphasize-lines: 13-14,16-17
"""Views module."""
"""Handlers module."""
from aiohttp import web
from dependency_injector.wiring import Provide
from .services import SearchService
from .containers import Container
async def index(
request: web.Request,
search_service: SearchService,
default_query: str,
default_limit: int,
search_service: SearchService = Provide[Container.search_service],
default_query: str = Provide[Container.config.default.query],
default_limit: int = Provide[Container.config.default.limit.as_int()],
) -> web.Response:
query = request.query.get('query', default_query)
limit = int(request.query.get('limit', default_limit))
@ -711,48 +675,7 @@ Edit ``views.py``:
},
)
Now we need to inject these values. Let's update the container.
Edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 31-32
"""Application containers module."""
from dependency_injector import containers, providers
from dependency_injector.ext import aiohttp
from aiohttp import web
from . import giphy, services, views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = aiohttp.Application(web.Application)
config = providers.Configuration()
giphy_client = providers.Factory(
giphy.GiphyClient,
api_key=config.giphy.api_key,
timeout=config.giphy.request_timeout,
)
search_service = providers.Factory(
services.SearchService,
giphy_client=giphy_client,
)
index_view = aiohttp.View(
views.index,
search_service=search_service,
default_query=config.search.default_query,
default_limit=config.search.default_limit,
)
Finally let's update the config.
Let's update the config.
Edit ``config.yml``:
@ -761,26 +684,21 @@ Edit ``config.yml``:
giphy:
request_timeout: 10
search:
default_query: "Dependency Injector"
default_limit: 10
default:
query: "Dependency Injector"
limit: 10
The refactoring is done. We've made it cleaner - hardcoded values are now moved to the config.
In the next section we will add some tests.
Tests
-----
It would be nice to add some tests. Let's do it.
We will use `pytest <https://docs.pytest.org/en/stable/>`_ and
`coverage <https://coverage.readthedocs.io/>`_.
In this section we will add some tests.
Create ``tests.py`` module in the ``giphynavigator`` package:
.. code-block:: bash
:emphasize-lines: 8
:emphasize-lines: 9
./
├── giphynavigator/
@ -788,16 +706,17 @@ Create ``tests.py`` module in the ``giphynavigator`` package:
│ ├── application.py
│ ├── containers.py
│ ├── giphy.py
│ ├── handlers.py
│ ├── services.py
│ ├── tests.py
│ └── views.py
│ └── tests.py
├── venv/
├── config.yml
└── requirements.txt
and put next into it:
.. code-block:: python
:emphasize-lines: 30,57,71
:emphasize-lines: 32,59,73
"""Tests module."""
@ -811,7 +730,9 @@ and put next into it:
@pytest.fixture
def app():
return create_app()
app = create_app()
yield app
app.container.unwire()
@pytest.fixture
@ -874,8 +795,8 @@ and put next into it:
assert response.status == 200
data = await response.json()
assert data['query'] == app.container.config.search.default_query()
assert data['limit'] == app.container.config.search.default_limit()
assert data['query'] == app.container.config.default.query()
assert data['limit'] == app.container.config.default.limit()
Now let's run it and check the coverage:
@ -885,7 +806,7 @@ Now let's run it and check the coverage:
You should see:
.. code-block:: bash
.. code-block::
platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
plugins: cov-2.10.0, aiohttp-0.3.0, asyncio-0.14.0
@ -897,15 +818,14 @@ You should see:
Name Stmts Miss Cover
---------------------------------------------------
giphynavigator/__init__.py 0 0 100%
giphynavigator/__main__.py 5 5 0%
giphynavigator/application.py 10 0 100%
giphynavigator/containers.py 10 0 100%
giphynavigator/application.py 12 0 100%
giphynavigator/containers.py 6 0 100%
giphynavigator/giphy.py 14 9 36%
giphynavigator/handlers.py 9 0 100%
giphynavigator/services.py 9 1 89%
giphynavigator/tests.py 35 0 100%
giphynavigator/views.py 7 0 100%
giphynavigator/tests.py 37 0 100%
---------------------------------------------------
TOTAL 90 15 83%
TOTAL 87 10 89%
.. note::
@ -920,45 +840,19 @@ In this tutorial we've built an ``aiohttp`` REST API application following the d
injection principle.
We've used the ``Dependency Injector`` as a dependency injection framework.
The benefit you get with the ``Dependency Injector`` is the container. It starts to payoff
when you need to understand or change your application structure. It's easy with the container,
cause you have everything defined explicitly in one place:
:ref:`containers` and :ref:`providers` helped to specify how to assemble search service and
giphy client.
.. code-block:: python
:ref:`configuration-provider` helped to deal with reading YAML file and environment variable.
"""Application containers module."""
We used :ref:`wiring` feature to inject the dependencies into the ``index()`` handler.
:ref:`provider-overriding` feature helped in testing.
from dependency_injector import containers, providers
from dependency_injector.ext import aiohttp
from aiohttp import web
We kept all the dependencies injected explicitly. This will help when you need to add or
change something in future.
from . import giphy, services, views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = aiohttp.Application(web.Application)
config = providers.Configuration()
giphy_client = providers.Factory(
giphy.GiphyClient,
api_key=config.giphy.api_key,
timeout=config.giphy.request_timeout,
)
search_service = providers.Factory(
services.SearchService,
giphy_client=giphy_client,
)
index_view = aiohttp.View(
views.index,
search_service=search_service,
default_query=config.search.default_query,
default_limit=config.search.default_limit,
)
You can find complete project on the
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/aiohttp>`_.
What's next?

View File

@ -27,7 +27,7 @@ Start from the scratch or jump to the section:
:backlinks: none
You can find complete project on the
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/monitoring-daemon-asyncio>`_.
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/asyncio-daemon>`_.
What are we going to build?
---------------------------
@ -42,7 +42,7 @@ response it will log:
- The amount of bytes in the response
- The time took to complete the response
.. image:: asyncio_images/diagram.png
.. image:: asyncio-images/diagram.png
Prerequisites
-------------
@ -79,8 +79,8 @@ Create the project root folder and set it as a working directory:
.. code-block:: bash
mkdir monitoring-daemon-tutorial
cd monitoring-daemon-tutorial
mkdir asyncio-daemon-tutorial
cd asyncio-daemon-tutorial
Now we need to create the initial project structure. Create the files and folders following next
layout. All files should be empty for now. We will fill them later.
@ -190,10 +190,10 @@ The output should look like:
.. code-block:: bash
Creating network "monitoring-daemon-tutorial_default" with the default driver
Creating monitoring-daemon-tutorial_monitor_1 ... done
Attaching to monitoring-daemon-tutorial_monitor_1
monitoring-daemon-tutorial_monitor_1 exited with code 0
Creating network "asyncio-daemon-tutorial_default" with the default driver
Creating asyncio-daemon-tutorial_monitor_1 ... done
Attaching to asyncio-daemon-tutorial_monitor_1
asyncio-daemon-tutorial_monitor_1 exited with code 0
The environment is ready. The application does not do any work and just exits with a code ``0``.
@ -214,7 +214,7 @@ Put next lines into the ``containers.py`` file:
.. code-block:: python
"""Application containers module."""
"""Containers module."""
import logging
import sys
@ -222,8 +222,7 @@ Put next lines into the ``containers.py`` file:
from dependency_injector import containers, providers
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
@ -259,29 +258,27 @@ Put next lines into the ``__main__.py`` file:
.. code-block:: python
"""Main module."""
"""Main module."""
from .containers import ApplicationContainer
from .containers import Container
def main() -> None:
"""Run the application."""
container = ApplicationContainer()
container.config.from_yaml('config.yml')
container.configure_logging()
def main() -> None:
...
if __name__ == '__main__':
main()
if __name__ == '__main__':
container = Container()
container.config.from_yaml('config.yml')
container.configure_logging()
main()
.. note::
Container is the first object in the application.
The container is used to create all other objects.
Logging and configuration parsing part is done. In the next section we will create the monitoring
Logging and configuration parsing part is done. In next section we will create the monitoring
checks dispatcher.
Dispatcher
@ -293,7 +290,7 @@ The dispatcher will control a list of the monitoring tasks. It will execute each
to the configured schedule. The ``Monitor`` class is the base class for all the monitors. You can
create different monitors by subclassing it and implementing the ``check()`` method.
.. image:: asyncio_images/class_1.png
.. image:: asyncio-images/classes-01.png
Let's create dispatcher and the monitor base classes.
@ -336,7 +333,7 @@ and next into the ``dispatcher.py``:
.. code-block:: python
""""Dispatcher module."""
"""Dispatcher module."""
import asyncio
import logging
@ -382,6 +379,7 @@ and next into the ``dispatcher.py``:
self._logger.info('Shutting down')
for task, monitor in zip(self._monitor_tasks, self._monitors):
task.cancel()
self._monitor_tasks.clear()
self._logger.info('Shutdown finished successfully')
@staticmethod
@ -407,9 +405,9 @@ Now we need to add the dispatcher to the container.
Edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 8,23-28
:emphasize-lines: 8,22-27
"""Application containers module."""
"""Containers module."""
import logging
import sys
@ -419,8 +417,7 @@ Edit ``containers.py``:
from . import dispatcher
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
@ -438,35 +435,35 @@ Edit ``containers.py``:
),
)
.. note::
At the last we will inject dispatcher into the ``main()`` function
and call the ``run()`` method. We will use :ref:`wiring` feature.
Every component should be added to the container.
At the last we will add the dispatcher in the ``main()`` function. We will retrieve the
dispatcher instance from the container and call the ``run()`` method.
Edit ``__main__.py``:
.. code-block:: python
:emphasize-lines: 13-14
:emphasize-lines: 3-7,11-12,19
"""Main module."""
from .containers import ApplicationContainer
import sys
from dependency_injector.wiring import Provide
from .dispatcher import Dispatcher
from .containers import Container
def main() -> None:
"""Run the application."""
container = ApplicationContainer()
container.config.from_yaml('config.yml')
container.configure_logging()
dispatcher = container.dispatcher()
def main(dispatcher: Dispatcher = Provide[Container.dispatcher]) -> None:
dispatcher.run()
if __name__ == '__main__':
container = Container()
container.config.from_yaml('config.yml')
container.configure_logging()
container.wire(modules=[sys.modules[__name__]])
main()
Finally let's start the daemon to check that all works.
@ -481,22 +478,22 @@ The output should look like:
.. code-block:: bash
Starting monitoring-daemon-tutorial_monitor_1 ... done
Attaching to monitoring-daemon-tutorial_monitor_1
Starting asyncio-daemon-tutorial_monitor_1 ... done
Attaching to asyncio-daemon-tutorial_monitor_1
monitor_1 | [2020-08-08 16:12:35,772] [INFO] [Dispatcher]: Starting up
monitor_1 | [2020-08-08 16:12:35,774] [INFO] [Dispatcher]: Shutting down
monitor_1 | [2020-08-08 16:12:35,774] [INFO] [Dispatcher]: Shutdown finished successfully
monitoring-daemon-tutorial_monitor_1 exited with code 0
asyncio-daemon-tutorial_monitor_1 exited with code 0
Everything works properly. Dispatcher starts up and exits because there are no monitoring tasks.
By the end of this section we have the application skeleton ready. In the next section will will
By the end of this section we have the application skeleton ready. In next section will will
add first monitoring task.
Example.com monitor
-------------------
In this section we will add the monitoring task that will check the availability of the
In this section we will add a monitoring task that will check the availability of the
`http://example.com <http://example.com>`_.
We will start from the extending of our class model with a new type of the monitoring check, the
@ -506,9 +503,9 @@ The ``HttpMonitor`` is a subclass of the ``Monitor``. We will implement the ``ch
will send the HTTP request to the specified URL. The http request sending will be delegated to
the ``HttpClient``.
.. image:: asyncio_images/class_2.png
.. image:: asyncio-images/classes-02.png
First, we need to create the ``HttpClient``.
First we need to create the ``HttpClient``.
Create ``http.py`` in the ``monitoringdaemon`` package:
@ -549,9 +546,9 @@ Now we need to add the ``HttpClient`` to the container.
Edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 8, 23
:emphasize-lines: 8,22
"""Application containers module."""
"""Containers module."""
import logging
import sys
@ -561,8 +558,7 @@ Edit ``containers.py``:
from . import http, dispatcher
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
@ -587,7 +583,7 @@ Now we're ready to add the ``HttpMonitor``. We will add it to the ``monitors`` m
Edit ``monitors.py``:
.. code-block:: python
:emphasize-lines: 4-5,7,20-56
:emphasize-lines: 4-7,20-56
"""Monitors module."""
@ -638,7 +634,7 @@ Edit ``monitors.py``:
' %s %s\n'
' response code: %s\n'
' content length: %s\n'
' request took: %s seconds\n',
' request took: %s seconds',
self._method,
self._url,
response.status,
@ -655,9 +651,9 @@ We make two changes in the container:
Edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 8,25-29,34
:emphasize-lines: 8,24-28,33
"""Application containers module."""
"""Containers module."""
import logging
import sys
@ -667,8 +663,7 @@ Edit ``containers.py``:
from . import http, monitors, dispatcher
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
@ -727,15 +722,14 @@ You should see:
.. code-block:: bash
Starting monitoring-daemon-tutorial_monitor_1 ... done
Attaching to monitoring-daemon-tutorial_monitor_1
Starting asyncio-daemon-tutorial_monitor_1 ... done
Attaching to asyncio-daemon-tutorial_monitor_1
monitor_1 | [2020-08-08 17:06:41,965] [INFO] [Dispatcher]: Starting up
monitor_1 | [2020-08-08 17:06:42,033] [INFO] [HttpMonitor]: Check
monitor_1 | GET http://example.com
monitor_1 | response code: 200
monitor_1 | content length: 648
monitor_1 | request took: 0.067 seconds
monitor_1 |
monitor_1 | [2020-08-08 17:06:47,040] [INFO] [HttpMonitor]: Check
monitor_1 | GET http://example.com
monitor_1 | response code: 200
@ -744,21 +738,21 @@ You should see:
Our daemon can monitor `http://example.com <http://example.com>`_ availability.
Let's add the monitor for the `http://httpbin.org <http://httpbin.org>`_.
Let's add a monitor for the `http://httpbin.org <http://httpbin.org>`_.
Httpbin.org monitor
-------------------
Adding of the monitor for the `httpbin.org`_ will be much easier because we have all the
Adding of a monitor for the `httpbin.org`_ will be much easier because we have all the
components ready. We just need to create a new provider in the container and update the
configuration.
Edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 31-35,41
:emphasize-lines: 30-34,40
"""Application containers module."""
"""Containers module."""
import logging
import sys
@ -768,8 +762,7 @@ Edit ``containers.py``:
from . import http, monitors, dispatcher
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
@ -837,27 +830,24 @@ You should see:
.. code-block:: bash
Starting monitoring-daemon-tutorial_monitor_1 ... done
Attaching to monitoring-daemon-tutorial_monitor_1
Starting asyncio-daemon-tutorial_monitor_1 ... done
Attaching to asyncio-daemon-tutorial_monitor_1
monitor_1 | [2020-08-08 18:09:08,540] [INFO] [Dispatcher]: Starting up
monitor_1 | [2020-08-08 18:09:08,618] [INFO] [HttpMonitor]: Check
monitor_1 | GET http://example.com
monitor_1 | response code: 200
monitor_1 | content length: 648
monitor_1 | request took: 0.077 seconds
monitor_1 |
monitor_1 | [2020-08-08 18:09:08,722] [INFO] [HttpMonitor]: Check
monitor_1 | GET https://httpbin.org/get
monitor_1 | response code: 200
monitor_1 | content length: 310
monitor_1 | request took: 0.18 seconds
monitor_1 |
monitor_1 | [2020-08-08 18:09:13,619] [INFO] [HttpMonitor]: Check
monitor_1 | GET http://example.com
monitor_1 | response code: 200
monitor_1 | content length: 648
monitor_1 | request took: 0.066 seconds
monitor_1 |
monitor_1 | [2020-08-08 18:09:13,681] [INFO] [HttpMonitor]: Check
monitor_1 | GET https://httpbin.org/get
monitor_1 | response code: 200
@ -867,12 +857,12 @@ You should see:
The functional part is done. Daemon monitors `http://example.com <http://example.com>`_ and
`https://httpbin.org <https://httpbin.org>`_.
In the next section we will add some tests.
In next section we will add some tests.
Tests
-----
It would be nice to add some tests. Let's do it.
In this section we will add some tests.
We will use `pytest <https://docs.pytest.org/en/stable/>`_ and
`coverage <https://coverage.readthedocs.io/>`_.
@ -909,7 +899,7 @@ and put next into it:
import pytest
from .containers import ApplicationContainer
from .containers import Container
@dataclasses.dataclass
@ -920,7 +910,7 @@ and put next into it:
@pytest.fixture
def container():
container = ApplicationContainer()
container = Container()
container.config.from_dict({
'log': {
'level': 'INFO',
@ -1002,14 +992,14 @@ You should see:
Name Stmts Miss Cover
----------------------------------------------------
monitoringdaemon/__init__.py 0 0 100%
monitoringdaemon/__main__.py 9 9 0%
monitoringdaemon/__main__.py 12 12 0%
monitoringdaemon/containers.py 11 0 100%
monitoringdaemon/dispatcher.py 43 5 88%
monitoringdaemon/dispatcher.py 44 5 89%
monitoringdaemon/http.py 6 3 50%
monitoringdaemon/monitors.py 23 1 96%
monitoringdaemon/tests.py 37 0 100%
----------------------------------------------------
TOTAL 129 18 86%
TOTAL 133 21 84%
.. note::
@ -1028,55 +1018,19 @@ In this tutorial we've built an ``asyncio`` monitoring daemon following the dep
injection principle.
We've used the ``Dependency Injector`` as a dependency injection framework.
The benefit you get with the ``Dependency Injector`` is the container. It starts to payoff
when you need to understand or change your application structure. It's easy with the container,
cause you have everything defined explicitly in one place:
With a help of :ref:`containers` and :ref:`providers` we have defined how to assemble application components.
.. code-block:: python
``List`` provider helped to inject a list of monitors into dispatcher.
:ref:`configuration-provider` helped to deal with reading YAML file.
"""Application containers module."""
We used :ref:`wiring` feature to inject dispatcher into the ``main()`` function.
:ref:`provider-overriding` feature helped in testing.
import logging
import sys
We kept all the dependencies injected explicitly. This will help when you need to add or
change something in future.
from dependency_injector import containers, providers
from . import http, monitors, dispatcher
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
config = providers.Configuration()
configure_logging = providers.Callable(
logging.basicConfig,
stream=sys.stdout,
level=config.log.level,
format=config.log.format,
)
http_client = providers.Factory(http.HttpClient)
example_monitor = providers.Factory(
monitors.HttpMonitor,
http_client=http_client,
options=config.monitors.example,
)
httpbin_monitor = providers.Factory(
monitors.HttpMonitor,
http_client=http_client,
options=config.monitors.httpbin,
)
dispatcher = providers.Factory(
dispatcher.Dispatcher,
monitors=providers.List(
example_monitor,
httpbin_monitor,
),
)
You can find complete project on the
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/asyncio-daemon>`_.
What's next?

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -50,7 +50,7 @@ inversion of control:
Here is a class diagram of the Movie Lister application:
.. image:: cli-images/classes_01.png
.. image:: cli-images/classes-01.png
The responsibilities are split next way:
@ -63,18 +63,18 @@ Prepare the environment
Let's create the environment for the project.
First we need to create a project folder and the virtual environment:
First we need to create a project folder:
.. code-block:: bash
mkdir movie-lister-tutorial
cd movie-lister-tutorial
python3 -m venv venv
Now let's activate the virtual environment:
Now let's create and activate virtual environment:
.. code-block:: bash
python3 -m venv venv
. venv/bin/activate
Project layout
@ -245,13 +245,13 @@ Edit ``containers.py``:
from dependency_injector import containers
class ApplicationContainer(containers.DeclarativeContainer):
class Container(containers.DeclarativeContainer):
...
Container is empty for now. We will add the providers in the following sections.
Let's also create the ``main()`` function. Its responsibility is to run our application. For now
it will just create the container.
it will just do nothing.
Edit ``__main__.py``:
@ -259,22 +259,18 @@ Edit ``__main__.py``:
"""Main module."""
from .containers import ApplicationContainer
from .containers import Container
def main():
container = ApplicationContainer()
def main() -> None:
...
if __name__ == '__main__':
container = Container()
main()
.. note::
Container is the first object in the application.
The container is used to create all other objects.
Csv finder
----------
@ -289,7 +285,7 @@ We will add:
After each step we will add the provider to the container.
.. image:: cli-images/classes_02.png
.. image:: cli-images/classes-02.png
Create the ``entities.py`` in the ``movies`` package:
@ -338,7 +334,7 @@ Now we need to add the ``Movie`` factory to the container. We need to add import
Edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 3,5,9
:emphasize-lines: 3,5,10
"""Containers module."""
@ -346,7 +342,8 @@ Edit ``containers.py``:
from . import entities
class ApplicationContainer(containers.DeclarativeContainer):
class Container(containers.DeclarativeContainer):
movie = providers.Factory(entities.Movie)
@ -420,7 +417,7 @@ Now let's add the csv finder into the container.
Edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 5,9,13-18
:emphasize-lines: 5,10,14-19
"""Containers module."""
@ -428,7 +425,8 @@ Edit ``containers.py``:
from . import finders, entities
class ApplicationContainer(containers.DeclarativeContainer):
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
@ -474,20 +472,21 @@ The configuration file is ready. Now let's update the ``main()`` function to sp
Edit ``__main__.py``:
.. code-block:: python
:emphasize-lines: 9
:emphasize-lines: 12
"""Main module."""
from .containers import ApplicationContainer
from .containers import Container
def main():
container = ApplicationContainer()
container.config.from_yaml('config.yml')
def main() -> None:
...
if __name__ == '__main__':
container = Container()
container.config.from_yaml('config.yml')
main()
Move on to the lister.
@ -542,7 +541,7 @@ and put next into it:
and edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 5,20-23
:emphasize-lines: 5,21-24
"""Containers module."""
@ -550,7 +549,8 @@ and edit ``containers.py``:
from . import finders, listers, entities
class ApplicationContainer(containers.DeclarativeContainer):
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
@ -570,36 +570,69 @@ and edit ``containers.py``:
All the components are created and added to the container.
Finally let's update the ``main()`` function.
Let's inject the ``lister`` into the ``main()`` function.
Edit ``__main__.py``:
.. code-block:: python
:emphasize-lines: 11-20
:emphasize-lines: 3-7,11,18
"""Main module."""
from .containers import ApplicationContainer
import sys
from dependency_injector.wiring import Provide
from .listers import MovieLister
from .containers import Container
def main():
container = ApplicationContainer()
container.config.from_yaml('config.yml')
lister = container.lister()
print(
'Francis Lawrence movies:',
lister.movies_directed_by('Francis Lawrence'),
)
print(
'2016 movies:',
lister.movies_released_in(2016),
)
def main(lister: MovieLister = Provide[Container.lister]) -> None:
...
if __name__ == '__main__':
container = Container()
container.config.from_yaml('config.yml')
container.wire(modules=[sys.modules[__name__]])
main()
Now when we call ``main()`` the container will assemble and inject the movie lister.
Let's add some payload to ``main()`` function. It will list movies directed by
Francis Lawrence and movies released in 2016.
Edit ``__main__.py``:
.. code-block:: python
:emphasize-lines: 12-18
"""Main module."""
import sys
from dependency_injector.wiring import Provide
from .listers import MovieLister
from .containers import Container
def main(lister: MovieLister = Provide[Container.lister]) -> None:
print('Francis Lawrence movies:')
for movie in lister.movies_directed_by('Francis Lawrence'):
print('\t-', movie)
print('2016 movies:')
for movie in lister.movies_released_in(2016):
print('\t-', movie)
if __name__ == '__main__':
container = Container()
container.config.from_yaml('config.yml')
container.wire(modules=[sys.modules[__name__]])
main()
All set. Now we run the application.
@ -612,12 +645,15 @@ Run in the terminal:
You should see:
.. code-block:: bash
.. code-block:: plain
Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]
2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')]
Francis Lawrence movies:
- Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')
2016 movies:
- Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards')
- Movie(title='The Jungle Book', year=2016, director='Jon Favreau')
Our application can work with the movies database in the csv format. We also need to support
Our application can work with the movies database in the csv format. We also want to support
the sqlite format. We will deal with it in the next section.
Sqlite finder
@ -688,7 +724,7 @@ Now we need to add the sqlite finder to the container and update lister's depend
Edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 20-24,28
:emphasize-lines: 21-25,29
"""Containers module."""
@ -696,7 +732,8 @@ Edit ``containers.py``:
from . import finders, listers, entities
class ApplicationContainer(containers.DeclarativeContainer):
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
@ -747,10 +784,13 @@ Run in the terminal:
You should see:
.. code-block:: bash
.. code-block:: plain
Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]
2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')]
Francis Lawrence movies:
- Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')
2016 movies:
- Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards')
- Movie(title='The Jungle Book', year=2016, director='Jon Favreau')
Our application now supports both formats: csv files and sqlite databases. Every time when we
need to work with the different format we need to make a code change in the container. We will
@ -782,7 +822,7 @@ Edit ``containers.py``:
from . import finders, listers, entities
class ApplicationContainer(containers.DeclarativeContainer):
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
@ -812,7 +852,7 @@ Edit ``containers.py``:
movie_finder=finder,
)
The switch is the ``config.finder.type`` option. When its value is ``csv``, the provider under
The switch is the ``config.finder.type`` option. When its value is ``csv``, the provider with the
``csv`` key is used. The same is for ``sqlite``.
Now we need to read the value of the ``config.finder.type`` option from the environment variable
@ -821,32 +861,34 @@ Now we need to read the value of the ``config.finder.type`` option from the envi
Edit ``__main__.py``:
.. code-block:: python
:emphasize-lines: 10
:emphasize-lines: 24
"""Main module."""
from .containers import ApplicationContainer
import sys
from dependency_injector.wiring import Provide
from .listers import MovieLister
from .containers import Container
def main():
container = ApplicationContainer()
def main(lister: MovieLister = Provide[Container.lister]) -> None:
print('Francis Lawrence movies:')
for movie in lister.movies_directed_by('Francis Lawrence'):
print('\t-', movie)
container.config.from_yaml('config.yml')
container.config.finder.type.from_env('MOVIE_FINDER_TYPE')
lister = container.lister()
print(
'Francis Lawrence movies:',
lister.movies_directed_by('Francis Lawrence'),
)
print(
'2016 movies:',
lister.movies_released_in(2016),
)
print('2016 movies:')
for movie in lister.movies_released_in(2016):
print('\t-', movie)
if __name__ == '__main__':
container = Container()
container.config.from_yaml('config.yml')
container.config.finder.type.from_env('MOVIE_FINDER_TYPE')
container.wire(modules=[sys.modules[__name__]])
main()
Done.
@ -858,12 +900,15 @@ Run in the terminal line by line:
MOVIE_FINDER_TYPE=csv python -m movies
MOVIE_FINDER_TYPE=sqlite python -m movies
The output should be something like this for each command:
The output should be similar for each command:
.. code-block:: bash
.. code-block:: plain
Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]
2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')]
Francis Lawrence movies:
- Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')
2016 movies:
- Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards')
- Movie(title='The Jungle Book', year=2016, director='Jon Favreau')
In the next section we will add some tests.
@ -908,12 +953,12 @@ and put next into it:
import pytest
from .containers import ApplicationContainer
from .containers import Container
@pytest.fixture
def container():
container = ApplicationContainer()
container = Container()
container.config.from_dict({
'finder': {
'type': 'csv',
@ -966,7 +1011,7 @@ Run in the terminal:
You should see:
.. code-block:: bash
.. code-block::
platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
plugins: cov-2.10.0
@ -974,18 +1019,18 @@ You should see:
movies/tests.py .. [100%]
---------- coverage: platform darwin, python 3.8.3-final-0 -----------
---------- coverage: platform darwin, python 3.8.5-final-0 -----------
Name Stmts Miss Cover
------------------------------------------
movies/__init__.py 0 0 100%
movies/__main__.py 10 10 0%
movies/__main__.py 17 17 0%
movies/containers.py 9 0 100%
movies/entities.py 7 1 86%
movies/finders.py 26 13 50%
movies/listers.py 8 0 100%
movies/tests.py 24 0 100%
------------------------------------------
TOTAL 84 24 71%
TOTAL 91 31 66%
.. note::
@ -1002,48 +1047,19 @@ Conclusion
In this tutorial we've built a CLI application following the dependency injection principle.
We've used the ``Dependency Injector`` as a dependency injection framework.
The benefit you get with the ``Dependency Injector`` is the container. It starts to payoff
when you need to understand or change your application structure. It's easy with the container,
cause you have everything defined explicitly in one place:
With a help of :ref:`containers` and :ref:`providers` we have defined how to assemble application components.
.. code-block:: python
``Selector`` provider served as a switch for selecting the database format based on a configuration.
:ref:`configuration-provider` helped to deal with reading YAML file and environment variable.
"""Containers module."""
We used :ref:`wiring` feature to inject the dependencies into the ``main()`` function.
:ref:`provider-overriding` feature helped in testing.
from dependency_injector import containers, providers
We kept all the dependencies injected explicitly. This will help when you need to add or
change something in future.
from . import finders, listers, entities
class ApplicationContainer(containers.DeclarativeContainer):
config = providers.Configuration()
movie = providers.Factory(entities.Movie)
csv_finder = providers.Singleton(
finders.CsvMovieFinder,
movie_factory=movie.provider,
path=config.finder.csv.path,
delimiter=config.finder.csv.delimiter,
)
sqlite_finder = providers.Singleton(
finders.SqliteMovieFinder,
movie_factory=movie.provider,
path=config.finder.sqlite.path,
)
finder = providers.Selector(
config.finder.type,
csv=csv_finder,
sqlite=sqlite_finder,
)
lister = providers.Factory(
listers.MovieLister,
movie_finder=finder,
)
You can find complete project on the
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/movie-lister>`_.
What's next?

View File

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

Before

Width:  |  Height:  |  Size: 647 KiB

After

Width:  |  Height:  |  Size: 647 KiB

View File

@ -21,7 +21,7 @@ Start from the scratch or jump to the section:
:backlinks: none
You can find complete project on the
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/ghnav-flask>`_.
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/flask>`_.
What are we going to build?
---------------------------
@ -43,25 +43,25 @@ How does Github Navigator work?
- User can click on the repository, the repository owner or the last commit to open its web page
on the Github.
.. image:: flask_images/screen_02.png
.. image:: flask-images/screen-02.png
Prepare the environment
-----------------------
Let's create the environment for the project.
First we need to create a project folder and the virtual environment:
First we need to create a project folder:
.. code-block:: bash
mkdir ghnav-flask-tutorial
cd ghnav-flask-tutorial
python3 -m venv venv
Now let's activate the virtual environment:
Now let's create and activate virtual environment:
.. code-block:: bash
python3 -m venv venv
. venv/bin/activate
Project layout
@ -110,13 +110,13 @@ You should see something like:
.. code-block:: bash
(venv) $ python -c "import dependency_injector; print(dependency_injector.__version__)"
3.22.0
4.0.0
(venv) $ python -c "import flask; print(flask.__version__)"
1.1.2
*Versions can be different. That's fine.*
Hello world!
Hello World!
------------
Let's create minimal application.
@ -133,34 +133,25 @@ Put next into the ``views.py``:
Ok, we have the view.
Now let's create the main part of our application - the container. Container will keep all of the
application components and their dependencies. First two providers we need to add are
the ``Flask`` application provider and the view provider.
Now let's create a container. Container will keep all of the application components and their dependencies.
Put next into the ``containers.py``:
Edit ``containers.py``:
.. code-block:: python
"""Application containers module."""
"""Containers module."""
from dependency_injector import containers
from dependency_injector.ext import flask
from flask import Flask
from . import views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
class Container(containers.DeclarativeContainer):
...
app = flask.Application(Flask, __name__)
Container is empty for now. We will add the providers in the following sections.
index_view = flask.View(views.index)
Finally we need to create the Flask application factory. It is traditionally called
``create_app()``. It will create the container. Then it will use the container to create
the Flask application. Last step is to configure the routing - we will assign ``index_view`` from
the container to handle user requests to the root ``/`` of our web application.
Finally we need to create Flask application factory. It will create and configure container
and Flask application. It is traditionally called ``create_app()``.
We will assign ``index`` view to handle user requests to the root ``/`` of our web application.
Put next into the ``application.py``:
@ -168,26 +159,21 @@ Put next into the ``application.py``:
"""Application module."""
from .containers import ApplicationContainer
from flask import Flask
from .containers import Container
from . import views
def create_app():
"""Create and return Flask application."""
container = ApplicationContainer()
def create_app() -> Flask:
container = Container()
app = container.app()
app = Flask(__name__)
app.container = container
app.add_url_rule('/', view_func=container.index_view.as_view())
app.add_url_rule('/', 'index', views.index)
return app
.. note::
Container is the first object in the application.
The container is used to create all other objects.
Ok. Now we're ready to say "Hello, World!".
Do next in the terminal:
@ -237,58 +223,34 @@ and run in the terminal:
.. code-block:: bash
pip install --upgrade -r requirements.txt
Now we need to add ``bootstrap-flask`` extension to the container.
Edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 6,16
"""Application containers module."""
from dependency_injector import containers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from . import views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = flask.Application(Flask, __name__)
bootstrap = flask.Extension(Bootstrap)
index_view = flask.View(views.index)
pip install -r requirements.txt
Let's initialize ``bootstrap-flask`` extension. We will need to modify ``create_app()``.
Edit ``application.py``:
.. code-block:: python
:emphasize-lines: 13-14
:emphasize-lines: 4,17-18
"""Application module."""
from .containers import ApplicationContainer
from flask import Flask
from flask_bootstrap import Bootstrap
from .containers import Container
from . import views
def create_app():
"""Create and return Flask application."""
container = ApplicationContainer()
def create_app() -> Flask:
container = Container()
app = container.app()
app = Flask(__name__)
app.container = container
app.add_url_rule('/', 'index', views.index)
bootstrap = container.bootstrap()
bootstrap = Bootstrap()
bootstrap.init_app(app)
app.add_url_rule('/', view_func=container.index_view.as_view())
return app
Now we need to add the templates. For doing this we will need to add the folder ``templates/`` to
@ -454,7 +416,7 @@ Make sure the app is running or use ``flask run`` and open ``http://127.0.0.1:50
You should see:
.. image:: flask_images/screen_01.png
.. image:: flask-images/screen-01.png
Connect to the GitHub
---------------------
@ -477,7 +439,7 @@ and run in the terminal:
.. code-block:: bash
pip install --upgrade -r requirements.txt
pip install -r requirements.txt
Now we need to add Github API client the container. We will need to add two more providers from
the ``dependency_injector.providers`` module:
@ -486,30 +448,18 @@ the ``dependency_injector.providers`` module:
- ``Configuration`` provider that will be used for providing the API token and the request timeout
for the ``Github`` client.
Let's do it.
Edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 3,7,19,21-25
:emphasize-lines: 3-4,9,11-15
"""Application containers module."""
"""Containers module."""
from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github
from . import views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = flask.Application(Flask, __name__)
bootstrap = flask.Extension(Bootstrap)
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
@ -519,8 +469,6 @@ Edit ``containers.py``:
timeout=config.github.request_timeout,
)
index_view = flask.View(views.index)
.. note::
We have used the configuration value before it was defined. That's the principle how
@ -528,11 +476,16 @@ Edit ``containers.py``:
Use first, define later.
.. note::
Don't forget to remove the Ellipsis ``...`` from the container. We don't need it anymore
since we container is not empty.
Now let's add the configuration file.
We will use YAML.
Create an empty file ``config.yml`` in the root root of the project:
Create an empty file ``config.yml`` in the root of the project:
.. code-block:: bash
:emphasize-lines: 11
@ -575,7 +528,7 @@ and install it:
.. code-block:: bash
pip install --upgrade -r requirements.txt
pip install -r requirements.txt
We will use environment variable ``GITHUB_TOKEN`` to provide the API token.
@ -587,27 +540,29 @@ Now we need to edit ``create_app()`` to make two things when application starts:
Edit ``application.py``:
.. code-block:: python
:emphasize-lines: 9-10
:emphasize-lines: 12-13
"""Application module."""
from .containers import ApplicationContainer
from flask import Flask
from flask_bootstrap import Bootstrap
from .containers import Container
from . import views
def create_app():
"""Create and return Flask application."""
container = ApplicationContainer()
def create_app() -> Flask:
container = Container()
container.config.from_yaml('config.yml')
container.config.github.auth_token.from_env('GITHUB_TOKEN')
app = container.app()
app = Flask(__name__)
app.container = container
app.add_url_rule('/', 'index', views.index)
bootstrap = container.bootstrap()
bootstrap = Bootstrap()
bootstrap.init_app(app)
app.add_url_rule('/', view_func=container.index_view.as_view())
return app
Now we need create an API token.
@ -636,7 +591,7 @@ Github API client setup is done.
Search service
--------------
Now it's time to add the ``SearchService``. It will:
Now it's time to add ``SearchService``. It will:
- Perform the search.
- Fetch commit extra data for each result.
@ -717,25 +672,17 @@ Now let's add ``SearchService`` to the container.
Edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 9,27-30
:emphasize-lines: 6,19-22
"""Application containers module."""
"""Containers module."""
from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github
from . import services, views
from . import services
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = flask.Application(Flask, __name__)
bootstrap = flask.Extension(Bootstrap)
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
@ -750,26 +697,28 @@ Edit ``containers.py``:
github_client=github_client,
)
index_view = flask.View(views.index)
Inject search service into view
-------------------------------
Make the search work
--------------------
Now we are ready to make the search work.
Now we are ready to make the search work. Let's use the ``SearchService`` in the ``index`` view.
Let's inject ``SearchService`` into the ``index`` view. We will use :ref:`Wiring` feature.
Edit ``views.py``:
.. code-block:: python
:emphasize-lines: 5,8,12
:emphasize-lines: 4,6-7,10,14
"""Views module."""
from flask import request, render_template
from dependency_injector.wiring import Provide
from .services import SearchService
from .containers import Container
def index(search_service: SearchService):
def index(search_service: SearchService = Provide[Container.search_service]):
query = request.args.get('query', 'Dependency Injector')
limit = request.args.get('limit', 10, int)
@ -782,54 +731,44 @@ Edit ``views.py``:
repositories=repositories,
)
Now let's inject the ``SearchService`` dependency into the ``index`` view.
To make the injection work we need to wire the container instance with the ``views`` module.
This needs to be done once. After it's done we can use ``Provide`` markers to specify as many
injections as needed for any view.
Edit ``containers.py``:
Edit ``application.py``:
.. code-block:: python
:emphasize-lines: 32-35
:emphasize-lines: 14
"""Application containers module."""
"""Application module."""
from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github
from . import services, views
from .containers import Container
from . import views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
def create_app() -> Flask:
container = Container()
container.config.from_yaml('config.yml')
container.config.github.auth_token.from_env('GITHUB_TOKEN')
container.wire(modules=[views])
app = flask.Application(Flask, __name__)
app = Flask(__name__)
app.container = container
app.add_url_rule('/', 'index', views.index)
bootstrap = flask.Extension(Bootstrap)
bootstrap = Bootstrap()
bootstrap.init_app(app)
config = providers.Configuration()
github_client = providers.Factory(
Github,
login_or_token=config.github.auth_token,
timeout=config.github.request_timeout,
)
search_service = providers.Factory(
services.SearchService,
github_client=github_client,
)
index_view = flask.View(
views.index,
search_service=search_service,
)
return app
Make sure the app is running or use ``flask run`` and open ``http://127.0.0.1:5000/``.
You should see:
.. image:: flask_images/screen_02.png
.. image:: flask-images/screen-02.png
Make some refactoring
---------------------
@ -844,19 +783,21 @@ Let's make some refactoring. We will move these values to the config.
Edit ``views.py``:
.. code-block:: python
:emphasize-lines: 8-14
:emphasize-lines: 10-16
"""Views module."""
from flask import request, render_template
from dependency_injector.wiring import Provide
from .services import SearchService
from .containers import Container
def index(
search_service: SearchService,
default_query: str,
default_limit: int,
search_service: SearchService = Provide[Container.search_service],
default_query: str = Provide[Container.config.default.query],
default_limit: int = Provide[Container.config.default.limit.as_int()],
):
query = request.args.get('query', default_query)
limit = request.args.get('limit', default_limit, int)
@ -870,53 +811,6 @@ Edit ``views.py``:
repositories=repositories,
)
Now we need to inject these values. Let's update the container.
Edit ``containers.py``:
.. code-block:: python
:emphasize-lines: 35-36
"""Application containers module."""
from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github
from . import services, views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = flask.Application(Flask, __name__)
bootstrap = flask.Extension(Bootstrap)
config = providers.Configuration()
github_client = providers.Factory(
Github,
login_or_token=config.github.auth_token,
timeout=config.github.request_timeout,
)
search_service = providers.Factory(
services.SearchService,
github_client=github_client,
)
index_view = flask.View(
views.index,
search_service=search_service,
default_query=config.search.default_query,
default_limit=config.search.default_limit,
)
Finally let's update the config.
Edit ``config.yml``:
.. code-block:: yaml
@ -924,20 +818,18 @@ Edit ``config.yml``:
github:
request_timeout: 10
search:
default_query: "Dependency Injector"
default_limit: 10
default:
query: "Dependency Injector"
limit: 10
That's it.
The refactoring is done. We've made it cleaner.
That's it. The refactoring is done. We've made it cleaner.
Tests
-----
It would be nice to add some tests. Let's do this.
In this section we will add some tests.
We will use `pytest <https://docs.pytest.org/en/stable/>`_ and
We will use `pytest <https://docs.pytest.org/en/stable/>`_ with its Flask extension and
`coverage <https://coverage.readthedocs.io/>`_.
Edit ``requirements.txt``:
@ -953,7 +845,7 @@ Edit ``requirements.txt``:
pytest-flask
pytest-cov
And let's install it:
And install added packages:
.. code-block:: bash
@ -982,7 +874,7 @@ Create empty file ``tests.py`` in the ``githubnavigator`` package:
and put next into it:
.. code-block:: python
:emphasize-lines: 42,65
:emphasize-lines: 44,67
"""Tests module."""
@ -997,7 +889,9 @@ and put next into it:
@pytest.fixture
def app():
return create_app()
app = create_app()
yield app
app.container.unwire()
def test_index(client, app):
@ -1074,13 +968,13 @@ You should see:
Name Stmts Miss Cover
----------------------------------------------------
githubnavigator/__init__.py 0 0 100%
githubnavigator/application.py 11 0 100%
githubnavigator/containers.py 13 0 100%
githubnavigator/application.py 15 0 100%
githubnavigator/containers.py 7 0 100%
githubnavigator/services.py 14 0 100%
githubnavigator/tests.py 32 0 100%
githubnavigator/views.py 7 0 100%
githubnavigator/tests.py 34 0 100%
githubnavigator/views.py 9 0 100%
----------------------------------------------------
TOTAL 77 0 100%
TOTAL 79 0 100%
.. note::
@ -1091,53 +985,22 @@ You should see:
Conclusion
----------
We are done.
In this tutorial we've built a ``Flask`` application following the dependency injection principle.
We've used the ``Dependency Injector`` as a dependency injection framework.
The main part of this application is the container. It keeps all the application components and
their dependencies defined explicitly in one place:
:ref:`containers` and :ref:`providers` helped to specify how to assemble search service and
integrate it with a 3rd-party library.
.. code-block:: python
:ref:`configuration-provider` helped to deal with reading YAML file and environment variable.
"""Application containers module."""
We used :ref:`wiring` feature to inject the dependencies into the ``index()`` view.
:ref:`provider-overriding` feature helped in testing.
from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github
We kept all the dependencies injected explicitly. This will help when you need to add or
change something in future.
from . import services, views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = flask.Application(Flask, __name__)
bootstrap = flask.Extension(Bootstrap)
config = providers.Configuration()
github_client = providers.Factory(
Github,
login_or_token=config.github.auth_token,
timeout=config.github.request_timeout,
)
search_service = providers.Factory(
services.SearchService,
github_client=github_client,
)
index_view = flask.View(
views.index,
search_service=search_service,
default_query=config.search.default_query,
default_limit=config.search.default_limit,
)
You can find complete project on the
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/flask>`_.
What's next?

201
docs/wiring.rst Normal file
View File

@ -0,0 +1,201 @@
.. _wiring:
Wiring
======
Wiring feature provides a way to inject container providers into the functions and methods.
To use wiring you need:
- **Place markers in the code**. Wiring marker specifies what provider to inject,
e.g. ``Provide[Container.bar]``. This helps container to find the injections.
- **Wire the container with the markers in the code**. Call ``container.wire()``
specifying modules and packages you would like to wire it with.
- **Use functions and classes as you normally do**. Framework will provide specified injections.
.. literalinclude:: ../examples/wiring/example.py
:language: python
:lines: 3-
Markers
-------
Wiring feature uses markers to make injections. Injection marker is specified as a default value of
a function or method argument:
.. code-block:: python
from dependency_injector.wiring import Provide
def foo(bar: Bar = Provide[Container.bar]):
...
Specifying an annotation is optional.
There are two types of markers:
- ``Provide[foo]`` - call the provider ``foo`` and injects the result
- ``Provider[foo]`` - injects the provider ``foo`` itself
.. code-block:: python
from dependency_injector.wiring import Provider
def foo(bar_provider: Callable[..., Bar] = Provider[Container.bar]):
bar = bar_provider()
...
You can use configuration, provided instance and sub-container providers as you normally do.
.. code-block:: python
def foo(token: str = Provide[Container.config.api_token]):
...
def foo(timeout: int = Provide[Container.config.timeout.as_(int)]):
...
def foo(baz: Baz = Provide[Container.bar.provided.baz]):
...
def foo(bar: Bar = Provide[Container.subcontainer.bar]):
...
Wiring with modules and packages
--------------------------------
To wire a container with a module you need to call ``container.wire(modules=[...])`` method. Argument
``modules`` is an iterable of the module objects.
.. code-block:: python
from yourapp import module1, module2
container = Container()
container.wire(modules=[module1, module2])
You can wire container with a package. Container walks recursively over package modules.
.. code-block:: python
from yourapp import package1, package2
container = Container()
container.wire(packages=[package1, package2])
Arguments ``modules`` and ``packages`` can be used together.
When wiring is done functions and methods with the markers are patched to provide injections when called.
.. code-block:: python
def foo(bar: Bar = Provide[Container.bar]):
...
container = Container()
container.wire(modules=[sys.modules[__name__]])
foo() # <--- Argument "bar" is injected
Injections are done as keyword arguments.
.. code-block:: python
foo() # Equivalent to:
foo(bar=container.bar())
Context keyword arguments have a priority over injections.
.. code-block:: python
foo(bar=Bar()) # Bar() is injected
To unpatch previously patched functions and methods call ``container.unwire()`` method.
.. code-block:: python
container.unwire()
You can use that in testing to re-create and re-wire a container before each test.
.. code-block:: python
import unittest
class SomeTest(unittest.TestCase):
def setUp(self):
self.container = Container()
self.container.wire(modules=[module1, module2])
self.addCleanup(self.container.unwire)
.. code-block:: python
import pytest
@pytest.fixture
def container():
container = Container()
container.wire(modules=[module1, module2])
yield container
container.unwire()
.. note::
Wiring can take time if you have a large codebase. Consider to persist a container instance and
avoid re-wiring between tests.
.. note::
Python has a limitation on patching already imported individual members. To protect from errors
prefer an import of modules instead of individual members or make sure that imports happen
after the wiring:
.. code-block:: python
from . import module
module.fn()
# instead of
from .module import fn
fn()
Integration with other frameworks
---------------------------------
Wiring feature helps to integrate with other frameworks like Django, Flask, etc.
With wiring you do not need to change the traditional application structure of your framework.
1. Create a container and put framework-independent components as providers.
2. Place wiring markers in the functions and methods where you want the providers
to be injected (Flask or Django views, Aiohttp or Sanic handlers, etc).
3. Wire the container with the application modules.
4. Run the application.
.. literalinclude:: ../examples/wiring/flask_example.py
:language: python
:lines: 3-
Take a look at other application examples:
- :ref:`application-single-container`
- :ref:`application-multiple-containers`
- :ref:`decoupled-packages`
- :ref:`django-example`
- :ref:`flask-example`
- :ref:`aiohttp-example`
- :ref:`sanic-example`
.. disqus::

29
examples/demo/after.py Normal file
View File

@ -0,0 +1,29 @@
import os
class ApiClient:
def __init__(self, api_key: str, timeout: int):
self.api_key = api_key # <-- dependency is injected
self.timeout = timeout # <-- dependency is injected
class Service:
def __init__(self, api_client: ApiClient):
self.api_client = api_client # <-- dependency is injected
def main(service: Service): # <-- dependency is injected
...
if __name__ == '__main__':
main(
service=Service(
api_client=ApiClient(
api_key=os.getenv('API_KEY'),
timeout=os.getenv('TIMEOUT'),
),
),
)

23
examples/demo/before.py Normal file
View File

@ -0,0 +1,23 @@
import os
class ApiClient:
def __init__(self):
self.api_key = os.getenv('API_KEY') # <-- dependency
self.timeout = os.getenv('TIMEOUT') # <-- dependency
class Service:
def __init__(self):
self.api_client = ApiClient() # <-- dependency
def main() -> None:
service = Service() # <-- dependency
...
if __name__ == '__main__':
main()

View File

@ -1,17 +1,10 @@
import sys
from unittest import mock
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide
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
from after import ApiClient, Service
class Container(containers.DeclarativeContainer):
@ -30,9 +23,17 @@ class Container(containers.DeclarativeContainer):
)
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')
container.wire(modules=[sys.modules[__name__]])
service = container.service()
main() # <-- dependency is injected automatically
with container.api_client.override(mock.Mock()):
main() # <-- overridden dependency is injected automatically

View File

@ -1,18 +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
if __name__ == '__main__':
service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))

View File

@ -1,18 +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()
if __name__ == '__main__':
service = Service()

View File

@ -1,11 +0,0 @@
from unittest import mock
from demo import Container
if __name__ == '__main__':
container = Container()
with container.api_client.override(mock.Mock()):
service = container.service()
assert isinstance(service.api_client, mock.Mock)

View File

@ -1,8 +1,10 @@
Aiohttp Dependency Injection Example
====================================
Aiohttp + Dependency Injector Example
=====================================
Application ``giphynavigator`` is an `Aiohttp <https://docs.aiohttp.org/>`_ +
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ application.
This is an `Aiohttp <https://docs.aiohttp.org/>`_ +
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ example application.
The example application is a REST API that searches for funny GIFs on the `Giphy <https://giphy.com/>`_.
Run
---
@ -106,12 +108,11 @@ The output should be something like:
Name Stmts Miss Cover
---------------------------------------------------
giphynavigator/__init__.py 0 0 100%
giphynavigator/__main__.py 5 5 0%
giphynavigator/application.py 10 0 100%
giphynavigator/containers.py 10 0 100%
giphynavigator/application.py 12 0 100%
giphynavigator/containers.py 6 0 100%
giphynavigator/giphy.py 14 9 36%
giphynavigator/handlers.py 9 0 100%
giphynavigator/services.py 9 1 89%
giphynavigator/tests.py 35 0 100%
giphynavigator/views.py 7 0 100%
giphynavigator/tests.py 37 0 100%
---------------------------------------------------
TOTAL 90 15 83%
TOTAL 87 10 89%

View File

@ -0,0 +1,5 @@
giphy:
request_timeout: 10
default:
query: "Dependency Injector"
limit: 10

View File

@ -0,0 +1,20 @@
"""Application module."""
from aiohttp import web
from .containers import Container
from . import handlers
def create_app() -> web.Application:
container = Container()
container.config.from_yaml('config.yml')
container.config.giphy.api_key.from_env('GIPHY_API_KEY')
container.wire(modules=[handlers])
app = web.Application()
app.container = container
app.add_routes([
web.get('/', handlers.index),
])
return app

View File

@ -0,0 +1,21 @@
"""Containers module."""
from dependency_injector import containers, providers
from . import giphy, services
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
giphy_client = providers.Factory(
giphy.GiphyClient,
api_key=config.giphy.api_key,
timeout=config.giphy.request_timeout,
)
search_service = providers.Factory(
services.SearchService,
giphy_client=giphy_client,
)

View File

@ -1,15 +1,17 @@
"""Views module."""
"""Handlers module."""
from aiohttp import web
from dependency_injector.wiring import Provide
from .services import SearchService
from .containers import Container
async def index(
request: web.Request,
search_service: SearchService,
default_query: str,
default_limit: int,
search_service: SearchService = Provide[Container.search_service],
default_query: str = Provide[Container.config.default.query],
default_limit: int = Provide[Container.config.default.limit.as_int()],
) -> web.Response:
query = request.query.get('query', default_query)
limit = int(request.query.get('limit', default_limit))

View File

@ -10,7 +10,9 @@ from giphynavigator.giphy import GiphyClient
@pytest.fixture
def app():
return create_app()
app = create_app()
yield app
app.container.unwire()
@pytest.fixture
@ -73,5 +75,5 @@ async def test_index_default_params(client, app):
assert response.status == 200
data = await response.json()
assert data['query'] == app.container.config.search.default_query()
assert data['limit'] == app.container.config.search.default_limit()
assert data['query'] == app.container.config.default.query()
assert data['limit'] == app.container.config.default.limit()

View File

@ -24,6 +24,6 @@ You should see:
.. code-block:: bash
[2020-09-04 16:06:00,750] [DEBUG] [example.services.UserService]: User user@example.com has been found in database
[2020-09-04 16:06:00,750] [DEBUG] [example.services.AuthService]: User user@example.com has been successfully authenticated
[2020-09-04 16:06:00,750] [DEBUG] [example.services.PhotoService]: Photo photo.jpg has been successfully uploaded by user user@example.com
[2020-10-06 15:36:55,961] [DEBUG] [example.services.UserService]: User user@example.com has been found in database
[2020-10-06 15:36:55,961] [DEBUG] [example.services.AuthService]: User user@example.com has been successfully authenticated
[2020-10-06 15:36:55,961] [DEBUG] [example.services.PhotoService]: Photo photo.jpg has been successfully uploaded by user user@example.com

View File

@ -2,23 +2,29 @@
import sys
from dependency_injector.wiring import Provide
from .services import UserService, AuthService, PhotoService
from .containers import Application
def main(email: str, password: str, photo: str) -> None:
application = Application()
application.config.from_yaml('config.yml')
application.core.configure_logging()
user_service = application.services.user()
auth_service = application.services.auth()
photo_service = application.services.photo()
def main(
email: str,
password: str,
photo: str,
user_service: UserService = Provide[Application.services.user],
auth_service: AuthService = Provide[Application.services.auth],
photo_service: PhotoService = Provide[Application.services.photo],
) -> None:
user = user_service.get_user(email)
auth_service.authenticate(user, password)
photo_service.upload_photo(user, photo)
if __name__ == '__main__':
application = Application()
application.config.from_yaml('config.yml')
application.core.configure_logging()
application.wire(modules=[sys.modules[__name__]])
main(*sys.argv[1:])

View File

@ -24,6 +24,6 @@ You should see:
.. code-block:: bash
[2020-09-04 15:27:27,727] [DEBUG] [example.services.UserService]: User user@example.com has been found in database
[2020-09-04 15:27:27,727] [DEBUG] [example.services.AuthService]: User user@example.com has been successfully authenticated
[2020-09-04 15:27:27,727] [DEBUG] [example.services.PhotoService]: Photo photo.jpg has been successfully uploaded by user user@example.com
[2020-10-06 15:32:33,195] [DEBUG] [example.services.UserService]: User user@example.com has been found in database
[2020-10-06 15:32:33,195] [DEBUG] [example.services.AuthService]: User user@example.com has been successfully authenticated
[2020-10-06 15:32:33,195] [DEBUG] [example.services.PhotoService]: Photo photo.jpg has been successfully uploaded by user user@example.com

View File

@ -2,23 +2,29 @@
import sys
from dependency_injector.wiring import Provide
from .services import UserService, AuthService, PhotoService
from .containers import Container
def main(email: str, password: str, photo: str) -> None:
container = Container()
container.configure_logging()
container.config.from_ini('config.ini')
user_service = container.user_service()
auth_service = container.auth_service()
photo_service = container.photo_service()
def main(
email: str,
password: str,
photo: str,
user_service: UserService = Provide[Container.user_service],
auth_service: AuthService = Provide[Container.auth_service],
photo_service: PhotoService = Provide[Container.photo_service],
) -> None:
user = user_service.get_user(email)
auth_service.authenticate(user, password)
photo_service.upload_photo(user, photo)
if __name__ == '__main__':
container = Container()
container.configure_logging()
container.config.from_ini('config.ini')
container.wire(modules=[sys.modules[__name__]])
main(*sys.argv[1:])

View File

@ -1,8 +1,10 @@
Asyncio Daemon Dependency Injection Example
===========================================
Asyncio Daemon + Dependency Injector Example
============================================
Application ``monitoringdaemon`` is an `asyncio <https://docs.python.org/3/library/asyncio.html>`_
+ `Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ application.
This is an `asyncio <https://docs.python.org/3/library/asyncio.html>`_ +
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ example application.
The example application is a daemon that monitors availability of web services.
Run
---
@ -31,19 +33,16 @@ The output should be something like:
monitor_1 | response code: 200
monitor_1 | content length: 648
monitor_1 | request took: 0.074 seconds
monitor_1 |
monitor_1 | [2020-08-08 17:04:36,811] [INFO] [HttpMonitor]: Check
monitor_1 | GET https://httpbin.org/get
monitor_1 | response code: 200
monitor_1 | content length: 310
monitor_1 | request took: 0.153 seconds
monitor_1 |
monitor_1 | [2020-08-08 17:04:41,731] [INFO] [HttpMonitor]: Check
monitor_1 | GET http://example.com
monitor_1 | response code: 200
monitor_1 | content length: 648
monitor_1 | request took: 0.067 seconds
monitor_1 |
monitor_1 | [2020-08-08 17:04:41,787] [INFO] [HttpMonitor]: Check
monitor_1 | GET https://httpbin.org/get
monitor_1 | response code: 200
@ -71,17 +70,17 @@ The output should be something like:
plugins: asyncio-0.14.0, cov-2.10.0
collected 2 items
monitoringdaemon/tests.py .. [100%]
monitoringdaemon/tests.py .. [100%]
----------- coverage: platform linux, python 3.8.3-final-0 -----------
Name Stmts Miss Cover
----------------------------------------------------
monitoringdaemon/__init__.py 0 0 100%
monitoringdaemon/__main__.py 9 9 0%
monitoringdaemon/__main__.py 12 12 0%
monitoringdaemon/containers.py 11 0 100%
monitoringdaemon/dispatcher.py 43 5 88%
monitoringdaemon/dispatcher.py 44 5 89%
monitoringdaemon/http.py 6 3 50%
monitoringdaemon/monitors.py 23 1 96%
monitoringdaemon/tests.py 37 0 100%
----------------------------------------------------
TOTAL 129 18 86%
TOTAL 133 21 84%

View File

@ -0,0 +1,21 @@
"""Main module."""
import sys
from dependency_injector.wiring import Provide
from .dispatcher import Dispatcher
from .containers import Container
def main(dispatcher: Dispatcher = Provide[Container.dispatcher]) -> None:
dispatcher.run()
if __name__ == '__main__':
container = Container()
container.config.from_yaml('config.yml')
container.configure_logging()
container.wire(modules=[sys.modules[__name__]])
main()

View File

@ -1,4 +1,4 @@
"""Application containers module."""
"""Containers module."""
import logging
import sys
@ -8,8 +8,7 @@ from dependency_injector import containers, providers
from . import http, monitors, dispatcher
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
class Container(containers.DeclarativeContainer):
config = providers.Configuration()

View File

@ -44,6 +44,7 @@ class Dispatcher:
self._logger.info('Shutting down')
for task, monitor in zip(self._monitor_tasks, self._monitors):
task.cancel()
self._monitor_tasks.clear()
self._logger.info('Shutdown finished successfully')
@staticmethod

View File

@ -47,7 +47,7 @@ class HttpMonitor(Monitor):
' %s %s\n'
' response code: %s\n'
' content length: %s\n'
' request took: %s seconds\n',
' request took: %s seconds',
self._method,
self._url,
response.status,

View File

@ -6,7 +6,7 @@ from unittest import mock
import pytest
from .containers import ApplicationContainer
from .containers import Container
@dataclasses.dataclass
@ -17,7 +17,7 @@ class RequestStub:
@pytest.fixture
def container():
container = ApplicationContainer()
container = Container()
container.config.from_dict({
'log': {
'level': 'INFO',

View File

@ -1,15 +1,26 @@
"""Main module."""
import sys
from dependency_injector.wiring import Provide
from .user.repositories import UserRepository
from .photo.repositories import PhotoRepository
from .analytics.services import AggregationService
from .containers import ApplicationContainer
def main() -> None:
application = ApplicationContainer()
application.config.from_ini('config.ini')
user_repository = application.user_package.user_repository()
photo_repository = application.photo_package.photo_repository()
def main(
user_repository: UserRepository = Provide[
ApplicationContainer.user_package.user_repository
],
photo_repository: PhotoRepository = Provide[
ApplicationContainer.photo_package.photo_repository
],
aggregation_service: AggregationService = Provide[
ApplicationContainer.analytics_package.aggregation_service
],
) -> None:
user1 = user_repository.get(id=1)
user1_photos = photo_repository.get_photos(user1.id)
print(f'Retrieve user id={user1.id}, photos count={len(user1_photos)}')
@ -18,11 +29,14 @@ def main() -> None:
user2_photos = photo_repository.get_photos(user2.id)
print(f'Retrieve user id={user2.id}, photos count={len(user2_photos)}')
aggregation_service = application.analytics_package.aggregation_service()
assert aggregation_service.user_repository is user_repository
assert aggregation_service.photo_repository is photo_repository
print('Aggregate analytics from user and photo packages')
if __name__ == '__main__':
application = ApplicationContainer()
application.config.from_ini('config.ini')
application.wire(modules=[sys.modules[__name__]])
main()

1
examples/miniapps/django/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.sqlite3

View File

@ -0,0 +1,2 @@
[pydocstyle]
ignore = D100,D101,D102,D103,D105,D107,D203,D212,D213,D400,D406,D407,D411,D413,D415

View File

@ -0,0 +1,113 @@
Django + Dependency Injector Example
====================================
This is a `Django <https://www.djangoproject.com/>`_ +
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ example application.
The example application helps to search for repositories on the Github.
.. image:: screenshot.png
Run
---
Create virtual environment:
.. code-block:: bash
virtualenv venv
. venv/bin/activate
Install requirements:
.. code-block:: bash
pip install -r requirements.txt
Run migrations:
.. code-block:: bash
python manage.py migrate
To run the application do:
.. code-block:: bash
python manage.py runserver
The output should be something like:
.. code-block::
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
October 05, 2020 - 03:17:05
Django version 3.1.2, using settings 'githubnavigator.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
After that visit http://127.0.0.1:8000/ in your browser.
.. note::
Github has a rate limit. When the rate limit is exceed you will see an exception
``github.GithubException.RateLimitExceededException``. For unauthenticated requests, the rate
limit allows for up to 60 requests per hour. To extend the limit to 5000 requests per hour you
need to set personal access token.
It's easy:
- Follow this `guide <https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token>`_ to create a token.
- Set a token to the environment variable:
.. code-block:: bash
export GITHUB_TOKEN=<your token>
- Restart the app with ``python manage.py runserver``
`Read more on Github rate limit <https://developer.github.com/v3/#rate-limiting>`_
Test
----
This application comes with the unit tests.
To run the tests do:
.. code-block:: bash
coverage run --source='.' manage.py test && coverage report
The output should be something like:
.. code-block::
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.037s
OK
Destroying test database for alias 'default'...
Name Stmts Miss Cover
---------------------------------------------------
githubnavigator/__init__.py 4 0 100%
githubnavigator/asgi.py 4 4 0%
githubnavigator/containers.py 7 0 100%
githubnavigator/services.py 14 0 100%
githubnavigator/settings.py 23 0 100%
githubnavigator/urls.py 3 0 100%
githubnavigator/wsgi.py 4 4 0%
manage.py 12 2 83%
web/__init__.py 0 0 100%
web/apps.py 7 0 100%
web/tests.py 28 0 100%
web/urls.py 3 0 100%
web/views.py 11 0 100%
---------------------------------------------------
TOTAL 120 10 92%

View File

@ -0,0 +1,8 @@
"""Project package."""
from .containers import Container
from . import settings
container = Container()
container.config.from_dict(settings.__dict__)

View File

@ -0,0 +1,15 @@
"""ASGI config for githubnavigator project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'githubnavigator.settings')
application = get_asgi_application()

View File

@ -0,0 +1,22 @@
"""Containers module."""
from dependency_injector import containers, providers
from github import Github
from . import services
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
github_client = providers.Factory(
Github,
login_or_token=config.GITHUB_TOKEN,
timeout=config.GITHUB_REQUEST_TIMEOUT,
)
search_service = providers.Factory(
services.SearchService,
github_client=github_client,
)

View File

@ -0,0 +1,131 @@
"""
Django settings for githubnavigator project.
Generated by 'django-admin startproject' using Django 3.0.8.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.0/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = ')6*iyg26c9l!fvyvwd&3+vyf-dcw)e=5x2t(j)(*c29z@ykhi0'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'web.apps.WebConfig',
'bootstrap4',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'githubnavigator.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'githubnavigator.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = '/static/'
# Github client settings
GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')
GITHUB_REQUEST_TIMEOUT = 10
# Search settings
DEFAULT_LIMIT = 5
DEFAULT_QUERY = 'Dependency Injector'
LIMIT_OPTIONS = [5, 10, 20]

View File

@ -0,0 +1,22 @@
"""githubnavigator URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('', include('web.urls')),
path('admin/', admin.site.urls),
]

View File

@ -0,0 +1,16 @@
"""
WSGI config for githubnavigator project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'githubnavigator.settings')
application = get_wsgi_application()

View File

@ -0,0 +1,21 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'githubnavigator.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,5 @@
dependency-injector
django
django-bootstrap4
pygithub
coverage

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

View File

@ -0,0 +1 @@
"""Web application package."""

View File

@ -0,0 +1,13 @@
"""Application config module."""
from django.apps import AppConfig
from githubnavigator import container
from . import views
class WebConfig(AppConfig):
name = 'web'
def ready(self):
container.wire(modules=[views])

View File

@ -0,0 +1,10 @@
{% extends 'bootstrap4/bootstrap4.html' %}
{% load bootstrap4 %}
{% block bootstrap4_title %}{% block title %}{% endblock %}{% endblock %}
{% block bootstrap4_content %}
{% autoescape off %}{% bootstrap_messages %}{% endautoescape %}
{% block content %}(no content){% endblock %}
{% endblock %}

View File

@ -0,0 +1,69 @@
{% extends "base.html" %}
{% block title %}Github Navigator{% endblock %}
{% block content %}
<div class="container">
<h1 class="mb-4">Github Navigator</h1>
<form>
<div class="form-group form-row">
<div class="col-10">
<label for="search_query" class="col-form-label">
Search for:
</label>
<input class="form-control" type="text" id="search_query"
placeholder="Type something to search on the GitHub"
name="query"
value="{{ query }}">
</div>
<div class="col">
<label for="search_limit" class="col-form-label">
Limit:
</label>
<select class="form-control" id="search_limit" name="limit">
{% for value in limit_options %}
<option {% if value == limit %}selected{% endif %}>
{{ value }}
</option>
{% endfor %}
</select>
</div>
</div>
</form>
<p><small>Results found: {{ repositories|length }}</small></p>
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Repository</th>
<th class="text-nowrap">Repository owner</th>
<th class="text-nowrap">Last commit</th>
</tr>
</thead>
<tbody>
{% for repository in repositories %} {{n}}
<tr>
<th>{{ loop.index }}</th>
<td><a href="{{ repository.url }}">
{{ repository.name }}</a>
</td>
<td><a href="{{ repository.owner.url }}">
<img src="{{ repository.owner.avatar_url }}"
alt="avatar" height="24" width="24"/></a>
<a href="{{ repository.owner.url }}">
{{ repository.owner.login }}</a>
</td>
<td><a href="{{ repository.latest_commit.url }}">
{{ repository.latest_commit.sha }}</a>
{{ repository.latest_commit.message }}
{{ repository.latest_commit.author_name }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,63 @@
"""Tests module."""
from unittest import mock
from django.urls import reverse
from django.test import TestCase
from github import Github
from githubnavigator import container
class IndexTests(TestCase):
def test_index(self):
github_client_mock = mock.Mock(spec=Github)
github_client_mock.search_repositories.return_value = [
mock.Mock(
html_url='repo1-url',
name='repo1-name',
owner=mock.Mock(
login='owner1-login',
html_url='owner1-url',
avatar_url='owner1-avatar-url',
),
get_commits=mock.Mock(return_value=[mock.Mock()]),
),
mock.Mock(
html_url='repo2-url',
name='repo2-name',
owner=mock.Mock(
login='owner2-login',
html_url='owner2-url',
avatar_url='owner2-avatar-url',
),
get_commits=mock.Mock(return_value=[mock.Mock()]),
),
]
with container.github_client.override(github_client_mock):
response = self.client.get(reverse('index'))
self.assertContains(response, 'Results found: 2')
self.assertContains(response, 'repo1-url')
self.assertContains(response, 'repo1-name')
self.assertContains(response, 'owner1-login')
self.assertContains(response, 'owner1-url')
self.assertContains(response, 'owner1-avatar-url')
self.assertContains(response, 'repo2-url')
self.assertContains(response, 'repo2-name')
self.assertContains(response, 'owner2-login')
self.assertContains(response, 'owner2-url')
self.assertContains(response, 'owner2-avatar-url')
def test_index_no_results(self):
github_client_mock = mock.Mock(spec=Github)
github_client_mock.search_repositories.return_value = []
with container.github_client.override(github_client_mock):
response = self.client.get(reverse('index'))
self.assertContains(response, 'Results found: 0')

View File

@ -0,0 +1,9 @@
"""URLs module."""
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
]

View File

@ -0,0 +1,34 @@
"""Views module."""
from typing import List
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from dependency_injector.wiring import Provide
from githubnavigator.containers import Container
from githubnavigator.services import SearchService
def index(
request: HttpRequest,
search_service: SearchService = Provide[Container.search_service],
default_query: str = Provide[Container.config.DEFAULT_QUERY],
default_limit: int = Provide[Container.config.DEFAULT_LIMIT.as_int()],
limit_options: List[int] = Provide[Container.config.LIMIT_OPTIONS],
) -> HttpResponse:
query = request.GET.get('query', default_query)
limit = int(request.GET.get('limit', default_limit))
repositories = search_service.search_repositories(query, limit)
return render(
request,
template_name='index.html',
context={
'query': query,
'limit': limit,
'limit_options': limit_options,
'repositories': repositories,
}
)

View File

@ -1,8 +1,10 @@
Flask Dependency Injection Example
==================================
Flask + Dependency Injector Example
===================================
Application ``githubnavigator`` is a `Flask <https://flask.palletsprojects.com/>`_ +
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ application.
This is a `Flask <https://flask.palletsprojects.com/>`_ +
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ example application.
The example application helps to search for repositories on the Github.
.. image:: screenshot.png
@ -46,8 +48,7 @@ After that visit http://127.0.0.1:5000/ in your browser.
.. note::
Github has a rate limit. When thre rate limit is exceed you will see an exception
Github has a rate limit. When the rate limit is exceed you will see an exception
``github.GithubException.RateLimitExceededException``. For unauthenticated requests, the rate
limit allows for up to 60 requests per hour. To extend the limit to 5000 requests per hour you
need to set personal access token.
@ -90,10 +91,10 @@ The output should be something like:
Name Stmts Miss Cover
----------------------------------------------------
githubnavigator/__init__.py 0 0 100%
githubnavigator/application.py 11 0 100%
githubnavigator/containers.py 13 0 100%
githubnavigator/application.py 15 0 100%
githubnavigator/containers.py 7 0 100%
githubnavigator/services.py 14 0 100%
githubnavigator/tests.py 32 0 100%
githubnavigator/views.py 7 0 100%
githubnavigator/tests.py 34 0 100%
githubnavigator/views.py 9 0 100%
----------------------------------------------------
TOTAL 77 0 100%
TOTAL 79 0 100%

View File

@ -0,0 +1,5 @@
github:
request_timeout: 10
default:
query: "Dependency Injector"
limit: 10

View File

@ -0,0 +1,23 @@
"""Application module."""
from flask import Flask
from flask_bootstrap import Bootstrap
from .containers import Container
from . import views
def create_app() -> Flask:
container = Container()
container.config.from_yaml('config.yml')
container.config.github.auth_token.from_env('GITHUB_TOKEN')
container.wire(modules=[views])
app = Flask(__name__)
app.container = container
app.add_url_rule('/', 'index', views.index)
bootstrap = Bootstrap()
bootstrap.init_app(app)
return app

View File

@ -0,0 +1,22 @@
"""Containers module."""
from dependency_injector import containers, providers
from github import Github
from . import services
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
github_client = providers.Factory(
Github,
login_or_token=config.github.auth_token,
timeout=config.github.request_timeout,
)
search_service = providers.Factory(
services.SearchService,
github_client=github_client,
)

View File

@ -0,0 +1,44 @@
"""Services module."""
from github import Github
from github.Repository import Repository
from github.Commit import Commit
class SearchService:
"""Search service performs search on Github."""
def __init__(self, github_client: Github):
self._github_client = github_client
def search_repositories(self, query, limit):
"""Search for repositories and return formatted data."""
repositories = self._github_client.search_repositories(
query=query,
**{'in': 'name'},
)
return [
self._format_repo(repository)
for repository in repositories[:limit]
]
def _format_repo(self, repository: Repository):
commits = repository.get_commits()
return {
'url': repository.html_url,
'name': repository.name,
'owner': {
'login': repository.owner.login,
'url': repository.owner.html_url,
'avatar_url': repository.owner.avatar_url,
},
'latest_commit': self._format_commit(commits[0]) if commits else {},
}
def _format_commit(self, commit: Commit):
return {
'sha': commit.sha,
'url': commit.html_url,
'message': commit.commit.message,
'author_name': commit.commit.author.name,
}

View File

@ -11,7 +11,9 @@ from .application import create_app
@pytest.fixture
def app():
return create_app()
app = create_app()
yield app
app.container.unwire()
def test_index(client, app):

View File

@ -1,14 +1,16 @@
"""Views module."""
from flask import request, render_template
from dependency_injector.wiring import Provide
from .services import SearchService
from .containers import Container
def index(
search_service: SearchService,
default_query: str,
default_limit: int,
search_service: SearchService = Provide[Container.search_service],
default_query: str = Provide[Container.config.default.query],
default_limit: int = Provide[Container.config.default.limit.as_int()],
):
query = request.args.get('query', default_query)
limit = request.args.get('limit', default_limit, int)

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 KiB

View File

@ -1,5 +0,0 @@
github:
request_timeout: 10
search:
default_query: "Dependency Injector"
default_limit: 10

View File

@ -1,20 +0,0 @@
"""Application module."""
from .containers import ApplicationContainer
def create_app():
"""Create and return Flask application."""
container = ApplicationContainer()
container.config.from_yaml('config.yml')
container.config.github.auth_token.from_env('GITHUB_TOKEN')
app = container.app()
app.container = container
bootstrap = container.bootstrap()
bootstrap.init_app(app)
app.add_url_rule('/', view_func=container.index_view.as_view())
return app

View File

@ -1,37 +0,0 @@
"""Application containers module."""
from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github
from . import views, services
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = flask.Application(Flask, __name__)
bootstrap = flask.Extension(Bootstrap)
config = providers.Configuration()
github_client = providers.Factory(
Github,
login_or_token=config.github.auth_token,
timeout=config.github.request_timeout,
)
search_service = providers.Factory(
services.SearchService,
github_client=github_client,
)
index_view = flask.View(
views.index,
search_service=search_service,
default_query=config.search.default_query,
default_limit=config.search.default_limit,
)

View File

@ -1,5 +0,0 @@
giphy:
request_timeout: 10
search:
default_query: "Dependency Injector"
default_limit: 10

Some files were not shown because too many files have changed in this diff Show More