Compare commits

..

22 Commits

Author SHA1 Message Date
Roman Mogylatov
b4890dcf80 Bump version to 4.0.0a2 2020-09-28 16:33:39 -04:00
Roman Mogylatov
d9334ef1fe Add protection for wiring only declarative container instances 2020-09-28 15:52:21 -04:00
Roman Mogylatov
cc1b6ba3e6 Add __all__ for wiring module 2020-09-28 15:39:41 -04:00
Roman Mogylatov
42b0bde8d2 Deprecate provider.delegate() method 2020-09-28 15:38:34 -04:00
Roman Mogylatov
5d4eeb648a Deprecate ext package modules and remove types module 2020-09-28 14:16:05 -04:00
Roman Mogylatov
26e490bf0a Add container.unwire() typing stub 2020-09-28 13:41:54 -04:00
Roman Mogylatov
6182b8448a
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
2020-09-27 23:10:11 -04:00
Roman Mogylatov
7f854548d6 Make flake8 happy 2020-09-26 01:17:42 -04:00
Roman Mogylatov
95db0eddc9 Implement Provide[foo.provided.bar.baz.call()] 2020-09-26 01:07:32 -04:00
Roman Mogylatov
6d92df32aa Implement wiring for Provide[foo.provider] 2020-09-26 00:31:29 -04:00
Roman Mogylatov
c20c57ae7c Update demo 2020-09-26 00:23:11 -04:00
Roman Mogylatov
7b2baeeb6f Remove not needed images 2020-09-24 20:51:45 -04:00
Roman Mogylatov
9653dfc263 Add sanic example 2020-09-23 18:29:13 -04:00
Roman Mogylatov
e37b5181e4 Rename views module to handlers in aiohttp example 2020-09-23 15:58:36 -04:00
Roman Mogylatov
a9970b63b9 Rename aiohttp example directory 2020-09-22 22:03:49 -04:00
Roman Mogylatov
170819c6ed Update flask example 2020-09-22 22:03:02 -04:00
Roman Mogylatov
4fab71c35b Update aiohttp example 2020-09-22 21:48:41 -04:00
Roman Mogylatov
170263de4d Add flake8 ignore for demo 2020-09-21 16:55:42 -04:00
Roman Mogylatov
b7efb1e3e2 Add pydocstyle ignore for demo 2020-09-21 16:51:57 -04:00
Roman Mogylatov
dd8778bf20 Updaet demo example 2020-09-21 16:46:02 -04:00
Roman Mogylatov
edd8979bf6 Bump version to 4.0 2020-09-20 21:51:48 -04:00
Roman Mogylatov
af7364e062
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
2020-09-20 21:50:25 -04:00
568 changed files with 120683 additions and 31499 deletions

7
.coveragerc Normal file
View File

@ -0,0 +1,7 @@
[run]
source = src/dependency_injector
omit = tests/unit
plugins = Cython.Coverage
[html]
directory=reports/unittests/

View File

@ -1,29 +0,0 @@
---
description: Code in Python and Cython
globs:
alwaysApply: false
---
- Follow PEP 8 rules
- When you write imports, split system, 3rd-party, and local imports with a new line
- Have two empty lines between the import block and the rest of the code
- Have an empty line (\n) at the end of every file
- If a file is supposed to be run, always add ``if __name__ == 'main'``
- Always follow a consistent pattern of using double or single quotes
- When there is a class without a docblock, leave one blank line before its members, e.g.:
```python
class Container(containers.DeclarativeContainer):
service = providers.Factory(Service)
```
- Avoid shortcuts in names unless absolutely necessary, exceptions:
```
arg
args
kwarg
kwargs
obj
cls
```
- Avoid inline comments unless absolutely necessary

View File

@ -1,7 +0,0 @@
---
description: Build and run tests
globs:
alwaysApply: false
---
- Use Makefile commands to build, test, lint and other similar operations when they are available.
- Activate virtualenv before running any commands by ``. venv/bin/actvate``

View File

@ -1,8 +0,0 @@
---
description: Run examples
globs:
alwaysApply: false
---
- When you run an example from the ``examples/`` folder, switch to the example folder and run it from there.
- If there are instructions on running the examples or its tests in readme, follow them
- Activate virtualenv before running any commands by ``. venv/bin/actvate``

View File

@ -1,12 +0,0 @@
version = 1
test_patterns = ["tests/**/test_*.py"]
exclude_patterns = ["docs/**"]
[[analyzers]]
name = "python"
enabled = true
[analyzers.meta]
runtime_version = "3.x.x"

View File

@ -1,9 +0,0 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{py,pyi,pxd,pyx}]
ij_visual_guides = 80,88

1
.github/FUNDING.yml vendored
View File

@ -1 +0,0 @@
github: rmk135

View File

@ -1,131 +0,0 @@
name: Publishing
on:
workflow_dispatch:
push:
tags:
- '*'
jobs:
tests:
name: Run tests
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.13
- run: pip install tox
- run: tox
env:
TOXENV: 3.13
linters:
name: Run linters
runs-on: ubuntu-24.04
strategy:
matrix:
toxenv: [flake8, pydocstyle, mypy, pylint]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.13
- run: pip install tox
- run: tox
env:
TOXENV: ${{ matrix.toxenv }}
build-sdist:
name: Build source tarball
needs: [tests, linters]
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.13
- run: |
python -m pip install --upgrade build
python -m build --sdist
- uses: actions/upload-artifact@v4
with:
name: cibw-sdist
path: ./dist/*
build-wheels:
name: Build wheels
needs: [tests, linters]
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-24.04, ubuntu-24.04-arm, windows-2022, macos-14]
env:
CIBW_ENABLE: pypy
CIBW_ENVIRONMENT: >-
PIP_CONFIG_SETTINGS="build_ext=-j4"
DEPENDENCY_INJECTOR_LIMITED_API="1"
CFLAGS="-g0"
steps:
- uses: actions/checkout@v3
- name: Build wheels
uses: pypa/cibuildwheel@v3.0.0
- uses: actions/upload-artifact@v4
with:
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
path: ./wheelhouse/*.whl
test-publish:
name: Upload release to TestPyPI
needs: [build-sdist, build-wheels]
runs-on: ubuntu-latest
environment: test-pypi
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: cibw-*
path: dist
merge-multiple: true
- uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
publish:
name: Upload release to PyPI
needs: [build-sdist, build-wheels, test-publish]
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: cibw-*
path: dist
merge-multiple: true
- uses: pypa/gh-action-pypi-publish@release/v1
publish-docs:
name: Publish docs
needs: [publish]
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.13
- run: pip install awscli
- run: pip install -r requirements-doc.txt
- run: pip install -e .
- run: (cd docs && make clean html)
- run: |
aws s3 sync docs/_build/html s3://python-dependency-injector-docs --delete
aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_CLOUDFRONT_DISTRIBUTION_ID }} --path "/*" > /dev/null
echo "Cache invalidation triggered"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}

View File

@ -1,67 +0,0 @@
name: Tests and linters
on: [push, pull_request, workflow_dispatch]
jobs:
test-on-different-versions:
name: Run tests
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- run: pip install tox
- run: tox
env:
DEPENDENCY_INJECTOR_LIMITED_API: 1
TOXENV: ${{ matrix.python-version }}
test-different-pydantic-versions:
name: Run tests with different pydantic versions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.12"
- run: pip install tox
- run: tox -e pydantic-v1,pydantic-v2
test-coverage:
name: Run tests with coverage
runs-on: ubuntu-latest
env:
DEPENDENCY_INJECTOR_DEBUG_MODE: 1
PIP_VERBOSE: 1
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.12
- run: pip install tox
- run: tox -vv
env:
TOXENV: coveralls
linters:
name: Run linters
runs-on: ubuntu-latest
strategy:
matrix:
toxenv: [flake8, pydocstyle, mypy, pylint]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.13
- run: pip install tox
- run: tox
env:
TOXENV: ${{ matrix.toxenv }}

21
.gitignore vendored
View File

@ -15,7 +15,6 @@ lib64/
parts/ parts/
sdist/ sdist/
var/ var/
wheelhouse/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
*.egg *.egg
@ -37,7 +36,6 @@ reports/
.cache .cache
nosetests.xml nosetests.xml
coverage.xml coverage.xml
.hypothesis/
# Translations # Translations
*.mo *.mo
@ -56,7 +54,7 @@ target/
.idea/ .idea/
# Virtualenv # Virtualenv
venv*/ venv/
# SQLite # SQLite
*.db *.db
@ -64,13 +62,10 @@ venv*/
# Vim Rope # Vim Rope
.ropeproject/ .ropeproject/
# Cython artifacts # C extensions
src/**/*.c src/dependency_injector/*.h
src/**/*.h src/dependency_injector/*.so
src/**/*.so src/dependency_injector/containers/*.h
src/**/*.html src/dependency_injector/containers/*.so
src/dependency_injector/providers/*.h
# Workspace for samples src/dependency_injector/providers/*.so
.workspace/
.vscode/

49
.pylintrc Normal file
View File

@ -0,0 +1,49 @@
[MASTER]
# Add <file or directory> to the black list. It should be a base name, not a
# path. You may set this option multiple times.
ignore=utils,tests
[MESSAGES CONTROL]
# Disable the message(s) with the given id(s).
# disable-msg=
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=5
[TYPECHECK]
ignore-mixin-members=yes
# ignored-classes=
zope=no
# generated-members=providedBy,implementedBy,rawDataReceived
[DESIGN]
# Maximum number of arguments for function / method
max-args=10
# Maximum number of locals for function / method body
max-locals=20
# Maximum number of return / yield for function / method body
max-returns=10
# Maximum number of branch for function / method body
max-branchs=10
# Maximum number of statements in function / method body
max-statements=60
# Maximum number of parents for a class (see R0901).
max-parents=10
# Maximum number of attributes for a class (see R0902).
max-attributes=30
# Minimum number of public methods for a class (see R0903).
min-public-methods=0
# Maximum number of public methods for a class (see R0904).
max-public-methods=30

101
.travis.yml Normal file
View File

@ -0,0 +1,101 @@
os: linux
dist: xenial
language: python
jobs:
include:
- python: 3.8
env: TOXENV=coveralls DEPENDENCY_INJECTOR_DEBUG_MODE=1
install:
- pip install tox
- pip install cython
- make cythonize
script: tox
- python: 3.6
env: TOXENV=pylint
install: pip install tox
script: tox
- python: 3.6
env: TOXENV=flake8
install: pip install tox
script: tox
- python: 3.6
env: TOXENV=pydocstyle
install: pip install tox
script: tox
- python: 3.6
env: TOXENV=mypy
install: pip install tox
script: tox
- python: 2.7
env: TOXENV=py27
install: pip install tox
script: tox
- python: 3.4
env: TOXENV=py34
install: pip install tox
script: tox
- python: 3.5
env: TOXENV=py35
install: pip install tox
script: tox
- python: 3.6
env: TOXENV=py36
install: pip install tox
script: tox
- python: 3.7
env: TOXENV=py37
install: pip install tox
script: tox
- python: 3.8
env: TOXENV=py38
install: pip install tox
script: tox
- python: pypy
env: TOXENV=pypy
install: pip install tox
script: tox
- python: pypy3
env: TOXENV=pypy3
install: pip install tox
script: tox
- python: 3.8
if: tag IS present
env: TWINE_USERNAME=__token__
install: pip install pip --upgrade
script: python setup.py sdist
after_success:
- python3 -m pip install twine
- python3 -m twine upload dist/*
- services: docker
if: tag IS present
env: TWINE_USERNAME=__token__
install: python3 -m pip install cibuildwheel==1.5.1
script: python3 -m cibuildwheel --output-dir wheelhouse
after_success:
- python3 -m pip install twine
- python3 -m twine upload wheelhouse/*.whl
- os: osx
if: tag IS present
language: shell
env: TWINE_USERNAME=__token__
install: python3 -m pip install cibuildwheel==1.5.1
script: python3 -m cibuildwheel --output-dir wheelhouse
after_success:
- python3 -m pip install twine
- python3 -m twine upload wheelhouse/*.whl
- os: windows
if: tag IS present
language: shell
env: TWINE_USERNAME=__token__
before_install:
- choco install python --version 3.8.0
- export PATH="/c/Python38:/c/Python38/Scripts:$PATH"
install: python -m pip install cibuildwheel==1.5.1
script: python -m cibuildwheel --output-dir wheelhouse
after_success:
- python -m pip install twine
- python -m twine upload wheelhouse/*.whl
notifications:
slack:
rooms:
secure: CdWDgKnfYW7vvvoH3nS3yg3TcNZiYLRUyEp6ukQ4rQiiuR4+ltuvyGyFJWgP8r7VVJ9yHkB0jebCKWLUMsAEt1my33B6eMDEVefovpkdh2eJjGswmm80brt0EJULpgwPOtB1U47Mwca8L5jDW4KSv9RypUFRgn8eHDoWw6LKf5g=

View File

@ -13,12 +13,3 @@ Dependency Injector Contributors
+ Bruno P. Kinoshita (kinow) + Bruno P. Kinoshita (kinow)
+ RobinsonMa (RobinsonMa) + RobinsonMa (RobinsonMa)
+ Rüdiger Busche (JarnoRFB) + Rüdiger Busche (JarnoRFB)
+ Dmitry Rassoshenko (rda-dev)
+ Fotis Koutoupas (kootoopas)
+ Shubhendra Singh Chauhan (withshubh)
+ sonthonaxrk (sonthonaxrk)
+ Ngo Thanh Loi (Leonn) (loingo95)
+ Thiago Hiromi (thiromi)
+ Felipe Rubio (krouw)
+ Anton Petrov (anton-petrov)
+ ZipFile (ZipFile)

View File

@ -1,4 +1,4 @@
Copyright (c) 2024, Roman Mogylatov Copyright (c) 2017, ETS Labs
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without

View File

@ -1,7 +1,9 @@
recursive-include src/dependency_injector *.py* *.c py.typed recursive-include src/dependency_injector *.py* *.c
recursive-include tests *.py recursive-include tests *.py
include README.rst include README.rst
include CONTRIBUTORS.rst include CONTRIBUTORS.rst
include LICENSE.rst include LICENSE.rst
include requirements.txt
include setup.py include setup.py
include tox.ini include tox.ini
include py.typed

View File

@ -1,6 +1,14 @@
VERSION := $(shell python setup.py --version) VERSION := $(shell python setup.py --version)
export COVERAGE_RCFILE := pyproject.toml CYTHON_SRC := $(shell find src/dependency_injector -name '*.pyx')
CYTHON_DIRECTIVES = -Xlanguage_level=2
ifdef DEPENDENCY_INJECTOR_DEBUG_MODE
CYTHON_DIRECTIVES += -Xprofile=True
CYTHON_DIRECTIVES += -Xlinetrace=True
endif
clean: clean:
# Clean sources # Clean sources
@ -17,28 +25,39 @@ clean:
find examples -name '*.py[co]' -delete find examples -name '*.py[co]' -delete
find examples -name '__pycache__' -delete find examples -name '__pycache__' -delete
build: clean cythonize:
# Compile C extensions # Compile Cython to C
python setup.py build_ext --inplace cython -a $(CYTHON_DIRECTIVES) $(CYTHON_SRC)
# Move all Cython html reports # Move all Cython html reports
mkdir -p reports/cython/ mkdir -p reports/cython/
find src -name '*.html' -exec mv {} reports/cython/ \; find src -name '*.html' -exec mv {} reports/cython/ \;
build: clean cythonize
# Compile C extensions
python setup.py build_ext --inplace
docs-live: docs-live:
sphinx-autobuild docs docs/_build/html sphinx-autobuild docs docs/_build/html
install: uninstall clean build install: uninstall clean cythonize
pip install -ve . pip install -ve .
uninstall: uninstall:
- pip uninstall -y -q dependency-injector 2> /dev/null - pip uninstall -y -q dependency-injector 2> /dev/null
test: test-py2: build
# Unit tests with coverage report # Unit tests with coverage report
coverage erase coverage erase
coverage run -m pytest coverage run --rcfile=./.coveragerc -m unittest2 discover -s tests/unit/ -p test_*_py2_py3.py
coverage report coverage report --rcfile=./.coveragerc
coverage html coverage html --rcfile=./.coveragerc
test-py3: build
# Unit tests with coverage report
coverage erase
coverage run --rcfile=./.coveragerc -m unittest2 discover -s tests/unit/ -p test_*py3*.py
coverage report --rcfile=./.coveragerc
coverage html --rcfile=./.coveragerc
check: check:
flake8 src/dependency_injector/ flake8 src/dependency_injector/
@ -49,9 +68,9 @@ check:
mypy tests/typing mypy tests/typing
test-publish: build test-publish: cythonize
# Create distributions # Create distributions
python -m build --sdist python setup.py sdist
# Upload distributions to PyPI # Upload distributions to PyPI
twine upload --repository testpypi dist/dependency-injector-$(VERSION)* twine upload --repository testpypi dist/dependency-injector-$(VERSION)*

View File

@ -35,10 +35,14 @@
:target: https://pypi.org/project/dependency-injector/ :target: https://pypi.org/project/dependency-injector/
:alt: Wheel :alt: Wheel
.. image:: https://img.shields.io/github/actions/workflow/status/ets-labs/python-dependency-injector/tests-and-linters.yml?branch=master .. image:: https://travis-ci.org/ets-labs/python-dependency-injector.svg?branch=master
:target: https://github.com/ets-labs/python-dependency-injector/actions :target: https://travis-ci.org/ets-labs/python-dependency-injector
:alt: Build Status :alt: Build Status
.. image:: http://readthedocs.org/projects/python-dependency-injector/badge/?version=latest
:target: http://python-dependency-injector.ets-labs.org/
:alt: Docs Status
.. image:: https://coveralls.io/repos/github/ets-labs/python-dependency-injector/badge.svg?branch=master .. image:: https://coveralls.io/repos/github/ets-labs/python-dependency-injector/badge.svg?branch=master
:target: https://coveralls.io/github/ets-labs/python-dependency-injector?branch=master :target: https://coveralls.io/github/ets-labs/python-dependency-injector?branch=master
:alt: Coverage Status :alt: Coverage Status
@ -48,91 +52,74 @@ What is ``Dependency Injector``?
``Dependency Injector`` is a dependency injection framework for Python. ``Dependency Injector`` is a dependency injection framework for Python.
It helps implement the dependency injection principle. It helps implementing the dependency injection principle.
Key features of the ``Dependency Injector``: Key features of the ``Dependency Injector``:
- **Providers**. Provides ``Factory``, ``Singleton``, ``Callable``, ``Coroutine``, ``Object``, - **Providers**. Provides ``Factory``, ``Singleton``, ``Callable``, ``Coroutine``, ``Object``,
``List``, ``Dict``, ``Configuration``, ``Resource``, ``Dependency``, and ``Selector`` providers ``List``, ``Configuration``, ``Dependency`` and ``Selector`` providers that help assembling your
that help assemble your objects. objects. See `Providers <http://python-dependency-injector.ets-labs.org/providers/index.html>`_.
See `Providers <https://python-dependency-injector.ets-labs.org/providers/index.html>`_.
- **Overriding**. Can override any provider by another provider on the fly. This helps in testing - **Overriding**. Can override any provider by another provider on the fly. This helps in testing
and configuring dev/stage environment to replace API clients with stubs etc. See and configuring dev / stage environment to replace API clients with stubs etc. See
`Provider overriding <https://python-dependency-injector.ets-labs.org/providers/overriding.html>`_. `Provider overriding <http://python-dependency-injector.ets-labs.org/providers/overriding.html>`_.
- **Configuration**. Reads configuration from ``yaml``, ``ini``, and ``json`` files, ``pydantic`` settings, - **Configuration**. Read configuration from ``yaml`` & ``ini`` files, environment variables
environment variables, and dictionaries. and dictionaries.
See `Configuration provider <https://python-dependency-injector.ets-labs.org/providers/configuration.html>`_. See `Configuration provider <http://python-dependency-injector.ets-labs.org/providers/configuration.html>`_.
- **Resources**. Helps with initialization and configuring of logging, event loop, thread
or process pool, etc. Can be used for per-function execution scope in tandem with wiring.
See `Resource provider <https://python-dependency-injector.ets-labs.org/providers/resource.html>`_.
- **Containers**. Provides declarative and dynamic containers. - **Containers**. Provides declarative and dynamic containers.
See `Containers <https://python-dependency-injector.ets-labs.org/containers/index.html>`_. See `Containers <http://python-dependency-injector.ets-labs.org/containers/index.html>`_.
- **Wiring**. Injects dependencies into functions and methods. Helps integrate with
other frameworks: Django, Flask, Aiohttp, Sanic, FastAPI, etc.
See `Wiring <https://python-dependency-injector.ets-labs.org/wiring.html>`_.
- **Asynchronous**. Supports asynchronous injections.
See `Asynchronous injections <https://python-dependency-injector.ets-labs.org/providers/async.html>`_.
- **Typing**. Provides typing stubs, ``mypy``-friendly.
See `Typing and mypy <https://python-dependency-injector.ets-labs.org/providers/typing_mypy.html>`_.
- **Performance**. Fast. Written in ``Cython``. - **Performance**. Fast. Written in ``Cython``.
- **Maturity**. Mature and production-ready. Well-tested, documented, and supported. - **Typing**. Provides typing stubs, ``mypy``-friendly.
See `Typing and mypy <http://python-dependency-injector.ets-labs.org/providers/typing_mypy.html>`_.
- **Maturity**. Mature and production-ready. Well-tested, documented and supported.
.. code-block:: python .. code-block:: python
from dependency_injector import containers, providers from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, inject from dependency_injector.wiring import Provide
class Container(containers.DeclarativeContainer): class Container(containers.DeclarativeContainer):
config = providers.Configuration() config = providers.Configuration()
api_client = providers.Singleton( api_client = providers.Singleton(
ApiClient, ApiClient,
api_key=config.api_key, api_key=config.api_key,
timeout=config.timeout, timeout=config.timeout.as_int(),
) )
service = providers.Factory( service = providers.Factory(
Service, Service,
api_client=api_client, api_client=api_client,
) )
@inject def main(service: Service = Provide[Container.service]):
def main(service: Service = Provide[Container.service]) -> None: ...
...
if __name__ == "__main__": if __name__ == '__main__':
container = Container() container = Container()
container.config.api_key.from_env("API_KEY", required=True)
container.config.timeout.from_env("TIMEOUT", as_=int, default=5)
container.wire(modules=[__name__])
main() # <-- dependency is injected automatically container.config.api_key.from_env('API_KEY')
container.config.timeout.from_env('TIMEOUT')
with container.api_client.override(mock.Mock()): container.wire(modules=[sys.modules[__name__]])
main() # <-- overridden dependency is injected automatically
When you call the ``main()`` function the ``Service`` dependency is assembled and injected automatically. main()
When you do testing, you call the ``container.api_client.override()`` method to replace the real API With the ``Dependency Injector`` you keep **application structure in one place**.
client with a mock. When you call ``main()``, the mock is injected. 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.
You can override any provider with another provider. .. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/di-map.svg
It also helps you in a re-configuring project for different environments: replace an API client
with a stub on the dev or stage.
With the ``Dependency Injector``, object assembling is consolidated in a container. Dependency injections are defined explicitly.
This makes it easier to understand and change how an 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 :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 Visit the docs to know more about the
`Dependency injection and inversion of control in Python <https://python-dependency-injector.ets-labs.org/introduction/di_in_python.html>`_. `Dependency injection and inversion of control in Python <http://python-dependency-injector.ets-labs.org/introduction/di_in_python.html>`_.
Installation Installation
------------ ------------
@ -144,67 +131,83 @@ The package is available on the `PyPi`_::
Documentation Documentation
------------- -------------
The documentation is available `here <https://python-dependency-injector.ets-labs.org/>`_. The documentation is available on the `Read The Docs <http://python-dependency-injector.ets-labs.org/>`_
Examples Examples
-------- --------
Choose one of the following: Choose one of the following:
- `Application example (single container) <https://python-dependency-injector.ets-labs.org/examples/application-single-container.html>`_ - `Application example (single container) <http://python-dependency-injector.ets-labs.org/examples/application-single-container.html>`_
- `Application example (multiple containers) <https://python-dependency-injector.ets-labs.org/examples/application-multiple-containers.html>`_ - `Application example (multiple containers) <http://python-dependency-injector.ets-labs.org/examples/application-multiple-containers.html>`_
- `Decoupled packages example (multiple containers) <https://python-dependency-injector.ets-labs.org/examples/decoupled-packages.html>`_ - `Decoupled packages example (multiple containers) <http://python-dependency-injector.ets-labs.org/examples/decoupled-packages.html>`_
- `Boto3 example <https://python-dependency-injector.ets-labs.org/examples/boto3.html>`_
- `Django example <https://python-dependency-injector.ets-labs.org/examples/django.html>`_
- `Flask example <https://python-dependency-injector.ets-labs.org/examples/flask.html>`_
- `Aiohttp example <https://python-dependency-injector.ets-labs.org/examples/aiohttp.html>`_
- `Sanic example <https://python-dependency-injector.ets-labs.org/examples/sanic.html>`_
- `FastAPI example <https://python-dependency-injector.ets-labs.org/examples/fastapi.html>`_
- `FastAPI + Redis example <https://python-dependency-injector.ets-labs.org/examples/fastapi-redis.html>`_
- `FastAPI + SQLAlchemy example <https://python-dependency-injector.ets-labs.org/examples/fastapi-sqlalchemy.html>`_
Tutorials Tutorials
--------- ---------
Choose one of the following: Choose one of the following:
- `Flask web application tutorial <https://python-dependency-injector.ets-labs.org/tutorials/flask.html>`_ - `Flask web application tutorial <http://python-dependency-injector.ets-labs.org/tutorials/flask.html>`_
- `Aiohttp REST API tutorial <https://python-dependency-injector.ets-labs.org/tutorials/aiohttp.html>`_ - `Aiohttp REST API tutorial <http://python-dependency-injector.ets-labs.org/tutorials/aiohttp.html>`_
- `Asyncio monitoring daemon tutorial <https://python-dependency-injector.ets-labs.org/tutorials/asyncio-daemon.html>`_ - `Asyncio monitoring daemon tutorial <http://python-dependency-injector.ets-labs.org/tutorials/asyncio-daemon.html>`_
- `CLI application tutorial <https://python-dependency-injector.ets-labs.org/tutorials/cli.html>`_ - `CLI application tutorial <http://python-dependency-injector.ets-labs.org/tutorials/cli.html>`_
Concept Concept
------- -------
The framework stands on the `PEP20 (The Zen of Python) <https://www.python.org/dev/peps/pep-0020/>`_ principle: ``Dependency Injector`` stands on two principles:
.. code-block:: bash - Explicit is better than implicit (PEP20).
- Do no magic to your code.
Explicit is better than implicit How is it different from the other frameworks?
You need to specify how to assemble and where to inject the dependencies explicitly. - **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.
The power of the framework is in its simplicity. ``Dependency Injector`` makes a simple contract with you:
``Dependency Injector`` is a simple tool for the powerful concept.
- 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.
Frequently asked questions Frequently asked questions
-------------------------- --------------------------
What is dependency injection? What is the dependency injection?
- dependency injection is a principle that decreases coupling and increases cohesion - dependency injection is a principle that decreases coupling and increases cohesion
Why should I do the dependency injection? Why should I do the dependency injection?
- your code becomes more flexible, testable, and clear 😎 - your code becomes more flexible, testable and clear
- you have no problems when you need to understand how it works or change it 😎
How do I start applying the dependency injection? How do I start doing the dependency injection?
- you start writing the code following the dependency injection principle - you start writing the code following the dependency injection principle
- you register all of your application components and their dependencies in the container - you register all of your application components and their dependencies in the container
- when you need a component, you specify where to inject it or get it from 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
What price do I pay and what do I get? What price do I pay and what do I get?
- you need to explicitly specify the dependencies - you need to explicitly specify the dependencies in the container
- it will be extra work in the beginning - it will be extra work in the beginning
- it will payoff as project grows - 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
Have a question? Have a question?
- Open a `Github Issue <https://github.com/ets-labs/python-dependency-injector/issues>`_ - Open a `Github Issue <https://github.com/ets-labs/python-dependency-injector/issues>`_

View File

@ -1,9 +0,0 @@
.no-border {
border: 0 !important;
box-shadow: none !important;
-webkit-box-shadow: none !important;
}
.no-border td {
border: 0px !important;
padding: 0px 10px 0px 0px !important;
}

11
docs/_static/disqus.js vendored Normal file
View File

@ -0,0 +1,11 @@
var disqus_shortname;
var disqus_identifier;
$(function() {
var disqus_thread = $("#disqus_thread");
disqus_shortname = disqus_thread.data('disqus-shortname');
disqus_identifier = disqus_thread.data('disqus-identifier');
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
});

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -0,0 +1,3 @@
.rst-content .highlight>pre, .rst-content .linenodiv>pre {
line-height: normal;
}

View File

@ -1 +0,0 @@
<iframe src="https://github.com/sponsors/rmk135/button" title="Sponsor Dependency Injector" height="32" width="114" style="border: 0; border-radius: 6px;"></iframe>

9
docs/api/aiohttpext.rst Normal file
View File

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

View File

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

9
docs/api/flaskext.rst Normal file
View File

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

View File

@ -2,11 +2,11 @@ API Documentation
================= =================
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
top-level top_level
providers providers
containers containers
wiring errors
errors aiohttpext
asgi-lifespan flaskext

View File

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

View File

@ -20,49 +20,49 @@ import alabaster
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath("..")) sys.path.insert(0, os.path.abspath('..'))
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = "1.0" #needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be # Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named "sphinx.ext.*") or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
"alabaster", 'alabaster',
"sphinx.ext.autodoc", 'sphinx.ext.autodoc',
"sphinx_disqus.disqus", 'sphinxcontrib.disqus',
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"] templates_path = ['_templates']
# The suffix(es) of source filenames. # The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string: # You can specify multiple suffix as a list of string:
# source_suffix = [".rst", ".md"] # source_suffix = ['.rst', '.md']
source_suffix = ".rst" source_suffix = '.rst'
# The encoding of source files. # The encoding of source files.
#source_encoding = "utf-8-sig" #source_encoding = 'utf-8-sig'
# The master toctree document. # The master toctree document.
master_doc = "index" master_doc = 'index'
# General information about the project. # General information about the project.
project = "Dependency Injector" project = u'Dependency Injector'
copyright = "2024, Roman Mogylatov" copyright = u'2020, ETS Labs'
author = "Roman Mogylatov" author = u'ETS Labs'
# The version info for the project you"re documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
# Getting version: # Getting version:
with open("../src/dependency_injector/__init__.py") as init_file: with open('../src/dependency_injector/__init__.py') as init_file:
version = re.search("__version__ = \"(.*?)\"", init_file.read()).group(1) version = re.search('__version__ = \'(.*?)\'', init_file.read()).group(1)
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = version release = version
@ -72,23 +72,23 @@ release = version
# #
# This is also used if you do content translation via gettext catalogs. # This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases. # Usually you set "language" from the command line for these cases.
language = "en" language = None
# There are two options for replacing |today|: either, you set today to some # There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used: # non-false value, then it is used:
#today = "" #today = ''
# Else, today_fmt is used as the format for a strftime call. # Else, today_fmt is used as the format for a strftime call.
#today_fmt = "%B %d, %Y" #today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
exclude_patterns = ["_build"] exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all # The reST default role (used for this markup: `text`) to use for all
# documents. # documents.
#default_role = None #default_role = None
# If true, "()" will be appended to :func: etc. cross-reference text. # If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True #add_function_parentheses = True
# If true, the current module name will be prepended to all description # If true, the current module name will be prepended to all description
@ -100,7 +100,7 @@ exclude_patterns = ["_build"]
#show_authors = False #show_authors = False
# The name of the Pygments (syntax highlighting) style to use. # The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx" pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting. # A list of ignored prefixes for module index sorting.
#modindex_common_prefix = [] #modindex_common_prefix = []
@ -116,8 +116,8 @@ todo_include_todos = False
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
# html_theme = "sphinx_rtd_theme" # html_theme = 'sphinx_rtd_theme'
html_theme = "alabaster" html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the
@ -141,24 +141,21 @@ html_theme_path = [alabaster.get_path()]
# The name of an image file (within the static path) to use as favicon of the # The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large. # pixels large.
html_favicon = "favicon.ico" #html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"] html_static_path = ['_static']
html_css_files = [
"custom.css",
]
# Add any extra paths that contain custom files (such as robots.txt or # Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied # .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation. # directly to the root of the documentation.
#html_extra_path = [] #html_extra_path = []
# If not "", a "Last updated on:" timestamp is inserted at every page bottom, # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format. # using the given strftime format.
#html_last_updated_fmt = "%b %d, %Y" #html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to # If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities. # typographically correct entities.
@ -192,50 +189,50 @@ html_css_files = [
# If true, an OpenSearch description file will be output, and all pages will # If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the # contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served. # base URL from which the finished HTML is served.
#html_use_opensearch = "" #html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml"). # This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None #html_file_suffix = None
# Language to be used for generating the HTML full-text search index. # Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages: # Sphinx supports the following languages:
# "da", "de", "en", "es", "fi", "fr", "hu", "it", "ja" # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# "nl", "no", "pt", "ro", "ru", "sv", "tr" # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
#html_search_language = "en" #html_search_language = 'en'
# A dictionary with options for the search language support, empty by default. # A dictionary with options for the search language support, empty by default.
# Now only "ja" uses this config value # Now only 'ja' uses this config value
#html_search_options = {"type": "default"} #html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that # The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used. # implements a search results scorer. If empty, the default will be used.
#html_search_scorer = "scorer.js" #html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = "dependency_injectordoc" htmlhelp_basename = 'dependency_injectordoc'
# -- Options for LaTeX output --------------------------------------------- # -- Options for LaTeX output ---------------------------------------------
latex_elements = { latex_elements = {
# The paper size ("letterpaper" or "a4paper"). # The paper size ('letterpaper' or 'a4paper').
#"papersize": "letterpaper", #'papersize': 'letterpaper',
# The font size ("10pt", "11pt" or "12pt"). # The font size ('10pt', '11pt' or '12pt').
#"pointsize": "10pt", #'pointsize': '10pt',
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
#"preamble": "", #'preamble': '',
# Latex figure (float) alignment # Latex figure (float) alignment
#"figure_align": "htbp", #'figure_align': 'htbp',
} }
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [
(master_doc, "dependency_injector.tex", u"Dependency Injector Documentation", (master_doc, 'dependency_injector.tex', u'Dependency Injector Documentation',
u"Roman Mogylatov", "manual"), u'ETS Labs', 'manual'),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
@ -264,7 +261,7 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [
(master_doc, "Dependency Injector", u"Dependency Injector Documentation", (master_doc, 'Dependency Injector', u'Dependency Injector Documentation',
[author], 1) [author], 1)
] ]
@ -278,9 +275,9 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
(master_doc, "Dependency Injector", u"Dependency Injector Documentation", (master_doc, 'Dependency Injector', u'Dependency Injector Documentation',
author, "Dependency Injector", "Dependency injection microframework for Python", author, 'Dependency Injector', 'Dependency injection microframework for Python',
"Miscellaneous"), 'Miscellaneous'),
] ]
# Documents to append as an appendix to all manuals. # Documents to append as an appendix to all manuals.
@ -289,25 +286,23 @@ texinfo_documents = [
# If false, no module index is generated. # If false, no module index is generated.
#texinfo_domain_indices = True #texinfo_domain_indices = True
# How to display URL addresses: "footnote", "no", or "inline". # How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = "footnote" #texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node"s menu. # If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False #texinfo_no_detailmenu = False
autodoc_member_order = "bysource" autodoc_member_order = 'bysource'
disqus_shortname = "python-dependency-injector" disqus_shortname = 'python-dependency-injector'
html_theme_options = { html_theme_options = {
"github_user": "ets-labs", 'github_user': 'ets-labs',
"github_repo": "python-dependency-injector", 'github_repo': 'python-dependency-injector',
"github_type": "star", 'github_type': 'star',
"github_button": True, 'github_button': True,
"github_banner": True, 'github_banner': True,
"logo": "logo.svg", 'logo': 'logo.svg',
"description": "Dependency injection framework for Python by Roman Mogylatov", 'description': 'Dependency injection framework for Python',
"code_font_size": "10pt", 'code_font_size': '10pt',
"analytics_id": "UA-67012059-1",
"donate_url": "https://github.com/sponsors/rmk135",
} }

View File

@ -1,18 +0,0 @@
.. _check-container-dependencies:
Check container dependencies
----------------------------
To check container dependencies use method ``.check_dependencies()``.
.. literalinclude:: ../../examples/containers/check_dependencies.py
:language: python
:lines: 3-
:emphasize-lines: 12
Method ``.check_dependencies()`` raises an error if container has any undefined dependencies.
If all dependencies are provided or have defaults, no error is raised.
See also: :ref:`dependency-provider`.
.. disqus::

View File

@ -1,23 +0,0 @@
Container copying
-----------------
You can create declarative container copies using ``@containers.copy()`` decorator.
.. literalinclude:: ../../examples/containers/declarative_copy_decorator1.py
:language: python
:lines: 3-
:emphasize-lines: 18-22
Decorator ``@containers.copy()`` copies providers from source container to destination container.
Destination container provider will replace source provider, if names match.
Decorator ``@containers.copy()`` helps you when you create derived declarative containers
from the base one. Base container often keeps default dependencies while derived containers define
overriding providers. Without ``@containers.copy()`` decorator, overridden providers are available
in the derived container, but base class dependencies continue to be bound to the base class providers.
.. literalinclude:: ../../examples/containers/declarative_copy_decorator2.py
:language: python
:lines: 11-
.. disqus::

View File

@ -34,39 +34,10 @@ Injections in the declarative container are done the usual way:
:language: python :language: python
:lines: 3- :lines: 3-
You can override container providers while creating a container instance: You can override the container providers when you create the container instance:
.. literalinclude:: ../../examples/containers/declarative_override_providers.py .. literalinclude:: ../../examples/containers/declarative_override_providers.py
:language: python :language: python
:lines: 3- :lines: 3-
:emphasize-lines: 13
Alternatively, you can call ``container.override_providers()`` method when the container instance
already exists:
.. code-block:: python
:emphasize-lines: 3
container = Container()
container.override_providers(foo=mock.Mock(Foo), bar=mock.Mock(Bar))
assert isinstance(container.foo(), mock.Mock)
assert isinstance(container.bar(), mock.Mock)
You can also use ``container.override_providers()`` with a context manager to reset
provided overriding after the context is closed:
.. code-block:: python
:emphasize-lines: 3
container = Container()
with container.override_providers(foo=mock.Mock(Foo), bar=mock.Mock(Bar)):
assert isinstance(container.foo(), mock.Mock)
assert isinstance(container.bar(), mock.Mock)
assert isinstance(container.foo(), Foo)
assert isinstance(container.bar(), Bar)
.. disqus:: .. disqus::

View File

@ -23,7 +23,3 @@ Containers module API docs - :py:mod:`dependency_injector.containers`.
dynamic dynamic
specialization specialization
overriding overriding
copying
reset_singletons
check_dependencies
traversal

View File

@ -21,20 +21,4 @@ The container also has:
:py:class:`DynamicContainer` has the same functionality. :py:class:`DynamicContainer` has the same functionality.
Another possible way to override container providers on declarative level is
``@containers.override()`` decorator:
.. literalinclude:: ../../examples/containers/declarative_override_decorator.py
:language: python
:lines: 3-
:emphasize-lines: 12-16
Decorator ``@containers.override()`` takes a container for overriding as an argument.
This container providers will be overridden by the providers with the same names from
the decorated container.
It helps to change the behaviour of application by importing extension modules but not a code change.
Imported module can override providers in main container. While the code uses main container as
before, the overridden providers provide components defined in the extension module.
.. disqus:: .. disqus::

View File

@ -1,31 +0,0 @@
.. _reset-container-singletons:
Reset container singletons
--------------------------
To reset all container singletons use method ``.reset_singletons()``.
.. literalinclude:: ../../examples/containers/reset_singletons.py
:language: python
:lines: 3-
:emphasize-lines: 16
Method ``.reset_singletons()`` also resets singletons in sub-containers: ``providers.Container`` and
``providers.DependenciesContainer.``
.. literalinclude:: ../../examples/containers/reset_singletons_subcontainers.py
:language: python
:lines: 3-
:emphasize-lines: 21
You can use ``.reset_singletons()`` method with a context manager. Singletons will be reset on
both entering and exiting a context.
.. literalinclude:: ../../examples/containers/reset_singletons_with.py
:language: python
:lines: 3-
:emphasize-lines: 14-15
See also: :ref:`singleton-provider`.
.. disqus::

View File

@ -1,33 +0,0 @@
Container providers traversal
-----------------------------
To traverse container providers use method ``.traverse()``.
.. literalinclude:: ../../examples/containers/traverse.py
:language: python
:lines: 3-
:emphasize-lines: 38
Method ``.traverse()`` returns a generator. Traversal generator visits all container providers.
This includes nested providers even if they are not present on the root level of the container.
Traversal generator guarantees that each container provider will be visited only once.
It can traverse cyclic provider graphs.
Traversal generator does not guarantee traversal order.
You can use ``types=[...]`` argument to filter providers. Traversal generator will only return
providers matching specified types.
.. code-block:: python
:emphasize-lines: 3
container = Container()
for provider in container.traverse(types=[providers.Resource]):
print(provider)
# <dependency_injector.providers.Resource(<function init_database at 0x10bd2cb80>) at 0x10d346b40>
# <dependency_injector.providers.Resource(<function init_cache at 0x10be373a0>) at 0x10d346bc0>
.. disqus::

View File

@ -19,7 +19,7 @@ additional arguments.
) )
if __name__ == "__main__": if __name__ == '__main__':
instance = concrete_factory() instance = concrete_factory()
# Same as: # instance = SomeClass(base_argument=1, extra_argument=2) # Same as: # instance = SomeClass(base_argument=1, extra_argument=2)
@ -43,21 +43,21 @@ Passing of the arguments works the same way like for any other :ref:`factory-pro
providers.Factory(dict, arg1=1), providers.Factory(dict, arg1=1),
arg2=2, arg2=2,
) )
print(chained_dict_factory()) # prints: {"arg1": 1, "arg2": 2} print(chained_dict_factory()) # prints: {'arg1': 1, 'arg2': 2}
# 2. Keyword arguments of upper level factory have priority # 2. Keyword arguments of upper level factory have priority
chained_dict_factory = providers.Factory( chained_dict_factory = providers.Factory(
providers.Factory(dict, arg1=1), providers.Factory(dict, arg1=1),
arg1=2, arg1=2,
) )
print(chained_dict_factory()) # prints: {"arg1": 2} print(chained_dict_factory()) # prints: {'arg1': 2}
# 3. Keyword arguments provided from context have the most priority # 3. Keyword arguments provided from context have the most priority
chained_dict_factory = providers.Factory( chained_dict_factory = providers.Factory(
providers.Factory(dict, arg1=1), providers.Factory(dict, arg1=1),
arg1=2, arg1=2,
) )
print(chained_dict_factory(arg1=3)) # prints: {"arg1": 3} print(chained_dict_factory(arg1=3)) # prints: {'arg1': 3}
Credits Credits

View File

@ -20,7 +20,7 @@ additional arguments.
) )
if __name__ == "__main__": if __name__ == '__main__':
instance = concrete_factory() instance = concrete_factory()
# Same as: # instance = SomeClass(base_argument=1, extra_argument=2) # Same as: # instance = SomeClass(base_argument=1, extra_argument=2)
@ -46,7 +46,7 @@ Passing of the arguments works the same way like for any other :ref:`factory-pro
arg1=1, arg1=1,
) )
dict_factory = factory_of_dict_factories(arg2=2) dict_factory = factory_of_dict_factories(arg2=2)
print(dict_factory()) # prints: {"arg1": 1, "arg2": 2} print(dict_factory()) # prints: {'arg1': 1, 'arg2': 2}
# 2. Keyword arguments of upper level factory have priority # 2. Keyword arguments of upper level factory have priority
factory_of_dict_factories = providers.Factory( factory_of_dict_factories = providers.Factory(
@ -55,7 +55,7 @@ Passing of the arguments works the same way like for any other :ref:`factory-pro
arg1=1, arg1=1,
) )
dict_factory = factory_of_dict_factories(arg1=2) dict_factory = factory_of_dict_factories(arg1=2)
print(dict_factory()) # prints: {"arg1": 2} print(dict_factory()) # prints: {'arg1': 2}
# 3. Keyword arguments provided from context have the most priority # 3. Keyword arguments provided from context have the most priority
factory_of_dict_factories = providers.Factory( factory_of_dict_factories = providers.Factory(
@ -64,7 +64,7 @@ Passing of the arguments works the same way like for any other :ref:`factory-pro
arg1=1, arg1=1,
) )
dict_factory = factory_of_dict_factories(arg1=2) dict_factory = factory_of_dict_factories(arg1=2)
print(dict_factory(arg1=3)) # prints: {"arg1": 3} print(dict_factory(arg1=3)) # prints: {'arg1': 3}
Credits Credits
------- -------

View File

@ -1,83 +0,0 @@
.. _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>`_.
.. include:: ../sponsor.rst
.. disqus::

View File

@ -84,6 +84,4 @@ Run the application
You can find the source code and instructions for running on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/application-multiple-containers>`_. You can find the source code and instructions for running on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/application-multiple-containers>`_.
.. include:: ../sponsor.rst
.. disqus:: .. disqus::

View File

@ -90,6 +90,4 @@ Run the application
You can find the source code and instructions for running on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/application-single-container>`_. You can find the source code and instructions for running on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/application-single-container>`_.
.. include:: ../sponsor.rst
.. disqus:: .. disqus::

View File

@ -1,22 +0,0 @@
.. _boto3-example:
Boto3 example
=============
.. meta::
:keywords: Python,Dependency Injection,Boto3,AWS,Amazon Web Services,S3,SQS,Rout53,EC2,Lambda,Example
:description: This example demonstrates a usage of Boto3 AWS client and Dependency Injector.
This example shows how to use ``Dependency Injector`` with `Boto3 <https://github.com/boto/boto3>`_.
The source code is available on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/boto3-session>`_.
Listing of ``boto3_session_example.py``:
.. literalinclude:: ../../examples/miniapps/boto3-session/boto3_session_example.py
:language: python
.. include:: ../sponsor.rst
.. disqus::

View File

@ -129,6 +129,4 @@ Run the application
You can find the source code and instructions for running on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/decoupled-packages>`_. You can find the source code and instructions for running on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/decoupled-packages>`_.
.. include:: ../sponsor.rst
.. disqus:: .. disqus::

View File

@ -1,99 +0,0 @@
.. _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: 12
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>`_.
.. include:: ../sponsor.rst
.. disqus::

View File

@ -1,100 +0,0 @@
.. _fastapi-redis-example:
FastAPI + Redis example
=======================
.. meta::
:keywords: Python,Dependency Injection,FastAPI,Redis,Example
:description: This example demonstrates a usage of the FastAPI, Redis, and Dependency Injector.
This example shows how to use ``Dependency Injector`` with `FastAPI <https://fastapi.tiangolo.com/>`_ and
`Redis <https://redis.io/>`_.
The source code is available on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/fastapi-redis>`_.
See also:
- Provider :ref:`async-injections`
- Resource provider :ref:`resource-async-initializers`
- Wiring :ref:`async-injections-wiring`
Application structure
---------------------
Application has next structure:
.. code-block:: bash
./
├── fastapiredis/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── redis.py
│ ├── services.py
│ └── tests.py
├── docker-compose.yml
├── Dockerfile
└── requirements.txt
Redis
-----
Module ``redis`` defines Redis connection pool initialization and shutdown. See ``fastapiredis/redis.py``:
.. literalinclude:: ../../examples/miniapps/fastapi-redis/fastapiredis/redis.py
:language: python
Service
-------
Module ``services`` contains example service. Service has a dependency on Redis connection pool.
It uses it for getting and setting a key asynchronously. Real life service will do something more meaningful.
See ``fastapiredis/services.py``:
.. literalinclude:: ../../examples/miniapps/fastapi-redis/fastapiredis/services.py
:language: python
Container
---------
Declarative container wires example service with Redis connection pool. See ``fastapiredis/containers.py``:
.. literalinclude:: ../../examples/miniapps/fastapi-redis/fastapiredis/containers.py
:language: python
Application
-----------
Module ``application`` creates ``FastAPI`` app, setup endpoint, and init container.
Endpoint ``index`` has a dependency on example service. The dependency is injected using :ref:`wiring` feature.
Listing of ``fastapiredis/application.py``:
.. literalinclude:: ../../examples/miniapps/fastapi-redis/fastapiredis/application.py
:language: python
Tests
-----
Tests use :ref:`provider-overriding` feature to replace example service with a mock. See ``fastapiredis/tests.py``:
.. literalinclude:: ../../examples/miniapps/fastapi-redis/fastapiredis/tests.py
:language: python
:emphasize-lines: 24
Sources
-------
The source code is available on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/fastapi-redis>`_.
See also:
- Provider :ref:`async-injections`
- Resource provider :ref:`resource-async-initializers`
- Wiring :ref:`async-injections-wiring`
.. include:: ../sponsor.rst
.. disqus::

View File

@ -1,121 +0,0 @@
.. _fastapi-sqlalchemy-example:
FastAPI + SQLAlchemy example
============================
.. meta::
:keywords: Python,Dependency Injection,FastAPI,SQLAlchemy,Example
:description: This example demonstrates a usage of the FastAPI, SQLAlchemy, and Dependency Injector.
This example shows how to use ``Dependency Injector`` with `FastAPI <https://fastapi.tiangolo.com/>`_ and
`SQLAlchemy <https://www.sqlalchemy.org/>`_.
The source code is available on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/fastapi-sqlalchemy>`_.
Thanks to `@ShvetsovYura <https://github.com/ShvetsovYura>`_ for providing initial example:
`FastAPI_DI_SqlAlchemy <https://github.com/ShvetsovYura/FastAPI_DI_SqlAlchemy>`_.
Application structure
---------------------
Application has next structure:
.. code-block:: bash
./
├── webapp/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── database.py
│ ├── endpoints.py
│ ├── models.py
│ ├── repositories.py
│ ├── services.py
│ └── tests.py
├── config.yml
├── docker-compose.yml
├── Dockerfile
└── requirements.txt
Application factory
-------------------
Application factory creates container, wires it with the ``endpoints`` module, creates
``FastAPI`` app, and setup routes.
Application factory also creates database if it does not exist.
Listing of ``webapp/application.py``:
.. literalinclude:: ../../examples/miniapps/fastapi-sqlalchemy/webapp/application.py
:language: python
Endpoints
---------
Module ``endpoints`` contains example endpoints. Endpoints have a dependency on user service.
User service is injected using :ref:`wiring` feature. See ``webapp/endpoints.py``:
.. literalinclude:: ../../examples/miniapps/fastapi-sqlalchemy/webapp/endpoints.py
:language: python
Container
---------
Declarative container wires example user service, user repository, and utility database class.
See ``webapp/containers.py``:
.. literalinclude:: ../../examples/miniapps/fastapi-sqlalchemy/webapp/containers.py
:language: python
Services
--------
Module ``services`` contains example user service. See ``webapp/services.py``:
.. literalinclude:: ../../examples/miniapps/fastapi-sqlalchemy/webapp/services.py
:language: python
Repositories
------------
Module ``repositories`` contains example user repository. See ``webapp/repositories.py``:
.. literalinclude:: ../../examples/miniapps/fastapi-sqlalchemy/webapp/repositories.py
:language: python
Models
------
Module ``models`` contains example SQLAlchemy user model. See ``webapp/models.py``:
.. literalinclude:: ../../examples/miniapps/fastapi-sqlalchemy/webapp/models.py
:language: python
Database
--------
Module ``database`` defines declarative base and utility class with engine and session factory.
See ``webapp/database.py``:
.. literalinclude:: ../../examples/miniapps/fastapi-sqlalchemy/webapp/database.py
:language: python
Tests
-----
Tests use :ref:`provider-overriding` feature to replace repository with a mock. See ``webapp/tests.py``:
.. literalinclude:: ../../examples/miniapps/fastapi-sqlalchemy/webapp/tests.py
:language: python
:emphasize-lines: 25, 45, 58, 74, 86, 97
Sources
-------
The source code is available on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/fastapi-sqlalchemy>`_.
.. include:: ../sponsor.rst
.. disqus::

View File

@ -1,81 +0,0 @@
.. _fastapi-example:
FastAPI example
===============
.. meta::
:keywords: Python,Dependency Injection,FastAPI,Example
:description: This example demonstrates a usage of the FastAPI and Dependency Injector.
This example shows how to use ``Dependency Injector`` with `FastAPI <https://fastapi.tiangolo.com/>`_.
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/fastapi>`_.
Application structure
---------------------
Application has next structure:
.. code-block:: bash
./
├── giphynavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── endpoints.py
│ ├── giphy.py
│ ├── services.py
│ └── tests.py
├── config.yml
└── requirements.txt
Container
---------
Declarative container is defined in ``giphynavigator/containers.py``:
.. literalinclude:: ../../examples/miniapps/fastapi/giphynavigator/containers.py
:language: python
Endpoints
---------
Endpoint has a dependency on search service. There are also some config options that are used as default values.
The dependencies are injected using :ref:`wiring` feature.
Listing of ``giphynavigator/endpoints.py``:
.. literalinclude:: ../../examples/miniapps/fastapi/giphynavigator/endpoints.py
:language: python
Application factory
-------------------
Application factory creates container, wires it with the ``endpoints`` module, creates
``FastAPI`` app, and setup routes.
Listing of ``giphynavigator/application.py``:
.. literalinclude:: ../../examples/miniapps/fastapi/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/fastapi/giphynavigator/tests.py
:language: python
:emphasize-lines: 29,57,72
Sources
-------
Explore the sources on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/fastapi>`_.
.. include:: ../sponsor.rst
.. disqus::

View File

@ -1,48 +0,0 @@
.. _fastdepends-example:
FastDepends example
===================
.. meta::
:keywords: Python,Dependency Injection,FastDepends,Example
:description: This example demonstrates a usage of the FastDepends and Dependency Injector.
This example demonstrates how to use ``Dependency Injector`` with `FastDepends <https://github.com/Lancetnik/FastDepends>`_, a lightweight dependency injection framework inspired by FastAPI's dependency system, but without the web framework components.
Basic Usage
-----------
The integration between FastDepends and Dependency Injector is straightforward. Simply use Dependency Injector's ``Provide`` marker within FastDepends' ``Depends`` function:
.. code-block:: python
import sys
from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide
from fast_depends import Depends
class CoefficientService:
@staticmethod
def get_coefficient() -> float:
return 1.2
class Container(containers.DeclarativeContainer):
service = providers.Factory(CoefficientService)
@inject
def apply_coefficient(
a: int,
coefficient_provider: CoefficientService = Depends(Provide[Container.service]),
) -> float:
return a * coefficient_provider.get_coefficient()
container = Container()
container.wire(modules=[sys.modules[__name__]])
apply_coefficient(100) == 120.0

View File

@ -1,91 +0,0 @@
.. _flask-blueprints-example:
Flask blueprints example
========================
.. meta::
:keywords: Python,Dependency Injection,Flask,Blueprints,Example
:description: This example demonstrates a usage of the Flask Blueprints and Dependency Injector.
This example shows how to use ``Dependency Injector`` with `Flask <https://flask.palletsprojects.com/en/1.1.x/>`_
blueprints.
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-blueprints>`_.
Application structure
---------------------
Application has next structure:
.. code-block:: bash
./
├── githubnavigator/
│ ├── blueprints
│ │ ├── __init__.py
│ │ └── example.py
│ ├── templates
│ │ ├── base.html
│ │ └── index.py
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── services.py
│ └── tests.py
├── config.yml
└── requirements.txt
Container
---------
Declarative container is defined in ``githubnavigator/containers.py``:
.. literalinclude:: ../../examples/miniapps/flask-blueprints/githubnavigator/containers.py
:language: python
Blueprints
----------
Blueprint's view has dependencies on search service and some config options. The dependencies are injected
using :ref:`wiring` feature.
Listing of ``githubnavigator/blueprints/example.py``:
.. literalinclude:: ../../examples/miniapps/flask-blueprints/githubnavigator/blueprints/example.py
:language: python
Application factory
-------------------
Application factory creates container, wires it with the blueprints, creates
``Flask`` app, and setup routes.
Listing of ``githubnavigator/application.py``:
.. literalinclude:: ../../examples/miniapps/flask-blueprints/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-blueprints/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-blueprints>`_.
.. include:: ../sponsor.rst
.. disqus::

View File

@ -1,89 +0,0 @@
.. _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>`_.
.. include:: ../sponsor.rst
.. disqus::

Binary file not shown.

Before

Width:  |  Height:  |  Size: 382 KiB

View File

@ -13,15 +13,5 @@ Explore the examples to see the ``Dependency Injector`` in action.
application-single-container application-single-container
application-multiple-containers application-multiple-containers
decoupled-packages decoupled-packages
boto3
django
flask
flask-blueprints
aiohttp
sanic
fastapi
fastapi-redis
fastapi-sqlalchemy
fastdepends
.. disqus:: .. disqus::

View File

@ -1,82 +0,0 @@
.. _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: 34,61,75
Sources
-------
Explore the sources on the `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/sanic>`_.
.. include:: ../sponsor.rst
.. disqus::

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -9,11 +9,11 @@ Dependency Injector --- Dependency injection framework for Python
:description: Dependency Injector is a dependency injection framework :description: Dependency Injector is a dependency injection framework
for Python. It helps to maintain you application structure. for Python. It helps to maintain you application structure.
It was designed to be unified, developer-friendly It was designed to be unified, developer-friendly
tool that helps to implement dependency injection design tool that helps to implement dependency injection design
pattern in formal, pretty, Pythonic way. Dependency Injector pattern in formal, pretty, Pythonic way. Dependency Injector
provides implementations of such popular design patterns provides implementations of such popular design patterns
like IoC container, Factory and Singleton. Dependency like IoC container, Factory and Singleton. Dependency
Injector providers are implemented as C extension types Injector providers are implemented as C extension types
using Cython. using Cython.
.. _index: .. _index:
@ -34,15 +34,15 @@ Dependency Injector --- Dependency injection framework for Python
:target: https://pypi.org/project/dependency-injector/ :target: https://pypi.org/project/dependency-injector/
:alt: Supported Python implementations :alt: Supported Python implementations
.. image:: https://static.pepy.tech/badge/dependency-injector .. image:: https://pepy.tech/badge/dependency-injector
:target: https://pepy.tech/project/dependency-injector :target: https://pepy.tech/project/dependency-injector
:alt: Downloads :alt: Downloads
.. image:: https://static.pepy.tech/badge/dependency-injector/month .. image:: https://pepy.tech/badge/dependency-injector/month
:target: https://pepy.tech/project/dependency-injector :target: https://pepy.tech/project/dependency-injector
:alt: Downloads :alt: Downloads
.. image:: https://static.pepy.tech/badge/dependency-injector/week .. image:: https://pepy.tech/badge/dependency-injector/week
:target: https://pepy.tech/project/dependency-injector :target: https://pepy.tech/project/dependency-injector
:alt: Downloads :alt: Downloads
@ -50,10 +50,14 @@ Dependency Injector --- Dependency injection framework for Python
:target: https://pypi.org/project/dependency-injector/ :target: https://pypi.org/project/dependency-injector/
:alt: Wheel :alt: Wheel
.. image:: https://img.shields.io/github/actions/workflow/status/ets-labs/python-dependency-injector/tests-and-linters.yml?branch=master .. image:: https://travis-ci.org/ets-labs/python-dependency-injector.svg?branch=master
:target: https://github.com/ets-labs/python-dependency-injector/actions :target: https://travis-ci.org/ets-labs/python-dependency-injector
:alt: Build Status :alt: Build Status
.. image:: http://readthedocs.org/projects/python-dependency-injector/badge/?version=latest
:target: http://python-dependency-injector.ets-labs.org/
:alt: Docs Status
.. image:: https://coveralls.io/repos/github/ets-labs/python-dependency-injector/badge.svg?branch=master .. image:: https://coveralls.io/repos/github/ets-labs/python-dependency-injector/badge.svg?branch=master
:target: https://coveralls.io/github/ets-labs/python-dependency-injector?branch=master :target: https://coveralls.io/github/ets-labs/python-dependency-injector?branch=master
:alt: Coverage Status :alt: Coverage Status
@ -65,69 +69,64 @@ It helps implementing the dependency injection principle.
Key features of the ``Dependency Injector``: Key features of the ``Dependency Injector``:
- **Providers**. Provides ``Factory``, ``Singleton``, ``Callable``, ``Coroutine``, ``Object``, - **Providers**. Provides ``Factory``, ``Singleton``, ``Callable``, ``Coroutine``, ``Object``,
``List``, ``Dict``, ``Configuration``, ``Resource``, ``Dependency``, and ``Selector`` providers ``List``, ``Configuration``, ``Dependency`` and ``Selector`` providers that help assembling your
that help assemble your objects. See :ref:`providers`. objects. See :ref:`providers`.
- **Overriding**. Can override any provider by another provider on the fly. This helps in testing - **Overriding**. Can override any provider by another provider on the fly. This helps in testing
and configuring dev/stage environment to replace API clients with stubs etc. See and configuring dev / stage environment to replace API clients with stubs etc. See
:ref:`provider-overriding`. :ref:`provider-overriding`.
- **Configuration**. Reads configuration from ``yaml``, ``ini``, and ``json`` files, ``pydantic`` settings, - **Configuration**. Read configuration from ``yaml`` & ``ini`` files, environment variables
environment variables, and dictionaries. See :ref:`configuration-provider`. and dictionaries. See :ref:`configuration-provider`.
- **Resources**. Helps with initialization and configuring of logging, event loop, thread
or process pool, etc. Can be used for per-function execution scope in tandem with wiring.
See :ref:`resource-provider`.
- **Containers**. Provides declarative and dynamic containers. See :ref:`containers`. - **Containers**. Provides declarative and dynamic containers. See :ref:`containers`.
- **Wiring**. Injects dependencies into functions and methods. Helps integrate with
other frameworks: Django, Flask, Aiohttp, Sanic, FastAPI, etc. See :ref:`wiring`.
- **Asynchronous**. Supports asynchronous injections. See :ref:`async-injections`.
- **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`.
- **Performance**. Fast. Written in ``Cython``. - **Performance**. Fast. Written in ``Cython``.
- **Maturity**. Mature and production-ready. Well-tested, documented, and supported. - **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`.
- **Maturity**. Mature and production-ready. Well-tested, documented and supported.
.. code-block:: python .. code-block:: python
from dependency_injector import containers, providers from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, inject from dependency_injector.wiring import Provide
class Container(containers.DeclarativeContainer): class Container(containers.DeclarativeContainer):
config = providers.Configuration() config = providers.Configuration()
api_client = providers.Singleton( api_client = providers.Singleton(
ApiClient, ApiClient,
api_key=config.api_key, api_key=config.api_key,
timeout=config.timeout, timeout=config.timeout.as_int(),
) )
service = providers.Factory( service = providers.Factory(
Service, Service,
api_client=api_client, api_client=api_client,
) )
@inject def main(service: Service = Provide[Container.service]):
def main(service: Service = Provide[Container.service]) -> None: ...
...
if __name__ == "__main__": if __name__ == '__main__':
container = Container() container = Container()
container.config.api_key.from_env("API_KEY", required=True)
container.config.timeout.from_env("TIMEOUT", as_=int, default=5)
container.wire(modules=[__name__])
main() # <-- dependency is injected automatically container.config.api_key.from_env('API_KEY')
container.config.timeout.from_env('TIMEOUT')
with container.api_client.override(mock.Mock()): container.wire(modules=[sys.modules[__name__]])
main() # <-- overridden dependency is injected automatically
With the ``Dependency Injector``, object assembling is consolidated in the container. main()
Dependency injections are defined explicitly.
This makes it easier to understand and change how the application works.
.. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/di-readme.svg 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.
.. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/di-map.svg
:target: https://github.com/ets-labs/python-dependency-injector :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``. Explore the documentation to know more about the ``Dependency Injector``.
.. _contents: .. _contents:
@ -135,16 +134,15 @@ Explore the documentation to know more about the ``Dependency Injector``.
Contents Contents
-------- --------
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
introduction/index introduction/index
examples/index examples/index
tutorials/index tutorials/index
providers/index providers/index
containers/index containers/index
wiring examples-other/index
examples-other/index api/index
api/index main/feedback
main/feedback main/changelog
main/changelog

View File

@ -9,27 +9,27 @@ Dependency injection and inversion of control in Python
Dependency Injector, its container, Factory, Singleton and Configuration Dependency Injector, its container, Factory, Singleton and Configuration
providers. The example show how to use Dependency Injector providers overriding providers. The example show how to use Dependency Injector providers overriding
feature for testing or configuring project in different environments and explains feature for testing or configuring project in different environments and explains
why it's better than monkey-patching. why it's better then monkey-patching.
Originally dependency injection pattern got popular in languages with static typing like Java. Originally dependency injection pattern got popular in the languages with a static typing,
Dependency injection is a principle that helps to achieve an inversion of control. A like Java. Dependency injection is a principle that helps to achieve an inversion of control.
dependency injection framework can significantly improve the flexibility of a language Dependency injection framework can significantly improve a flexibility of the language
with static typing. Implementation of a dependency injection framework for a language with a static typing. Implementation of a dependency injection framework for a language
with static typing is not something that one can do quickly. It will be a quite complex thing with a static typing is not something that one can do quickly. It will be a quite complex thing
to be done well. And will take time. to be done well. And will take time.
Python is an interpreted language with dynamic typing. There is an opinion that dependency Python is an interpreted language with a dynamic typing. There is an opinion that dependency
injection doesn't work for it as well as it does for Java. A lot of the flexibility is already injection doesn't work for it as well as it does for Java. A lot of the flexibility is already
built-in. Also, there is an opinion that a dependency injection framework is something that built in. Also there is an opinion that a dependency injection framework is something that
Python developer rarely needs. Python developers say that dependency injection can be implemented Python developer rarely needs. Python developers say that dependency injection can be implemented
easily using language fundamentals. easily using language fundamentals.
This page describes the advantages of applying dependency injection in Python. It This page describes the advantages of the dependency injection usage in Python. It
contains Python examples that show how to implement dependency injection. It demonstrates the usage contains Python examples that show how to implement dependency injection. It demonstrates a usage
of the ``Dependency Injector`` framework, its container, ``Factory``, ``Singleton``, of the dependency injection framework ``Dependency Injector``, its container, ``Factory``,
and ``Configuration`` providers. The example shows how to use providers' overriding feature ``Singleton`` and ``Configuration`` providers. The example shows how to use ``Dependency Injector``
of ``Dependency Injector`` for testing or re-configuring a project in different environments and providers overriding feature for testing or configuring project in different environments and
explains why it's better than monkey-patching. explains why it's better then monkey-patching.
What is dependency injection? What is dependency injection?
----------------------------- -----------------------------
@ -44,14 +44,14 @@ What is coupling and cohesion?
Coupling and cohesion are about how tough the components are tied. Coupling and cohesion are about how tough the components are tied.
- **High coupling**. If the coupling is high it's like using superglue or welding. No easy way - **High coupling**. If the coupling is high it's like using a superglue or welding. No easy way
to disassemble. to disassemble.
- **High cohesion**. High cohesion is like using screws. Quite easy to disassemble and - **High cohesion**. High cohesion is like using the screws. Very easy to disassemble and
re-assemble in a different way. It is an opposite to high coupling. assemble back or assemble a different way. It is an opposite to high coupling.
Cohesion often correlates with coupling. Higher cohesion usually leads to lower coupling and vice versa. When the cohesion is high the coupling is low.
Low coupling brings flexibility. Your code becomes easier to change and test. Low coupling brings a flexibility. Your code becomes easier to change and test.
How to implement the dependency injection? How to implement the dependency injection?
@ -66,25 +66,21 @@ Before:
class ApiClient: class ApiClient:
def __init__(self) -> None: def __init__(self):
self.api_key = os.getenv("API_KEY") # <-- dependency self.api_key = os.getenv('API_KEY') # <-- the dependency
self.timeout = int(os.getenv("TIMEOUT")) # <-- dependency self.timeout = os.getenv('TIMEOUT') # <-- the dependency
class Service: class Service:
def __init__(self) -> None: def __init__(self):
self.api_client = ApiClient() # <-- dependency self.api_client = ApiClient() # <-- the dependency
def main() -> None: if __name__ == '__main__':
service = Service() # <-- dependency service = Service()
...
if __name__ == "__main__":
main()
After: After:
.. code-block:: python .. code-block:: python
@ -94,30 +90,19 @@ After:
class ApiClient: class ApiClient:
def __init__(self, api_key: str, timeout: int) -> None: def __init__(self, api_key: str, timeout: int):
self.api_key = api_key # <-- dependency is injected self.api_key = api_key # <-- the dependency is injected
self.timeout = timeout # <-- dependency is injected self.timeout = timeout # <-- the dependency is injected
class Service: class Service:
def __init__(self, api_client: ApiClient) -> None: def __init__(self, api_client: ApiClient):
self.api_client = api_client # <-- dependency is injected self.api_client = api_client # <-- the dependency is injected
def main(service: Service) -> None: # <-- dependency is injected if __name__ == '__main__':
... service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))
if __name__ == "__main__":
main(
service=Service(
api_client=ApiClient(
api_key=os.getenv("API_KEY"),
timeout=int(os.getenv("TIMEOUT")),
),
),
)
``ApiClient`` is decoupled from knowing where the options come from. You can read a key and a ``ApiClient`` is decoupled from knowing where the options come from. You can read a key and a
timeout from a configuration file or even get them from a database. timeout from a configuration file or even get them from a database.
@ -125,22 +110,11 @@ 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 ``Service`` is decoupled from the ``ApiClient``. It does not create it anymore. You can provide a
stub or other compatible object. stub or other compatible object.
Function ``main()`` is decoupled from ``Service``. It receives it as an argument.
Flexibility comes with a price. Flexibility comes with a price.
Now you need to assemble and inject the objects like this: Now you need to assemble the objects like this::
.. code-block:: python service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))
main(
service=Service(
api_client=ApiClient(
api_key=os.getenv("API_KEY"),
timeout=int(os.getenv("TIMEOUT")),
),
),
)
The assembly code might get duplicated and it'll become harder to change the application structure. The assembly code might get duplicated and it'll become harder to change the application structure.
@ -149,20 +123,18 @@ Here comes the ``Dependency Injector``.
What does the Dependency Injector do? What does the Dependency Injector do?
------------------------------------- -------------------------------------
With the dependency injection pattern, objects lose the responsibility of assembling With the dependency injection pattern objects loose the responsibility of assembling the
the dependencies. The ``Dependency Injector`` absorbs that responsibility. dependencies. The ``Dependency Injector`` absorbs that responsibility.
``Dependency Injector`` helps to assemble and inject the dependencies. ``Dependency Injector`` helps to assemble the objects.
It provides a container and providers that help you with the objects assembly. It provides a container and providers that help you with the objects assembly. When you
When you need an object you place a ``Provide`` marker as a default value of a need an object you get it from the container. The rest of the assembly work is done by the
function argument. When you call this function, framework assembles and injects framework:
the dependency.
.. code-block:: python .. code-block:: python
from dependency_injector import containers, providers from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, inject
class Container(containers.DeclarativeContainer): class Container(containers.DeclarativeContainer):
@ -172,7 +144,7 @@ the dependency.
api_client = providers.Singleton( api_client = providers.Singleton(
ApiClient, ApiClient,
api_key=config.api_key, api_key=config.api_key,
timeout=config.timeout, timeout=config.timeout.as_int(),
) )
service = providers.Factory( service = providers.Factory(
@ -181,95 +153,96 @@ the dependency.
) )
@inject if __name__ == '__main__':
def main(service: Service = Provide[Container.service]) -> None:
...
if __name__ == "__main__":
container = Container() container = Container()
container.config.api_key.from_env("API_KEY", required=True) container.config.api_key.from_env('API_KEY')
container.config.timeout.from_env("TIMEOUT", as_=int, default=5) container.config.timeout.from_env('TIMEOUT')
container.wire(modules=[__name__])
main() # <-- dependency is injected automatically service = container.service()
with container.api_client.override(mock.Mock()): Retrieving of the ``Service`` instance now is done like this::
main() # <-- overridden dependency is injected automatically
When you call the ``main()`` function the ``Service`` dependency is assembled and injected automatically. service = container.service()
When you do testing, you call the ``container.api_client.override()`` method to replace the real API Objects assembling is consolidated in the container. When you need to make a change you do it in
client with a mock. When you call ``main()``, the mock is injected. one place.
When doing a testing you call the ``container.api_client.override()`` to replace the real API
client with a mock:
.. code-block:: python
from unittest import mock
with container.api_client.override(mock.Mock()):
service = container.service()
You can override any provider with another provider. You can override any provider with another provider.
It also helps you in a re-configuring project for different environments: replace an API client It also helps you in configuring project for the different environments: replace an API client
with a stub on the dev or stage. with a stub on the dev or stage.
Objects assembling is consolidated in a container. Dependency injections are defined explicitly.
This makes it easier to understand and change how an application works.
Testing, Monkey-patching and dependency injection Testing, Monkey-patching and dependency injection
------------------------------------------------- -------------------------------------------------
The testability benefit is opposed to monkey-patching. The testability benefit is opposed to a monkey-patching.
In Python, you can monkey-patch anything, anytime. The problem with monkey-patching is In Python you can monkey-patch
that it's too fragile. The cause of it is that when you monkey-patch you do something that anything, anytime. The problem with a monkey-patching is that it's too fragile. The reason is that
wasn't intended to be done. You monkey-patch the implementation details. When implementation when you monkey-patch you do something that wasn't intended to be done. You monkey-patch the
changes the monkey-patching is broken. implementation details. When implementation changes the monkey-patching is broken.
With dependency injection, you patch the interface, not an implementation. This is a way more With a dependency injection you patch the interface, not an implementation. This is a way more
stable approach. stable approach.
Also, monkey-patching is way too dirty to be used outside of the testing code for Also monkey-patching is a way too dirty to be used outside of the testing code for
re-configuring the project for the different environments. reconfiguring the project for the different environments.
Conclusion Conclusion
---------- ----------
Dependency injection provides you with three advantages: Dependency injection brings you 3 advantages:
- **Flexibility**. The components are loosely coupled. You can easily extend or change the - **Flexibility**. The components are loosely coupled. You can easily extend or change a
functionality of a system by combining the components in a different way. You even can do it on functionality of the system by combining the components different way. You even can do it on
the fly. the fly.
- **Testability**. Testing is easier because you can easily inject mocks instead of real objects - **Testability**. Testing is easy because you can easily inject mocks instead of real objects
that use API or database, etc. that use API or database, etc.
- **Clearness and maintainability**. Dependency injection helps you reveal the dependencies. - **Clearness and maintainability**. Dependency injection helps you reveal the dependencies.
Implicit becomes explicit. And "Explicit is better than implicit" (PEP 20 - The Zen of Python). Implicit becomes explicit. And "Explicit is better than implicit" (PEP 20 - The Zen of Python).
You have all the components and dependencies defined explicitly in a container. This You have all the components and dependencies defined explicitly in the container. This
provides an overview and control of the application structure. It is easier to understand and provides an overview and control on the application structure. It is easy to understand and
change it. change it.
Is it worth applying dependency injection in Python? Is it worth to use a dependency injection in Python?
It depends on what you build. The advantages above are not too important if you use Python as a It depends on what you build. The advantages above are not too important if you use Python as a
scripting language. The picture is different when you use Python to create an application. The scripting language. The picture is different when you use Python to create an application. The
larger the application the more significant the benefits. larger the application the more significant is the benefit.
Is it worth using a framework for applying dependency injection? Is it worth to use a framework for the dependency injection?
The complexity of the dependency injection pattern implementation in Python is The complexity of the dependency injection pattern implementation in Python is
lower than in other languages but it's still in place. It doesn't mean you have to use a lower than in the other languages but it's still in place. It doesn't mean you have to use a
framework but using a framework is beneficial because the framework is: framework but using a framework is beneficial because the framework is:
- Already implemented - Already implemented
- Tested on all platforms and versions of Python - Tested on all platforms and versions of Python
- Documented - Documented
- Supported - Supported
- Other engineers are familiar with it - Known to the other engineers
An advice at last: Few advices at last:
- **Give it a try**. Dependency injection is counter-intuitive. Our nature is that - **Give it a try**. Dependency injection is counter-intuitive. Our nature is that
when we need something the first thought that comes to our mind is to go and get it. Dependency when we need something the first thought that comes to our mind is to go and get it. Dependency
injection is just like "Wait, I need to state a need instead of getting something right away". injection is just like "Wait, I need to state a need instead of getting something right now".
It's like a little investment that will pay-off later. The advice is to just give it a try for It's like a little investment that will pay-off later. The advice is to just give it a try for
two weeks. This time will be enough for getting your own impression. If you don't like it you two weeks. This time will be enough for getting your own impression. If you don't like it you
won't lose too much. won't lose too much.
- **Common sense first**. Use common sense when applying dependency injection. It is a good - **Common sense first**. Use a common sense when apply dependency injection. It is a good
principle, but not a silver bullet. If you do it too much you will reveal too many of the principle, but not a silver bullet. If you do it too much you will reveal too much of the
implementation details. Experience comes with practice and time. implementation details. Experience comes with practice and time.
What's next? What's next?
@ -281,15 +254,6 @@ Choose one of the following as a next step:
- :ref:`application-single-container` - :ref:`application-single-container`
- :ref:`application-multiple-containers` - :ref:`application-multiple-containers`
- :ref:`decoupled-packages` - :ref:`decoupled-packages`
- :ref:`boto3-example`
- :ref:`django-example`
- :ref:`flask-example`
- :ref:`flask-blueprints-example`
- :ref:`aiohttp-example`
- :ref:`sanic-example`
- :ref:`fastapi-example`
- :ref:`fastapi-redis-example`
- :ref:`fastapi-sqlalchemy-example`
- Pass the tutorials: - Pass the tutorials:
- :ref:`flask-tutorial` - :ref:`flask-tutorial`
- :ref:`aiohttp-tutorial` - :ref:`aiohttp-tutorial`
@ -297,19 +261,17 @@ Choose one of the following as a next step:
- :ref:`cli-tutorial` - :ref:`cli-tutorial`
- Know more about the ``Dependency Injector`` :ref:`key-features` - Know more about the ``Dependency Injector`` :ref:`key-features`
- Know more about the :ref:`providers` - Know more about the :ref:`providers`
- Know more about the :ref:`wiring`
- Go to the :ref:`contents` - Go to the :ref:`contents`
Useful links Useful links
------------ ------------
A few useful links related to a dependency injection design pattern for further reading: There are some useful links related to dependency injection design pattern
that could be used for further reading:
+ https://en.wikipedia.org/wiki/Dependency_injection + https://en.wikipedia.org/wiki/Dependency_injection
+ https://martinfowler.com/articles/injection.html + https://martinfowler.com/articles/injection.html
+ https://github.com/ets-labs/python-dependency-injector + https://github.com/ets-labs/python-dependency-injector
+ https://pypi.org/project/dependency-injector/ + https://pypi.org/project/dependency-injector/
.. include:: ../sponsor.rst
.. disqus:: .. disqus::

View File

@ -7,8 +7,8 @@ Introduction
overview of the dependency injection, inversion of overview of the dependency injection, inversion of
control and Dependency Injector framework. control and Dependency Injector framework.
The current section of the documentation provides an overview of the Current section of the documentation provides an overview of the
dependency injection, inversion of control, and the ``Dependency Injector`` framework. dependency injection, inversion of control and the ``Dependency Injector`` framework.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2

View File

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

View File

@ -11,33 +11,28 @@ Key features
Key features of the ``Dependency Injector``: Key features of the ``Dependency Injector``:
- **Providers**. Provides ``Factory``, ``Singleton``, ``Callable``, ``Coroutine``, ``Object``, - **Providers**. Provides ``Factory``, ``Singleton``, ``Callable``, ``Coroutine``, ``Object``,
``List``, ``Dict``, ``Configuration``, ``Resource``, ``Dependency``, and ``Selector`` providers ``List``, ``Configuration``, ``Dependency`` and ``Selector`` providers that help assembling your
that help assemble your objects. See :ref:`providers`. objects. See :ref:`providers`.
- **Overriding**. Can override any provider by another provider on the fly. This helps in testing - **Overriding**. Can override any provider by another provider on the fly. This helps in testing
and configuring dev/stage environment to replace API clients with stubs etc. See and configuring dev / stage environment to replace API clients with stubs etc. See
:ref:`provider-overriding`. :ref:`provider-overriding`.
- **Configuration**. Reads configuration from ``yaml``, ``ini``, and ``json`` files, ``pydantic`` settings, - **Configuration**. Read configuration from ``yaml`` & ``ini`` files, environment variables
environment variables, and dictionaries. See :ref:`configuration-provider`. and dictionaries. See :ref:`configuration-provider`.
- **Resources**. Helps with initialization and configuring of logging, event loop, thread
or process pool, etc. Can be used for per-function execution scope in tandem with wiring.
See :ref:`resource-provider`.
- **Containers**. Provides declarative and dynamic containers. See :ref:`containers`. - **Containers**. Provides declarative and dynamic containers. See :ref:`containers`.
- **Wiring**. Injects dependencies into functions and methods. Helps integrate with
other frameworks: Django, Flask, Aiohttp, Sanic, FastAPI, etc. See :ref:`wiring`.
- **Asynchronous**. Supports asynchronous injections. See :ref:`async-injections`.
- **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`.
- **Performance**. Fast. Written in ``Cython``. - **Performance**. Fast. Written in ``Cython``.
- **Maturity**. Mature and production-ready. Well-tested, documented, and supported. - **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`.
- **Maturity**. Mature and production-ready. Well-tested, documented and supported.
The framework stands on the `PEP20 (The Zen of Python) <https://www.python.org/dev/peps/pep-0020/>`_ principle: The framework stands on two principles:
.. code-block:: text - **Explicit is better than implicit (PEP20)**.
- **Do not do any magic to your code**.
Explicit is better than implicit How is that different from the other frameworks?
You need to specify how to assemble and where to inject the dependencies explicitly. - **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.
The power of the framework is in its simplicity. The power of the framework is in a simplicity. ``Dependency Injector`` is a simple tool for the powerful concept.
``Dependency Injector`` is a simple tool for the powerful concept.
.. disqus:: .. disqus::

File diff suppressed because it is too large Load Diff

View File

@ -1,72 +0,0 @@
.. _aggregate-provider:
Aggregate provider
==================
.. meta::
:keywords: Python,DI,Dependency injection,IoC,Inversion of Control,Configuration,Injection,
Aggregate,Polymorphism,Environment Variable,Flexibility
:description: Aggregate provider aggregates other providers.
This page demonstrates how to implement the polymorphism and increase the
flexibility of your application using the Aggregate provider.
:py:class:`Aggregate` provider aggregates a group of other providers.
.. currentmodule:: dependency_injector.providers
.. literalinclude:: ../../examples/providers/aggregate.py
:language: python
:lines: 3-
:emphasize-lines: 24-27
Each provider in the ``Aggregate`` is associated with a key. You can call aggregated providers by providing
their key as a first argument. All positional and keyword arguments following the key will be forwarded to
the called provider:
.. code-block:: python
yaml_reader = container.config_readers("yaml", "./config.yml", foo=...)
You can also retrieve an aggregated provider by providing its key as an attribute name:
.. code-block:: python
yaml_reader = container.config_readers.yaml("./config.yml", foo=...)
To retrieve a dictionary of aggregated providers, use ``.providers`` attribute:
.. code-block:: python
container.config_readers.providers == {
"yaml": <YAML provider>,
"json": <JSON provider>,
}
.. note::
You can not override the ``Aggregate`` provider.
.. note::
When you inject the ``Aggregate`` provider, it is passed "as is".
To use non-string keys or string keys with ``.`` and ``-``, provide a dictionary as a positional argument:
.. code-block:: python
aggregate = providers.Aggregate({
SomeClass: providers.Factory(...),
"key.with.periods": providers.Factory(...),
"key-with-dashes": providers.Factory(...),
})
.. seealso::
:ref:`selector-provider` to make injections based on a configuration value, environment variable, or a result of a callable.
``Aggregate`` provider is different from the :ref:`selector-provider`. ``Aggregate`` provider doesn't select which provider
to inject and doesn't have a selector. It is a group of providers and is always injected "as is". The rest of the interface
of both providers is similar.
.. note::
``Aggregate`` provider is a successor of :ref:`factory-aggregate-provider` provider. ``Aggregate`` provider doesn't have
a restriction on the provider type, while ``FactoryAggregate`` aggregates only ``Factory`` providers.
.. disqus::

View File

@ -1,110 +0,0 @@
.. _async-injections:
Asynchronous injections
=======================
.. meta::
:keywords: Python,DI,Dependency injection,IoC,Inversion of Control,Providers,Async,Injections,Asynchronous,Await,
Asyncio
:description: Dependency Injector providers support asynchronous injections. This page
demonstrates how make asynchronous dependency injections in Python.
Providers support asynchronous injections.
.. literalinclude:: ../../examples/providers/async.py
:language: python
:emphasize-lines: 26-29
:lines: 3-
If provider has any awaitable injections it switches into async mode. In async mode provider always returns awaitable.
This causes a cascade effect:
.. code-block:: bash
provider1() <── Async mode enabled <──┐
│ │
├──> provider2() │
│ │
├──> provider3() <── Async mode enabled <──┤
│ │ │
│ └──> provider4() <── Async provider ───────┘
└──> provider5()
└──> provider6()
In async mode provider prepares injections asynchronously.
If provider has multiple awaitable dependencies, it will run them concurrently. Provider will wait until all
dependencies are ready and inject them afterwards.
.. code-block:: bash
provider1()
├──> provider2() <── Async mode enabled
├──> provider3() <── Async mode enabled
└──> provider4() <── Async mode enabled
Here is what provider will do for the previous example:
.. code-block:: python
injections = await asyncio.gather(
provider2(),
provider3(),
provider4(),
)
await provider1(*injections)
Overriding behaviour
--------------------
In async mode provider always returns awaitable. It applies to the overriding too. If provider in async mode is
overridden by a provider that doesn't return awaitable result, the result will be wrapped into awaitable.
.. literalinclude:: ../../examples/providers/async_overriding.py
:language: python
:emphasize-lines: 19-24
:lines: 3-
Async mode mechanics and API
----------------------------
By default provider's async mode is undefined.
When provider async mode is undefined, provider will automatically select the mode during the next call.
If the result is awaitable, provider will enable async mode, if not - disable it.
If provider async mode is enabled, provider always returns awaitable. If the result is not awaitable,
provider wraps it into awaitable explicitly. You can safely ``await`` provider in async mode.
If provider async mode is disabled, provider behaves the regular way. It doesn't do async injections
preparation or non-awaitables to awaitables conversion.
Once provider async mode is enabled or disabled, provider will stay in this state. No automatic switching
will be done.
.. image:: images/async_mode.png
You can also use following methods to change provider's async mode manually:
- ``Provider.enable_async_mode()``
- ``Provider.disable_async_mode()``
- ``Provider.reset_async_mode()``
To check the state of provider's async mode use:
- ``Provider.is_async_mode_enabled()``
- ``Provider.is_async_mode_disabled()``
- ``Provider.is_async_mode_undefined()``
See also:
- Wiring :ref:`async-injections-wiring`
- Resource provider :ref:`resource-async-initializers`
- :ref:`fastapi-redis-example`
.. disqus::

View File

@ -5,14 +5,10 @@ Configuration provider
.. meta:: .. meta::
:keywords: Python,DI,Dependency injection,IoC,Inversion of Control,Configuration,Injection, :keywords: Python,DI,Dependency injection,IoC,Inversion of Control,Configuration,Injection,
Option,Ini,Json,Yaml,Pydantic,Dict,Environment Variable Interpolation, Option,Ini,Json,Yaml,Dict,Environment Variable,Load,Read,Get
Environment Variable Substitution,Environment Variable in Config,
Environment Variable in YAML file,Environment Variable in INI file,Default,Load,Read
:description: Configuration provides configuration options to the other providers. This page :description: Configuration provides configuration options to the other providers. This page
demonstrates how to use Configuration provider to inject the dependencies, load demonstrates how to use Configuration provider to inject the dependencies, load
a configuration from an ini or yaml file, a dictionary, an environment variable, a configuration from an ini or yaml file, dictionary or an environment variable.
or a pydantic settings object. This page also describes how to substitute (interpolate)
environment variables in YAML and INI configuration files.
.. currentmodule:: dependency_injector.providers .. currentmodule:: dependency_injector.providers
@ -25,10 +21,6 @@ Configuration provider
It implements the principle "use first, define later". It implements the principle "use first, define later".
.. contents::
:local:
:backlinks: none
Loading from an INI file Loading from an INI file
------------------------ ------------------------
@ -45,31 +37,9 @@ where ``examples/providers/configuration/config.ini`` is:
.. literalinclude:: ../../examples/providers/configuration/config.ini .. literalinclude:: ../../examples/providers/configuration/config.ini
:language: ini :language: ini
Alternatively, you can provide a path to the INI file over the configuration provider argument. In that case, :py:meth:`Configuration.from_ini` method supports environment variables interpolation. Use
the container will call ``config.from_ini()`` automatically: ``${ENV_NAME}`` format in the configuration file to substitute value of the environment
variable ``ENV_NAME``.
.. code-block:: python
:emphasize-lines: 3
class Container(containers.DeclarativeContainer):
config = providers.Configuration(ini_files=["./config.ini"])
if __name__ == "__main__":
container = Container() # Config is loaded from ./config.ini
:py:meth:`Configuration.from_ini` method supports environment variables interpolation.
.. code-block:: ini
[section]
option1 = ${ENV_VAR}
option2 = ${ENV_VAR}/path
option3 = ${ENV_VAR:default}
See also: :ref:`configuration-envs-interpolation`.
Loading from a YAML file Loading from a YAML file
------------------------ ------------------------
@ -87,40 +57,9 @@ where ``examples/providers/configuration/config.yml`` is:
.. literalinclude:: ../../examples/providers/configuration/config.yml .. literalinclude:: ../../examples/providers/configuration/config.yml
:language: ini :language: ini
Alternatively, you can provide a path to the YAML file over the configuration provider argument. In that case, :py:meth:`Configuration.from_yaml` method supports environment variables interpolation. Use
the container will call ``config.from_yaml()`` automatically: ``${ENV_NAME}`` format in the configuration file to substitute value of the environment
variable ``ENV_NAME``.
.. code-block:: python
:emphasize-lines: 3
class Container(containers.DeclarativeContainer):
config = providers.Configuration(yaml_files=["./config.yml"])
if __name__ == "__main__":
container = Container() # Config is loaded from ./config.yml
:py:meth:`Configuration.from_yaml` method supports environment variables interpolation.
.. code-block:: ini
section:
option1: ${ENV_VAR}
option2: ${ENV_VAR}/path
option3: ${ENV_VAR:default}
See also: :ref:`configuration-envs-interpolation`.
:py:meth:`Configuration.from_yaml` method uses custom version of ``yaml.SafeLoader``.
To use another loader use ``loader`` argument:
.. code-block:: python
import yaml
container.config.from_yaml("config.yml", loader=yaml.UnsafeLoader)
.. note:: .. note::
@ -136,102 +75,6 @@ To use another loader use ``loader`` argument:
*Don't forget to mirror the changes in the requirements file.* *Don't forget to mirror the changes in the requirements file.*
Loading from a JSON file
------------------------
``Configuration`` provider can load configuration from a ``json`` file using the
:py:meth:`Configuration.from_json` method:
.. literalinclude:: ../../examples/providers/configuration/configuration_json.py
:language: python
:lines: 3-
:emphasize-lines: 12
where ``examples/providers/configuration/config.json`` is:
.. literalinclude:: ../../examples/providers/configuration/config.json
:language: json
Alternatively, you can provide a path to a json file over the configuration provider argument. In that case,
the container will call ``config.from_json()`` automatically:
.. code-block:: python
:emphasize-lines: 3
class Container(containers.DeclarativeContainer):
config = providers.Configuration(json_files=["./config.json"])
if __name__ == "__main__":
container = Container() # Config is loaded from ./config.json
:py:meth:`Configuration.from_json` method supports environment variables interpolation.
.. code-block:: json
{
"section": {
"option1": "${ENV_VAR}",
"option2": "${ENV_VAR}/path",
"option3": "${ENV_VAR:default}"
}
}
See also: :ref:`configuration-envs-interpolation`.
Loading from a Pydantic settings
--------------------------------
``Configuration`` provider can load configuration from a ``pydantic_settings.BaseSettings`` object using the
:py:meth:`Configuration.from_pydantic` method:
.. literalinclude:: ../../examples/providers/configuration/configuration_pydantic.py
:language: python
:lines: 3-
:emphasize-lines: 32
To get the data from pydantic settings ``Configuration`` provider calls its ``model_dump()`` method.
If you need to pass an argument to this call, use ``.from_pydantic()`` keyword arguments.
.. code-block:: python
container.config.from_pydantic(Settings(), exclude={"optional"})
Alternatively, you can provide a ``pydantic_settings.BaseSettings`` object over the configuration provider argument. In that case,
the container will call ``config.from_pydantic()`` automatically:
.. code-block:: python
:emphasize-lines: 3
class Container(containers.DeclarativeContainer):
config = providers.Configuration(pydantic_settings=[Settings()])
if __name__ == "__main__":
container = Container() # Config is loaded from Settings()
.. note::
``Dependency Injector`` doesn't install ``pydantic-settings`` by default.
You can install the ``Dependency Injector`` with an extra dependency::
pip install dependency-injector[pydantic2]
or install ``pydantic-settings`` directly::
pip install pydantic-settings
*Don't forget to mirror the changes in the requirements file.*
.. note::
For backward-compatibility, Pydantic v1 is still supported.
Passing ``pydantic.BaseSettings`` instances will work just as fine as ``pydantic_settings.BaseSettings``.
Loading from a dictionary Loading from a dictionary
------------------------- -------------------------
@ -254,35 +97,6 @@ Loading from an environment variable
:lines: 3- :lines: 3-
:emphasize-lines: 18-20 :emphasize-lines: 18-20
You can use ``as_`` argument for the type casting of an environment variable value:
.. code-block:: python
:emphasize-lines: 2,6,10
# API_KEY=secret
container.config.api_key.from_env("API_KEY", as_=str, required=True)
assert container.config.api_key() == "secret"
# SAMPLING_RATIO=0.5
container.config.sampling.from_env("SAMPLING_RATIO", as_=float, required=True)
assert container.config.sampling() == 0.5
# TIMEOUT undefined, default is used
container.config.timeout.from_env("TIMEOUT", as_=int, default=5)
assert container.config.timeout() == 5
Loading a value
---------------
``Configuration`` provider can load configuration value using the
:py:meth:`Configuration.from_value` method:
.. literalinclude:: ../../examples/providers/configuration/configuration_value.py
:language: python
:lines: 3-
:emphasize-lines: 14-15
Loading from the multiple sources Loading from the multiple sources
--------------------------------- ---------------------------------
@ -299,123 +113,6 @@ where ``examples/providers/configuration/config.local.yml`` is:
.. literalinclude:: ../../examples/providers/configuration/config.local.yml .. literalinclude:: ../../examples/providers/configuration/config.local.yml
:language: ini :language: ini
.. _configuration-envs-interpolation:
Using environment variables in configuration files
--------------------------------------------------
``Configuration`` provider supports environment variables interpolation in configuration files.
Use ``${ENV_NAME}`` in the configuration file to substitute value from environment
variable ``ENV_NAME``.
.. code-block:: ini
section:
option: ${ENV_NAME}
You can also specify a default value using ``${ENV_NAME:default}`` format. If environment
variable ``ENV_NAME`` is undefined, configuration provider will substitute value ``default``.
.. code-block:: ini
[section]
option = ${ENV_NAME:default}
If you'd like to specify a default value for environment variable inside of the application you can use
``os.environ.setdefault()``.
.. literalinclude:: ../../examples/providers/configuration/configuration_env_interpolation_os_default.py
:language: python
:lines: 3-
:emphasize-lines: 12
If environment variable is undefined and doesn't have a default, ``Configuration`` provider
will replace it with an empty value. This is a default behavior. To raise an error on
undefined environment variable that doesn't have a default value, pass argument
``envs_required=True`` to a configuration reading method:
.. code-block:: python
container.config.from_yaml("config.yml", envs_required=True)
See also: :ref:`configuration-strict-mode`.
.. note::
``Configuration`` provider makes environment variables interpolation before parsing. This preserves
original parser behavior. For instance, undefined environment variable in YAML configuration file
will be replaced with an empty value and then YAML parser will load the file.
Original configuration file:
.. code-block:: ini
section:
option: ${ENV_NAME}
Configuration file after interpolation where ``ENV_NAME`` is undefined:
.. code-block:: ini
section:
option:
Configuration provider after parsing interpolated YAML file contains ``None`` in
option ``section.option``:
.. code-block:: python
assert container.config.section.option() is None
If you want to disable environment variables interpolation, pass ``envs_required=None``:
.. code-block:: yaml
:caption: templates.yml
template_string: 'Hello, ${name}!'
.. code-block:: python
>>> container.config.from_yaml("templates.yml", envs_required=None)
>>> container.config.template_string()
'Hello, ${name}!'
Mandatory and optional sources
------------------------------
By default, methods ``.from_yaml()`` and ``.from_ini()`` ignore errors if configuration file does not exist.
You can use this to specify optional configuration files.
If configuration file is mandatory, use ``required`` argument. Configuration provider will raise an error
if required file does not exist.
You can also use ``required`` argument when loading configuration from dictionaries and environment variables.
Mandatory YAML file:
.. code-block:: python
container.config.from_yaml("config.yaml", required=True)
Mandatory INI file:
.. code-block:: python
container.config.from_ini("config.ini", required=True)
Mandatory dictionary:
.. code-block:: python
container.config.from_dict(config_dict, required=True)
Mandatory environment variable:
.. code-block:: python
container.config.api_key.from_env("API_KEY", required=True)
See also: :ref:`configuration-strict-mode`.
Specifying the value type Specifying the value type
------------------------- -------------------------
@ -446,119 +143,6 @@ With the ``.as_(callback, *args, **kwargs)`` you can specify a function that wil
before the injection. The value from the config will be passed as a first argument. The returned before the injection. The value from the config will be passed as a first argument. The returned
value will be injected. Parameters ``*args`` and ``**kwargs`` are handled as any other injections. value will be injected. Parameters ``*args`` and ``**kwargs`` are handled as any other injections.
.. _configuration-strict-mode:
Strict mode and required options
--------------------------------
You can use configuration provider in strict mode. In strict mode configuration provider raises an error
on access to any undefined option.
.. literalinclude:: ../../examples/providers/configuration/configuration_strict.py
:language: python
:lines: 3-
:emphasize-lines: 12
Methods ``.from_*()`` in strict mode raise an exception if configuration file does not exist or
configuration data is undefined:
.. code-block:: python
:emphasize-lines: 10,15,20,25,30
class Container(containers.DeclarativeContainer):
config = providers.Configuration(strict=True)
if __name__ == "__main__":
container = Container()
try:
container.config.from_yaml("does-not_exist.yml") # raise exception
except FileNotFoundError:
...
try:
container.config.from_ini("does-not_exist.ini") # raise exception
except FileNotFoundError:
...
try:
container.config.from_pydantic(EmptySettings()) # raise exception
except ValueError:
...
try:
container.config.from_env("UNDEFINED_ENV_VAR") # raise exception
except ValueError:
...
try:
container.config.from_dict({}) # raise exception
except ValueError:
...
Environment variables interpolation in strict mode raises an exception when encounters
an undefined environment variable without a default value.
.. code-block:: ini
section:
option: ${UNDEFINED}
.. code-block:: python
try:
container.config.from_yaml("undefined_env.yml") # raise exception
except ValueError:
...
You can override ``.from_*()`` methods behaviour in strict mode using ``required`` argument:
.. code-block:: python
class Container(containers.DeclarativeContainer):
config = providers.Configuration(strict=True)
if __name__ == "__main__":
container = Container()
container.config.from_yaml("config.yml")
container.config.from_yaml("config.local.yml", required=False)
You can also use ``.required()`` option modifier when making an injection. It does not require to switch
configuration provider to strict mode.
.. literalinclude:: ../../examples/providers/configuration/configuration_required.py
:language: python
:lines: 11-20
:emphasize-lines: 8-9
.. note::
Modifier ``.required()`` should be specified before type modifier ``.as_*()``.
Aliases
-------
You can use ``Configuration`` provider with a context manager to create aliases.
.. literalinclude:: ../../examples/providers/configuration/configuration_alias.py
:language: python
:lines: 3-
:emphasize-lines: 14,22
.. note::
Library ``environs`` is a 3rd party library. You need to install it
separately::
pip install environs
Documentation is available on GitHub: https://github.com/sloria/environs
Injecting invariants Injecting invariants
-------------------- --------------------

View File

@ -16,16 +16,12 @@ To create a custom provider you need to follow these rules:
1. New provider class should inherit :py:class:`Provider`. 1. New provider class should inherit :py:class:`Provider`.
2. You need to implement the ``Provider._provide()`` method. 2. You need to implement the ``Provider._provide()`` method.
3. You need to implement the ``Provider.__deepcopy__()`` method. It should return an 3. You need to implement the ``Provider.__deepcopy__()`` method. It should return an
equivalent copy of a provider. All providers must be copied with the ``deepcopy()`` function equivalent copy of a provider. All providers must be copied with a ``deepcopy()`` function
from the ``providers`` module. It's essential to pass ``memo`` into ``deepcopy`` in order to keep from the ``providers`` module. After the a new provider object is created use
the preconfigured ``args`` and ``kwargs`` of stored providers. After the a new provider object ``Provider._copy_overriding()`` method to copy all overriding providers. See the example
is created, use ``Provider._copy_overriding()`` method to copy all overriding providers. See the below.
example below. 4. If the new provider has a ``__init__()`` method, it should call the parent
4. If new provider has a ``__init__()`` method, it should call the parent
``Provider.__init__()``. ``Provider.__init__()``.
5. If new provider stores any other providers, these providers should be listed in
``.related`` property. Property ``.related`` also should yield providers from parent
``.related`` property.
.. literalinclude:: ../../examples/providers/custom_factory.py .. literalinclude:: ../../examples/providers/custom_factory.py
:language: python :language: python
@ -34,7 +30,7 @@ To create a custom provider you need to follow these rules:
.. note:: .. note::
1. Prefer delegation over inheritance. If you choose between inheriting a ``Factory`` or 1. Prefer delegation over inheritance. If you choose between inheriting a ``Factory`` or
inheriting a ``Provider`` and use ``Factory`` internally - the last is better. inheriting a ``Provider`` and use ``Factory`` internally - the last is better.
2. When creating a new provider follow the ``Factory``-like injections style. Consistency matters. 2. When create a new provider follow the ``Factory``-like injections style. Consistency matters.
3. Use the ``__slots__`` attribute to make sure nothing could be attached to your provider. You 3. Use the ``__slots__`` attribute to make sure nothing could be attached to your provider. You
will also save some memory. will also save some memory.

View File

@ -1,38 +1,21 @@
.. _dependency-provider:
Dependency provider Dependency provider
=================== ===================
.. currentmodule:: dependency_injector.providers .. currentmodule:: dependency_injector.providers
:py:class:`Dependency` provider is a placeholder for a dependency of a certain type. :py:class:`Dependency` provider is a placeholder for the dependency of the specified type.
To specify a type of the dependency use ``instance_of`` argument: ``Dependency(instance_of=SomeClass)``. The first argument of the ``Dependency`` provider specifies a type of the dependency. It is
Dependency provider will control that returned object is an instance of ``instance_of`` type. called ``instance_of``. ``Dependency`` provider controls the type of the returned object to be an
instance of the ``instance_of`` type.
The ``Dependency`` provider must be overridden before usage. It can be overridden by any type of
the provider. The only rule is that overriding provider must return an instance of ``instance_of``
dependency type.
.. literalinclude:: ../../examples/providers/dependency.py .. literalinclude:: ../../examples/providers/dependency.py
:language: python :language: python
:lines: 3- :lines: 3-
:emphasize-lines: 26,35-36 :emphasize-lines: 26
To provide a dependency you need to override the ``Dependency`` provider. You can call
provider ``.override()`` method or provide an overriding provider when creating a container.
See :ref:`provider-overriding`. If you don't provide a dependency, ``Dependency`` provider
will raise an error:
.. literalinclude:: ../../examples/providers/dependency_undefined_error.py
:language: python
:lines: 18-
You also can provide a default for the dependency. To provide a default use ``default`` argument:
``Dependency(..., default=...)``. Default can be a value or a provider. If default is not a provider,
dependency provider will wrap it into the ``Object`` provider.
.. literalinclude:: ../../examples/providers/dependency_default.py
:language: python
:lines: 16-23
:emphasize-lines: 3
See also: :ref:`check-container-dependencies`.
.. disqus:: .. disqus::

View File

@ -1,37 +0,0 @@
Dict provider
=============
.. meta::
:keywords: Python,DI,Dependency injection,IoC,Inversion of Control,Dict,Injection
:description: Dict provider helps to inject a dictionary of the dependencies. This page demonstrates
how to use Dict provider.
.. currentmodule:: dependency_injector.providers
:py:class:`Dict` provider provides a dictionary of values.
.. literalinclude:: ../../examples/providers/dict.py
:language: python
:lines: 3-
:emphasize-lines: 21-24
``Dict`` provider handles keyword arguments the same way as a :ref:`factory-provider`.
To use non-string keys or keys with ``.`` and ``-`` provide a dictionary as a positional argument:
.. code-block:: python
providers.Dict({
SomeClass: providers.Factory(...),
"key.with.periods": providers.Factory(...),
"key-with-dashes": providers.Factory(...),
})
Example:
.. literalinclude:: ../../examples/providers/dict_non_string_keys.py
:language: python
:lines: 3-
:emphasize-lines: 40-43
.. disqus::

View File

@ -40,14 +40,6 @@ injected following these rules:
:language: python :language: python
:lines: 3- :lines: 3-
``Factory`` provider can inject attributes. Use ``.add_attributes()`` method to specify
attribute injections.
.. literalinclude:: ../../examples/providers/factory_attribute_injections.py
:language: python
:lines: 3-
:emphasize-lines: 18-18
Passing arguments to the underlying providers Passing arguments to the underlying providers
--------------------------------------------- ---------------------------------------------
@ -110,45 +102,6 @@ attribute of the provider that you're going to inject.
.. note:: Any provider has a ``.provider`` attribute. .. note:: Any provider has a ``.provider`` attribute.
.. _factory-string-imports:
String imports
--------------
``Factory`` provider can handle string imports:
.. code-block:: python
class Container(containers.DeclarativeContainer):
service = providers.Factory("myapp.mypackage.mymodule.Service")
You can also make a relative import:
.. code-block:: python
# in myapp/container.py
class Container(containers.DeclarativeContainer):
service = providers.Factory(".mypackage.mymodule.Service")
or import a member of the current module just specifying its name:
.. code-block:: python
class Service:
...
class Container(containers.DeclarativeContainer):
service = providers.Factory("Service")
.. note::
``Singleton``, ``Callable``, ``Resource``, and ``Coroutine`` providers handle string imports
the same way as a ``Factory`` provider.
.. _factory-specialize-provided-type: .. _factory-specialize-provided-type:
Specializing the provided type Specializing the provided type
@ -184,20 +137,16 @@ provider with two peculiarities:
:lines: 3- :lines: 3-
:emphasize-lines: 34 :emphasize-lines: 34
.. _factory-aggregate-provider:
Factory aggregate Factory aggregate
----------------- -----------------
:py:class:`FactoryAggregate` provider aggregates multiple factories. :py:class:`FactoryAggregate` provider aggregates multiple factories. When you call the
``FactoryAggregate`` it delegates the call to one of the factories.
.. seealso:: The aggregated factories are associated with the string names. When you call the
:ref:`aggregate-provider` it's a successor of ``FactoryAggregate`` provider that can aggregate ``FactoryAggregate`` you have to provide one of the these names as a first argument.
any type of provider, not only ``Factory``. ``FactoryAggregate`` looks for the factory with a matching name and delegates it the work. The
rest of the arguments are passed to the delegated ``Factory``.
The aggregated factories are associated with the string keys. When you call the
``FactoryAggregate`` you have to provide one of the these keys as a first argument.
``FactoryAggregate`` looks for the factory with a matching key and calls it with the rest of the arguments.
.. image:: images/factory_aggregate.png .. image:: images/factory_aggregate.png
:width: 100% :width: 100%
@ -208,12 +157,12 @@ The aggregated factories are associated with the string keys. When you call the
:lines: 3- :lines: 3-
:emphasize-lines: 33-37,47 :emphasize-lines: 33-37,47
You can get a dictionary of the aggregated providers using ``.providers`` attribute. You can get a dictionary of the aggregated factories using the ``.factories`` attribute of the
To get a game provider dictionary from the previous example you can use ``FactoryAggregate``. To get a game factories dictionary from the previous example you can use
``game_factory.providers`` attribute. ``game_factory.factories`` attribute.
You can also access an aggregated factory as an attribute. To create the ``Chess`` object from the You can also access an aggregated factory as an attribute. To create the ``Chess`` object from the
previous example you can do ``chess = game_factory.chess("John", "Jane")``. previous example you can do ``chess = game_factory.chess('John', 'Jane')``.
.. note:: .. note::
You can not override the ``FactoryAggregate`` provider. You can not override the ``FactoryAggregate`` provider.
@ -221,22 +170,4 @@ previous example you can do ``chess = game_factory.chess("John", "Jane")``.
.. note:: .. note::
When you inject the ``FactoryAggregate`` provider it is passed "as is". When you inject the ``FactoryAggregate`` provider it is passed "as is".
To use non-string keys or string keys with ``.`` and ``-``, you can provide a dictionary as a positional argument:
.. code-block:: python
providers.FactoryAggregate({
SomeClass: providers.Factory(...),
"key.with.periods": providers.Factory(...),
"key-with-dashes": providers.Factory(...),
})
Example:
.. literalinclude:: ../../examples/providers/factory_aggregate_non_string_keys.py
:language: python
:lines: 3-
:emphasize-lines: 30-33,39-40
.. disqus:: .. disqus::

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -43,15 +43,10 @@ Providers module API docs - :py:mod:`dependency_injector.providers`
coroutine coroutine
object object
list list
dict
configuration configuration
resource
aggregate
selector selector
dependency dependency
overriding overriding
provided_instance provided_instance
inject_self
custom custom
async
typing_mypy typing_mypy

View File

@ -1,20 +0,0 @@
Injecting container "self"
==========================
You can inject container "self" into container providers.
.. literalinclude:: ../../examples/containers/inject_self.py
:language: python
:lines: 3-
:emphasize-lines: 20, 26
To inject container "self" you need to define ``Self`` provider. Container can have only one ``Self`` provider.
Usually you will use name ``__self__``.
You can also use different name. When you use different name container will also reference
defined ``Self`` provider in ``.__self__`` attribute.
Provider ``Self`` is not listed in container ``.providers`` attributes.
.. disqus::

View File

@ -35,4 +35,24 @@ You can do nested constructions:
:emphasize-lines: 26-32 :emphasize-lines: 26-32
:lines: 3- :lines: 3-
The ``.provided`` attribute is available for the next providers:
- :py:class:`Factory` and its subclasses
- :py:class:`Singleton` and its subclasses
- :py:class:`Callable` and its subclasses
- :py:class:`Object`
- :py:class:`List`
- :py:class:`Selector`
- :py:class:`Dependency`
When you create a new provider subclass and want to implement the ``.provided`` attribute, you
should use the :py:class:`ProvidedInstance` provider. Add the ``.provided`` property
implementation to a new subclass:
.. code-block:: python
@property
def provided(self):
return ProvidedInstance(self)
.. disqus:: .. disqus::

View File

@ -1,520 +0,0 @@
.. _resource-provider:
Resource provider
=================
.. meta::
:keywords: Python,DI,Dependency injection,IoC,Inversion of Control,Resource,Injection,
Logging,Event Loop,Thread Pool
:description: Resource provider provides a component with initialization and shutdown. It works
well for configuring logging, event loop, thread or process pool, etc.
This page demonstrates how to use resource provider.
.. currentmodule:: dependency_injector.providers
:py:class:`Resource` provider provides a component with initialization and shutdown.
.. literalinclude:: ../../examples/providers/resource.py
:language: python
:lines: 3-
Resource providers help to initialize and configure logging, event loop, thread or process pool, etc.
Resource provider is similar to ``Singleton``. Resource initialization happens only once.
You can make injections and use provided instance the same way like you do with any other provider.
.. code-block:: python
:emphasize-lines: 12
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
thread_pool = providers.Resource(
init_thread_pool,
max_workers=config.max_workers,
)
dispatcher = providers.Factory(
TaskDispatcher,
executor=thread_pool,
)
Container has an interface to initialize and shutdown all resources at once:
.. code-block:: python
container = Container()
container.init_resources()
container.shutdown_resources()
You can also initialize and shutdown resources one-by-one using ``init()`` and
``shutdown()`` methods of the provider:
.. code-block:: python
container = Container()
container.thread_pool.init()
container.thread_pool.shutdown()
When you call ``.shutdown()`` method on a resource provider, it will remove the reference to the initialized resource,
if any, and switch to uninitialized state. Some of resource initializer types support specifying custom
resource shutdown.
Resource provider supports 4 types of initializers:
- Function
- Context Manager
- Generator (legacy)
- Subclass of ``resources.Resource`` (legacy)
Function initializer
--------------------
Function is the most common way to specify resource initialization:
.. code-block:: python
def init_resource(argument1=..., argument2=...):
return SomeResource()
class Container(containers.DeclarativeContainer):
resource = providers.Resource(
init_resource,
argument1=...,
argument2=...,
)
Function initializer may not return a value. This often happens when
you configure global resource:
.. code-block:: python
import logging.config
class Container(containers.DeclarativeContainer):
configure_logging = providers.Resource(
logging.config.fileConfig,
fname="logging.ini",
)
Function initializer does not provide a way to specify custom resource shutdown.
Context Manager initializer
---------------------------
This is an extension to the Function initializer. Resource provider automatically detects if the initializer returns a
context manager and uses it to manage the resource lifecycle.
.. code-block:: python
from dependency_injector import containers, providers
class DatabaseConnection:
def __init__(self, host, port, user, password):
self.host = host
self.port = port
self.user = user
self.password = password
def __enter__(self):
print(f"Connecting to {self.host}:{self.port} as {self.user}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("Closing connection")
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
db = providers.Resource(
DatabaseConnection,
host=config.db.host,
port=config.db.port,
user=config.db.user,
password=config.db.password,
)
Generator initializer (legacy)
------------------------------
Resource provider can use 2-step generators:
- First step of generator is an initialization phase
- The second is step is a shutdown phase
.. code-block:: python
def init_resource(argument1=..., argument2=...):
resource = SomeResource() # initialization
yield resource
# shutdown
...
class Container(containers.DeclarativeContainer):
resource = providers.Resource(
init_resource,
argument1=...,
argument2=...,
)
Generator initialization phase ends on the first ``yield`` statement. You can return a
resource object using ``yield resource`` like in the example above. Returning of the
object is not mandatory. You can leave ``yield`` statement empty:
.. code-block:: python
def init_resource(argument1=..., argument2=...):
# initialization
...
yield
# shutdown
...
class Container(containers.DeclarativeContainer):
resource = providers.Resource(
init_resource,
argument1=...,
argument2=...,
)
.. note::
Generator initializers are automatically wrapped with ``contextmanager`` or ``asynccontextmanager`` decorator when
provided to a ``Resource`` provider.
Subclass initializer (legacy)
-----------------------------
You can create resource initializer by implementing a subclass of the ``resources.Resource``:
.. code-block:: python
from dependency_injector import resources
class MyResource(resources.Resource):
def init(self, argument1=..., argument2=...) -> SomeResource:
return SomeResource()
def shutdown(self, resource: SomeResource) -> None:
# shutdown
...
class Container(containers.DeclarativeContainer):
resource = providers.Resource(
MyResource,
argument1=...,
argument2=...,
)
Subclass must implement two methods: ``init()`` and ``shutdown()``.
Method ``init()`` receives arguments specified in resource provider.
It performs initialization and returns resource object. Returning of the object
is not mandatory.
Method ``shutdown()`` receives resource object returned from ``init()``. If ``init()``
didn't return an object ``shutdown()`` method will be called anyway with ``None`` as a
first argument.
.. code-block:: python
from dependency_injector import resources
class MyResource(resources.Resource):
def init(self, argument1=..., argument2=...) -> None:
# initialization
...
def shutdown(self, _: None) -> None:
# shutdown
...
.. _resource-provider-wiring-closing:
Scoping Resources using specialized subclasses
----------------------------------------------
You can use specialized subclasses of ``Resource`` provider to initialize and shutdown resources by type.
Allowing for example to only initialize a subgroup of resources.
.. code-block:: python
class ScopedResource(resources.Resource):
pass
def init_service(name) -> Service:
print(f"Init {name}")
yield Service()
print(f"Shutdown {name}")
class Container(containers.DeclarativeContainer):
scoped = ScopedResource(
init_service,
"scoped",
)
generic = providers.Resource(
init_service,
"generic",
)
To initialize resources by type you can use ``init_resources(resource_type)`` and ``shutdown_resources(resource_type)``
methods adding the resource type as an argument:
.. code-block:: python
def main():
container = Container()
container.init_resources(ScopedResource)
# Generates:
# >>> Init scoped
container.shutdown_resources(ScopedResource)
# Generates:
# >>> Shutdown scoped
And to initialize all resources you can use ``init_resources()`` and ``shutdown_resources()`` without arguments:
.. code-block:: python
def main():
container = Container()
container.init_resources()
# Generates:
# >>> Init scoped
# >>> Init generic
container.shutdown_resources()
# Generates:
# >>> Shutdown scoped
# >>> Shutdown generic
It works using the ``traverse()`` method to find all resources of the specified type, selecting all resources
which are instances of the specified type.
Resources, wiring, and per-function execution scope
---------------------------------------------------
You can compound ``Resource`` provider with :ref:`wiring` to implement per-function
execution scope. For doing this you need to use additional ``Closing`` marker from
``wiring`` module.
.. literalinclude:: ../../examples/wiring/flask_resource_closing.py
:language: python
:lines: 3-
:emphasize-lines: 22
Framework initializes and injects the resource into the function. With the ``Closing`` marker
framework calls resource ``shutdown()`` method when function execution is over.
The example above produces next output:
.. code-block:: bash
Init service
Shutdown service
127.0.0.1 - - [29/Oct/2020 22:39:40] "GET / HTTP/1.1" 200 -
Init service
Shutdown service
127.0.0.1 - - [29/Oct/2020 22:39:41] "GET / HTTP/1.1" 200 -
Init service
Shutdown service
127.0.0.1 - - [29/Oct/2020 22:39:41] "GET / HTTP/1.1" 200 -
.. _resource-async-initializers:
Asynchronous initializers
-------------------------
When you write an asynchronous application, you might need to initialize resources asynchronously. Resource
provider supports asynchronous initialization and shutdown.
Asynchronous function initializer:
.. code-block:: python
async def init_async_resource(argument1=..., argument2=...):
return await connect()
class Container(containers.DeclarativeContainer):
resource = providers.Resource(
init_resource,
argument1=...,
argument2=...,
)
Asynchronous Context Manager initializer:
.. code-block:: python
@asynccontextmanager
async def init_async_resource(argument1=..., argument2=...):
connection = await connect()
yield connection
await connection.close()
class Container(containers.DeclarativeContainer):
resource = providers.Resource(
init_async_resource,
argument1=...,
argument2=...,
)
Asynchronous subclass initializer:
.. code-block:: python
from dependency_injector import resources
class AsyncConnection(resources.AsyncResource):
async def init(self, argument1=..., argument2=...):
yield await connect()
async def shutdown(self, connection):
await connection.close()
class Container(containers.DeclarativeContainer):
resource = providers.Resource(
AsyncConnection,
argument1=...,
argument2=...,
)
When you use resource provider with asynchronous initializer you need to call its ``__call__()``,
``init()``, and ``shutdown()`` methods asynchronously:
.. code-block:: python
import asyncio
class Container(containers.DeclarativeContainer):
connection = providers.Resource(init_async_connection)
async def main():
container = Container()
connection = await container.connection()
connection = await container.connection.init()
connection = await container.connection.shutdown()
if __name__ == "__main__":
asyncio.run(main())
Container ``init_resources()`` and ``shutdown_resources()`` methods should be used asynchronously if there is
at least one asynchronous resource provider:
.. code-block:: python
import asyncio
class Container(containers.DeclarativeContainer):
connection1 = providers.Resource(init_async_connection)
connection2 = providers.Resource(init_sync_connection)
async def main():
container = Container()
await container.init_resources()
await container.shutdown_resources()
if __name__ == "__main__":
asyncio.run(main())
See also:
- Provider :ref:`async-injections`
- Wiring :ref:`async-injections-wiring`
- :ref:`fastapi-redis-example`
ASGI Lifespan Protocol Support
------------------------------
The :mod:`dependency_injector.ext.starlette` module provides a :class:`~dependency_injector.ext.starlette.Lifespan`
class that integrates resource providers with ASGI applications using the `Lifespan Protocol`_. This allows resources to
be automatically initialized at application startup and properly shut down when the application stops.
.. code-block:: python
from contextlib import asynccontextmanager
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, inject
from dependency_injector.ext.starlette import Lifespan
from fastapi import FastAPI, Request, Depends, APIRouter
class Connection: ...
@asynccontextmanager
async def init_database():
print("opening database connection")
yield Connection()
print("closing database connection")
router = APIRouter()
@router.get("/")
@inject
async def index(request: Request, db: Connection = Depends(Provide["db"])):
# use the database connection here
return "OK!"
class Container(containers.DeclarativeContainer):
__self__ = providers.Self()
db = providers.Resource(init_database)
lifespan = providers.Singleton(Lifespan, __self__)
app = providers.Singleton(FastAPI, lifespan=lifespan)
_include_router = providers.Resource(
app.provided.include_router.call(),
router,
)
if __name__ == "__main__":
import uvicorn
container = Container()
app = container.app()
uvicorn.run(app, host="localhost", port=8000)
.. _Lifespan Protocol: https://asgi.readthedocs.io/en/latest/specs/lifespan.html
.. disqus::

View File

@ -30,7 +30,4 @@ When a ``Selector`` provider is called, it gets a ``selector`` value and delegat
the provider with a matching name. The ``selector`` callable works as a switch: when the returned the provider with a matching name. The ``selector`` callable works as a switch: when the returned
value is changed the ``Selector`` provider will delegate the work to another provider. value is changed the ``Selector`` provider will delegate the work to another provider.
.. seealso::
:ref:`aggregate-provider` to inject a group of providers.
.. disqus:: .. disqus::

View File

@ -1,5 +1,3 @@
.. _singleton-provider:
Singleton provider Singleton provider
================== ==================
@ -20,12 +18,13 @@ returns it on the rest of the calls.
:language: python :language: python
:lines: 3- :lines: 3-
``Singleton`` provider handles dependencies injection the same way like a :ref:`factory-provider`. ``Singleton`` provider handles an injection of the dependencies the same way like a
:ref:`factory-provider`.
.. note:: .. note::
``Singleton`` provider makes dependencies injection only when it creates an object. When an object ``Singleton`` provider does dependencies injection only when creates the object. When the object
is created and memorized ``Singleton`` provider just returns it without applying injections. is created and memorized ``Singleton`` provider just returns it without applying the injections.
Specialization of the provided type and abstract singletons work the same like like for the Specialization of the provided type and abstract singletons work the same like like for the
factories: factories:
@ -55,38 +54,6 @@ provider.
Resetting of the memorized object clears the reference to it. Further object's lifecycle is Resetting of the memorized object clears the reference to it. Further object's lifecycle is
managed by the garbage collector. managed by the garbage collector.
You can use ``.reset()`` method with a context manager. Memorized instance will be reset on
both entering and exiting a context.
.. literalinclude:: ../../examples/providers/singleton_resetting_with.py
:language: python
:lines: 3-
:emphasize-lines: 18-19
Context manager ``.reset()`` returns resetting singleton provider. You can use it for aliasing.
.. code-block:: python
with container.user_service.reset() as user_service:
...
Method ``.reset()`` resets only current provider. To reset all dependent singleton providers
call ``.full_reset()`` method.
.. literalinclude:: ../../examples/providers/singleton_full_resetting.py
:language: python
:lines: 3-
:emphasize-lines: 25
Method ``.full_reset()`` supports context manager interface like ``.reset()`` does.
.. code-block:: python
with container.user_service.full_reset() as user_service:
...
See also: :ref:`reset-container-singletons`.
Using singleton with multiple threads Using singleton with multiple threads
------------------------------------- -------------------------------------

View File

@ -30,7 +30,7 @@ IDE.
provider = providers.Factory(Cat) provider = providers.Factory(Cat)
if __name__ == "__main__": if __name__ == '__main__':
animal = provider() # mypy knows that animal is of type "Cat" animal = provider() # mypy knows that animal is of type "Cat"
@ -54,7 +54,5 @@ function or method.
provider: providers.Provider[Animal] = providers.Factory(Cat) provider: providers.Provider[Animal] = providers.Factory(Cat)
if __name__ == "__main__": if __name__ == '__main__':
animal = provider() # mypy knows that animal is of type "Animal" animal = provider() # mypy knows that animal is of type "Animal"
.. disqus::

View File

@ -1,7 +0,0 @@
.. list-table::
:class: no-border
:align: left
* - Sponsor the project on GitHub:
- .. raw:: html
:file: _static/sponsor.html

View File

@ -21,7 +21,7 @@ Start from the scratch or jump to the section:
:backlinks: none :backlinks: none
You can find complete project on the You can find complete project on the
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/aiohttp>`_. `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/giphynav-aiohttp>`_.
What are we going to build? What are we going to build?
--------------------------- ---------------------------
@ -88,18 +88,18 @@ Prepare the environment
Let's create the environment for the project. Let's create the environment for the project.
First we need to create a project folder: First we need to create a project folder and the virtual environment:
.. code-block:: bash .. code-block:: bash
mkdir giphynav-aiohttp-tutorial mkdir giphynav-aiohttp-tutorial
cd giphynav-aiohttp-tutorial cd giphynav-aiohttp-tutorial
python3 -m venv venv
Now let's create and activate virtual environment: Now let's activate the virtual environment:
.. code-block:: bash .. code-block:: bash
python3 -m venv venv
. venv/bin/activate . venv/bin/activate
Environment is ready and now we're going to create the layout of the project. Environment is ready and now we're going to create the layout of the project.
@ -116,7 +116,7 @@ Initial project layout::
│ ├── __init__.py │ ├── __init__.py
│ ├── application.py │ ├── application.py
│ ├── containers.py │ ├── containers.py
│ └── handlers.py │ └── views.py
├── venv/ ├── venv/
└── requirements.txt └── requirements.txt
@ -127,6 +127,8 @@ Now it's time to install the project requirements. We will use next packages:
- ``dependency-injector`` - the dependency injection framework - ``dependency-injector`` - the dependency injection framework
- ``aiohttp`` - the web framework - ``aiohttp`` - the web framework
- ``aiohttp-devtools`` - the helper library that will provide a development server with live
reloading
- ``pyyaml`` - the YAML files parsing library, used for the reading of the configuration files - ``pyyaml`` - the YAML files parsing library, used for the reading of the configuration files
- ``pytest-aiohttp`` - the helper library for the testing of the ``aiohttp`` application - ``pytest-aiohttp`` - the helper library for the testing of the ``aiohttp`` application
- ``pytest-cov`` - the helper library for measuring the test coverage - ``pytest-cov`` - the helper library for measuring the test coverage
@ -137,6 +139,7 @@ Put next lines into the ``requirements.txt`` file:
dependency-injector dependency-injector
aiohttp aiohttp
aiohttp-devtools
pyyaml pyyaml
pytest-aiohttp pytest-aiohttp
pytest-cov pytest-cov
@ -161,51 +164,60 @@ The requirements are setup. Now we will build a minimal application.
Minimal application Minimal application
------------------- -------------------
In this section we will build a minimal application. It will have an endpoint that In this section we will build a minimal application. It will have an endpoint that we can call.
will answer our requests in json format. There will be no payload for now. The endpoint will answer in the right format and will have no data.
Edit ``handlers.py``: Edit ``views.py``:
.. code-block:: python .. code-block:: python
"""Handlers module.""" """Views module."""
from aiohttp import web from aiohttp import web
async def index(request: web.Request) -> web.Response: async def index(request: web.Request) -> web.Response:
query = request.query.get("query", "Dependency Injector") query = request.query.get('query', 'Dependency Injector')
limit = int(request.query.get("limit", 10)) limit = int(request.query.get('limit', 10))
gifs = [] gifs = []
return web.json_response( return web.json_response(
{ {
"query": query, 'query': query,
"limit": limit, 'limit': limit,
"gifs": gifs, 'gifs': gifs,
}, },
) )
Now let's create a container. Container will keep all of the application components and their dependencies. 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.
Edit ``containers.py``: Put next into the ``containers.py``:
.. code-block:: python .. code-block:: python
"""Containers module.""" """Application containers module."""
from dependency_injector import containers from dependency_injector import containers
from dependency_injector.ext import aiohttp
from aiohttp import web
from . import views
class Container(containers.DeclarativeContainer): class ApplicationContainer(containers.DeclarativeContainer):
... """Application container."""
Container is empty for now. We will add the providers in the following sections. app = aiohttp.Application(web.Application)
Finally we need to create ``aiohttp`` application factory. It will create and configure container index_view = aiohttp.View(views.index)
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. 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.
Put next into the ``application.py``: Put next into the ``application.py``:
@ -215,24 +227,27 @@ Put next into the ``application.py``:
from aiohttp import web from aiohttp import web
from .containers import Container from .containers import ApplicationContainer
from . import handlers
def create_app() -> web.Application: def create_app():
container = Container() """Create and return aiohttp application."""
container = ApplicationContainer()
app = web.Application() app: web.Application = container.app()
app.container = container app.container = container
app.add_routes([ app.add_routes([
web.get("/", handlers.index), web.get('/', container.index_view.as_view()),
]) ])
return app return app
.. note::
if __name__ == "__main__": Container is the first object in the application.
app = create_app()
web.run_app(app) The container is used to create all other objects.
Now we're ready to run our application Now we're ready to run our application
@ -240,30 +255,30 @@ Do next in the terminal:
.. code-block:: bash .. code-block:: bash
python -m giphynavigator.application adev runserver giphynavigator/application.py --livereload
The output should be something like: The output should be something like:
.. code-block:: bash .. code-block:: bash
======== Running on http://0.0.0.0:8080 ======== [18:52:59] Starting aux server at http://localhost:8001 ◆
(Press CTRL+C to quit) [18:52:59] Starting dev server at http://localhost:8000 ●
Let's check that it works. Open another terminal session and use ``httpie``: Let's use ``httpie`` to check that it works:
.. code-block:: bash .. code-block:: bash
http http://0.0.0.0:8080/ http http://127.0.0.1:8000/
You should see: You should see:
.. code-block:: http .. code-block:: json
HTTP/1.1 200 OK HTTP/1.1 200 OK
Content-Length: 844 Content-Length: 844
Content-Type: application/json; charset=utf-8 Content-Type: application/json; charset=utf-8
Date: Wed, 29 Jul 2020 21:01:50 GMT Date: Wed, 29 Jul 2020 21:01:50 GMT
Server: Python/3.10 aiohttp/3.6.2 Server: Python/3.8 aiohttp/3.6.2
{ {
"gifs": [], "gifs": [],
@ -291,7 +306,7 @@ Create ``giphy.py`` module in the ``giphynavigator`` package:
│ ├── application.py │ ├── application.py
│ ├── containers.py │ ├── containers.py
│ ├── giphy.py │ ├── giphy.py
│ └── handlers.py │ └── views.py
├── venv/ ├── venv/
└── requirements.txt └── requirements.txt
@ -306,7 +321,7 @@ and put next into it:
class GiphyClient: class GiphyClient:
API_URL = "https://api.giphy.com/v1" API_URL = 'http://api.giphy.com/v1'
def __init__(self, api_key, timeout): def __init__(self, api_key, timeout):
self._api_key = api_key self._api_key = api_key
@ -314,11 +329,11 @@ and put next into it:
async def search(self, query, limit): async def search(self, query, limit):
"""Make search API call and return result.""" """Make search API call and return result."""
url = f"{self.API_URL}/gifs/search" url = f'{self.API_URL}/gifs/search'
params = { params = {
"q": query, 'q': query,
"api_key": self._api_key, 'api_key': self._api_key,
"limit": limit, 'limit': limit,
} }
async with ClientSession(timeout=self._timeout) as session: async with ClientSession(timeout=self._timeout) as session:
async with session.get(url, params=params) as response: async with session.get(url, params=params) as response:
@ -330,26 +345,29 @@ Now we need to add ``GiphyClient`` into the container. The ``GiphyClient`` has t
that have to be injected: the API key and the request timeout. We will need to use two more that have to be injected: the API key and the request timeout. We will need to use two more
providers from the ``dependency_injector.providers`` module: providers from the ``dependency_injector.providers`` module:
- ``Factory`` provider. It will create a ``GiphyClient`` client. - ``Factory`` provider that will create the ``GiphyClient`` client.
- ``Configuration`` provider. It will provide an API key and a request timeout for the ``GiphyClient`` - ``Configuration`` provider that will provide the API key and the request timeout.
client. We will specify the location of the configuration file. The configuration provider will parse
the configuration file when we create a container instance.
Edit ``containers.py``: Edit ``containers.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 3-5,10-16 :emphasize-lines: 3,7,15,17-21
"""Containers module.""" """Application containers module."""
from dependency_injector import containers, providers from dependency_injector import containers, providers
from dependency_injector.ext import aiohttp
from aiohttp import web
from . import giphy from . import giphy, views
class Container(containers.DeclarativeContainer): class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
config = providers.Configuration(yaml_files=["config.yml"]) app = aiohttp.Application(web.Application)
config = providers.Configuration()
giphy_client = providers.Factory( giphy_client = providers.Factory(
giphy.GiphyClient, giphy.GiphyClient,
@ -357,8 +375,20 @@ Edit ``containers.py``:
timeout=config.giphy.request_timeout, timeout=config.giphy.request_timeout,
) )
Now let's add the configuration file. We will use YAML. Create an empty file ``config.yml`` in index_view = aiohttp.View(views.index)
the root root of the project:
.. note::
We have used the configuration value before it was defined. That's the principle how the
``Configuration`` provider works.
Use first, define later.
Now let's add the configuration file.
We will use YAML.
Create an empty file ``config.yml`` in the root root of the project:
.. code-block:: bash .. code-block:: bash
:emphasize-lines: 9 :emphasize-lines: 9
@ -369,7 +399,7 @@ the root root of the project:
│ ├── application.py │ ├── application.py
│ ├── containers.py │ ├── containers.py
│ ├── giphy.py │ ├── giphy.py
│ └── handlers.py │ └── views.py
├── venv/ ├── venv/
├── config.yml ├── config.yml
└── requirements.txt └── requirements.txt
@ -381,39 +411,40 @@ and put next into it:
giphy: giphy:
request_timeout: 10 request_timeout: 10
We will use an environment variable ``GIPHY_API_KEY`` to provide the API key.
We will use the ``GIPHY_API_KEY`` environment variable to provide the API key. Lets edit Now we need to edit ``create_app()`` to make two things when application starts:
``create_app()`` to fetch the key value from it.
- Load the configuration file the ``config.yml``.
- Load the API key from the ``GIPHY_API_KEY`` environment variable.
Edit ``application.py``: Edit ``application.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 11 :emphasize-lines: 11-12
"""Application module.""" """Application module."""
from aiohttp import web from aiohttp import web
from .containers import Container from .containers import ApplicationContainer
from . import handlers
def create_app() -> web.Application: def create_app():
container = Container() """Create and return aiohttp application."""
container.config.giphy.api_key.from_env("GIPHY_API_KEY") container = ApplicationContainer()
container.config.from_yaml('config.yml')
container.config.giphy.api_key.from_env('GIPHY_API_KEY')
app = web.Application() app: web.Application = container.app()
app.container = container app.container = container
app.add_routes([ app.add_routes([
web.get("/", handlers.index), web.get('/', container.index_view.as_view()),
]) ])
return app return app
if __name__ == "__main__":
app = create_app()
web.run_app(app)
Now we need to create an API key and set it to the environment variable. Now we need to create an API key and set it to the environment variable.
As for now, dont worry, just take this one: As for now, dont worry, just take this one:
@ -442,7 +473,7 @@ Now it's time to add the ``SearchService``. It will:
Create ``services.py`` module in the ``giphynavigator`` package: Create ``services.py`` module in the ``giphynavigator`` package:
.. code-block:: bash .. code-block:: bash
:emphasize-lines: 8 :emphasize-lines: 7
./ ./
├── giphynavigator/ ├── giphynavigator/
@ -450,10 +481,9 @@ Create ``services.py`` module in the ``giphynavigator`` package:
│ ├── application.py │ ├── application.py
│ ├── containers.py │ ├── containers.py
│ ├── giphy.py │ ├── giphy.py
│ ├── handlers.py │ ├── services.py
│ └── services.py │ └── views.py
├── venv/ ├── venv/
├── config.yml
└── requirements.txt └── requirements.txt
and put next into it: and put next into it:
@ -477,26 +507,31 @@ and put next into it:
result = await self._giphy_client.search(query, limit) result = await self._giphy_client.search(query, limit)
return [{"url": gif["url"]} for gif in result["data"]] return [{'url': gif['url']} for gif in result['data']]
The ``SearchService`` has a dependency on the ``GiphyClient``. This dependency will be The ``SearchService`` has a dependency on the ``GiphyClient``. This dependency will be injected.
injected when we add ``SearchService`` to the container. Let's add ``SearchService`` to the container.
Edit ``containers.py``: Edit ``containers.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 5,18-21 :emphasize-lines: 7,23-26
"""Containers module.""" """Application containers module."""
from dependency_injector import containers, providers from dependency_injector import containers, providers
from dependency_injector.ext import aiohttp
from aiohttp import web
from . import giphy, services from . import giphy, services, views
class Container(containers.DeclarativeContainer): class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
config = providers.Configuration(yaml_files=["config.yml"]) app = aiohttp.Application(web.Application)
config = providers.Configuration()
giphy_client = providers.Factory( giphy_client = providers.Factory(
giphy.GiphyClient, giphy.GiphyClient,
@ -509,67 +544,67 @@ Edit ``containers.py``:
giphy_client=giphy_client, giphy_client=giphy_client,
) )
The search service is ready. In next section we're going to put it to work. index_view = aiohttp.View(views.index)
The search service is ready. In the next section we're going to make it work.
Make the search work Make the search work
-------------------- --------------------
Now we are ready to put the search into work. Let's inject ``SearchService`` into Now we are ready to make the search work. Let's use the ``SearchService`` in the ``index`` view.
the ``index`` handler. We will use :ref:`wiring` feature.
Edit ``handlers.py``: Edit ``views.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 4-7,10-14,18 :emphasize-lines: 5,8-11,15
"""Handlers module.""" """Views module."""
from aiohttp import web from aiohttp import web
from dependency_injector.wiring import Provide, inject
from .services import SearchService from .services import SearchService
from .containers import Container
@inject
async def index( async def index(
request: web.Request, request: web.Request,
search_service: SearchService = Provide[Container.search_service], search_service: SearchService,
) -> web.Response: ) -> web.Response:
query = request.query.get("query", "Dependency Injector") query = request.query.get('query', 'Dependency Injector')
limit = int(request.query.get("limit", 10)) limit = int(request.query.get('limit', 10))
gifs = await search_service.search(query, limit) gifs = await search_service.search(query, limit)
return web.json_response( return web.json_response(
{ {
"query": query, 'query': query,
"limit": limit, 'limit': limit,
"gifs": gifs, 'gifs': gifs,
}, },
) )
To make the injection work we need to wire the container with the ``handlers`` module. Now let's inject the ``SearchService`` dependency into the ``index`` view.
Let's configure the container to automatically make wiring with the ``handlers`` module when we
create a container instance.
Edit ``containers.py``: Edit ``containers.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 10 :emphasize-lines: 28-31
"""Containers module.""" """Application containers module."""
from dependency_injector import containers, providers from dependency_injector import containers, providers
from dependency_injector.ext import aiohttp
from aiohttp import web
from . import giphy, services from . import giphy, services, views
class Container(containers.DeclarativeContainer): class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
wiring_config = containers.WiringConfiguration(modules=[".handlers"]) app = aiohttp.Application(web.Application)
config = providers.Configuration(yaml_files=["config.yml"]) config = providers.Configuration()
giphy_client = providers.Factory( giphy_client = providers.Factory(
giphy.GiphyClient, giphy.GiphyClient,
@ -582,47 +617,52 @@ Edit ``containers.py``:
giphy_client=giphy_client, giphy_client=giphy_client,
) )
Make sure the app is running: index_view = aiohttp.View(
views.index,
search_service=search_service,
)
Make sure the app is running or use:
.. code-block:: bash .. code-block:: bash
python -m giphynavigator.application adev runserver giphynavigator/application.py --livereload
and make a request to the API in the terminal: and make a request to the API in the terminal:
.. code-block:: bash .. code-block:: bash
http http://0.0.0.0:8080/ query=="wow,it works" limit==5 http http://localhost:8000/ query=="wow,it works" limit==5
You should see: You should see:
.. code-block:: http .. code-block:: json
HTTP/1.1 200 OK HTTP/1.1 200 OK
Content-Length: 492 Content-Length: 850
Content-Type: application/json; charset=utf-8 Content-Type: application/json; charset=utf-8
Date: Fri, 09 Oct 2020 01:35:48 GMT Date: Wed, 29 Jul 2020 22:22:55 GMT
Server: Python/3.10 aiohttp/3.6.2 Server: Python/3.8 aiohttp/3.6.2
{ {
"gifs": [ "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/discoverychannel-nugget-gold-rush-rick-ness-KGGPIlnC4hr4u2s3pY"
}, },
{ {
"url": "https://giphy.com/gifs/soulpancake-wow-work-xUe4HVXTPi0wQ2OAJC" "url": "https://giphy.com/gifs/primevideoin-ll1hyBS2IrUPLE0E71"
}, },
{ {
"url": "https://giphy.com/gifs/readingrainbow-teamwork-levar-burton-reading-rainbow-3o7qE1EaTWLQGDSabK" "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"
},
], ],
"limit": 5, "limit": 10,
"query": "wow,it works" "query": "wow,it works"
} }
@ -633,48 +673,86 @@ The search works!
Make some refactoring Make some refactoring
--------------------- ---------------------
Our ``index`` handler has two hardcoded config values: Our ``index`` view has two hardcoded config values:
- Default search query - Default search query
- Default results limit - Default results limit
Let's make some refactoring. We will move these values to the config. Let's make some refactoring. We will move these values to the config.
Edit ``handlers.py``: Edit ``views.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 14-15,17-18 :emphasize-lines: 11-12,14-15
"""Handlers module.""" """Views module."""
from aiohttp import web from aiohttp import web
from dependency_injector.wiring import Provide, inject
from .services import SearchService from .services import SearchService
from .containers import Container
@inject
async def index( async def index(
request: web.Request, request: web.Request,
search_service: SearchService = Provide[Container.search_service], search_service: SearchService,
default_query: str = Provide[Container.config.default.query], default_query: str,
default_limit: int = Provide[Container.config.default.limit.as_int()], default_limit: int,
) -> web.Response: ) -> web.Response:
query = request.query.get("query", default_query) query = request.query.get('query', default_query)
limit = int(request.query.get("limit", default_limit)) limit = int(request.query.get('limit', default_limit))
gifs = await search_service.search(query, limit) gifs = await search_service.search(query, limit)
return web.json_response( return web.json_response(
{ {
"query": query, 'query': query,
"limit": limit, 'limit': limit,
"gifs": gifs, 'gifs': gifs,
}, },
) )
Let's update the config. 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.
Edit ``config.yml``: Edit ``config.yml``:
@ -683,21 +761,26 @@ Edit ``config.yml``:
giphy: giphy:
request_timeout: 10 request_timeout: 10
default: search:
query: "Dependency Injector" default_query: "Dependency Injector"
limit: 10 default_limit: 10
The refactoring is done. We've made it cleaner - hardcoded values are now moved to the config. 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 Tests
----- -----
In this section we will add some 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/>`_.
Create ``tests.py`` module in the ``giphynavigator`` package: Create ``tests.py`` module in the ``giphynavigator`` package:
.. code-block:: bash .. code-block:: bash
:emphasize-lines: 9 :emphasize-lines: 8
./ ./
├── giphynavigator/ ├── giphynavigator/
@ -705,17 +788,16 @@ Create ``tests.py`` module in the ``giphynavigator`` package:
│ ├── application.py │ ├── application.py
│ ├── containers.py │ ├── containers.py
│ ├── giphy.py │ ├── giphy.py
│ ├── handlers.py
│ ├── services.py │ ├── services.py
│ └── tests.py │ ├── tests.py
│ └── views.py
├── venv/ ├── venv/
├── config.yml
└── requirements.txt └── requirements.txt
and put next into it: and put next into it:
.. code-block:: python .. code-block:: python
:emphasize-lines: 32,59,73 :emphasize-lines: 30,57,71
"""Tests module.""" """Tests module."""
@ -729,9 +811,7 @@ and put next into it:
@pytest.fixture @pytest.fixture
def app(): def app():
app = create_app() return create_app()
yield app
app.container.unwire()
@pytest.fixture @pytest.fixture
@ -742,29 +822,29 @@ and put next into it:
async def test_index(client, app): async def test_index(client, app):
giphy_client_mock = mock.AsyncMock(spec=GiphyClient) giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
giphy_client_mock.search.return_value = { giphy_client_mock.search.return_value = {
"data": [ 'data': [
{"url": "https://giphy.com/gif1.gif"}, {'url': 'https://giphy.com/gif1.gif'},
{"url": "https://giphy.com/gif2.gif"}, {'url': 'https://giphy.com/gif2.gif'},
], ],
} }
with app.container.giphy_client.override(giphy_client_mock): with app.container.giphy_client.override(giphy_client_mock):
response = await client.get( response = await client.get(
"/", '/',
params={ params={
"query": "test", 'query': 'test',
"limit": 10, 'limit': 10,
}, },
) )
assert response.status == 200 assert response.status == 200
data = await response.json() data = await response.json()
assert data == { assert data == {
"query": "test", 'query': 'test',
"limit": 10, 'limit': 10,
"gifs": [ 'gifs': [
{"url": "https://giphy.com/gif1.gif"}, {'url': 'https://giphy.com/gif1.gif'},
{"url": "https://giphy.com/gif2.gif"}, {'url': 'https://giphy.com/gif2.gif'},
], ],
} }
@ -772,30 +852,30 @@ and put next into it:
async def test_index_no_data(client, app): async def test_index_no_data(client, app):
giphy_client_mock = mock.AsyncMock(spec=GiphyClient) giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
giphy_client_mock.search.return_value = { giphy_client_mock.search.return_value = {
"data": [], 'data': [],
} }
with app.container.giphy_client.override(giphy_client_mock): with app.container.giphy_client.override(giphy_client_mock):
response = await client.get("/") response = await client.get('/')
assert response.status == 200 assert response.status == 200
data = await response.json() data = await response.json()
assert data["gifs"] == [] assert data['gifs'] == []
async def test_index_default_params(client, app): async def test_index_default_params(client, app):
giphy_client_mock = mock.AsyncMock(spec=GiphyClient) giphy_client_mock = mock.AsyncMock(spec=GiphyClient)
giphy_client_mock.search.return_value = { giphy_client_mock.search.return_value = {
"data": [], 'data': [],
} }
with app.container.giphy_client.override(giphy_client_mock): with app.container.giphy_client.override(giphy_client_mock):
response = await client.get("/") response = await client.get('/')
assert response.status == 200 assert response.status == 200
data = await response.json() data = await response.json()
assert data["query"] == app.container.config.default.query() assert data['query'] == app.container.config.search.default_query()
assert data["limit"] == app.container.config.default.limit() assert data['limit'] == app.container.config.search.default_limit()
Now let's run it and check the coverage: Now let's run it and check the coverage:
@ -805,26 +885,27 @@ Now let's run it and check the coverage:
You should see: You should see:
.. code-block:: .. code-block:: bash
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
plugins: asyncio-0.16.0, anyio-3.3.4, aiohttp-0.3.0, cov-3.0.0 plugins: cov-2.10.0, aiohttp-0.3.0, asyncio-0.14.0
collected 3 items collected 3 items
giphynavigator/tests.py ... [100%] giphynavigator/tests.py ... [100%]
---------- coverage: platform darwin, python 3.10.0-final-0 ---------- ---------- coverage: platform darwin, python 3.8.3-final-0 -----------
Name Stmts Miss Cover Name Stmts Miss Cover
--------------------------------------------------- ---------------------------------------------------
giphynavigator/__init__.py 0 0 100% giphynavigator/__init__.py 0 0 100%
giphynavigator/application.py 13 2 85% giphynavigator/__main__.py 5 5 0%
giphynavigator/containers.py 7 0 100% giphynavigator/application.py 10 0 100%
giphynavigator/containers.py 10 0 100%
giphynavigator/giphy.py 14 9 36% giphynavigator/giphy.py 14 9 36%
giphynavigator/handlers.py 10 0 100%
giphynavigator/services.py 9 1 89% giphynavigator/services.py 9 1 89%
giphynavigator/tests.py 37 0 100% giphynavigator/tests.py 35 0 100%
giphynavigator/views.py 7 0 100%
--------------------------------------------------- ---------------------------------------------------
TOTAL 90 12 87% TOTAL 90 15 83%
.. note:: .. note::
@ -839,19 +920,45 @@ In this tutorial we've built an ``aiohttp`` REST API application following the d
injection principle. injection principle.
We've used the ``Dependency Injector`` as a dependency injection framework. We've used the ``Dependency Injector`` as a dependency injection framework.
:ref:`containers` and :ref:`providers` helped to specify how to assemble search service and The benefit you get with the ``Dependency Injector`` is the container. It starts to payoff
giphy client. 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:`configuration-provider` helped to deal with reading YAML file and environment variable. .. code-block:: python
We used :ref:`wiring` feature to inject the dependencies into the ``index()`` handler. """Application containers module."""
:ref:`provider-overriding` feature helped in testing.
We kept all the dependencies injected explicitly. This will help when you need to add or from dependency_injector import containers, providers
change something in future. from dependency_injector.ext import aiohttp
from aiohttp import web
You can find complete project on the from . import giphy, services, views
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/aiohttp>`_.
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,
)
What's next? What's next?
@ -859,6 +966,4 @@ What's next?
- Know more about the :ref:`providers` - Know more about the :ref:`providers`
- Go to the :ref:`contents` - Go to the :ref:`contents`
.. include:: ../sponsor.rst
.. disqus:: .. disqus::

View File

@ -18,7 +18,7 @@ In this tutorial we will use:
- Python 3 - Python 3
- Docker - Docker
- Docker Compose - Docker-compose
Start from the scratch or jump to the section: Start from the scratch or jump to the section:
@ -27,7 +27,7 @@ Start from the scratch or jump to the section:
:backlinks: none :backlinks: none
You can find complete project on the You can find complete project on the
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/asyncio-daemon>`_. `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/monitoring-daemon-asyncio>`_.
What are we going to build? What are we going to build?
--------------------------- ---------------------------
@ -42,32 +42,33 @@ response it will log:
- The amount of bytes in the response - The amount of bytes in the response
- The time took to complete the response - The time took to complete the response
.. image:: asyncio-images/diagram.png .. image:: asyncio_images/diagram.png
Prerequisites Prerequisites
------------- -------------
We will use `docker compose <https://docs.docker.com/compose/>`_ in this tutorial. Let's check the versions: We will use `Docker <https://www.docker.com/>`_ and
`docker-compose <https://docs.docker.com/compose/>`_ in this tutorial. Let's check the versions:
.. code-block:: bash .. code-block:: bash
docker --version docker --version
docker compose version docker-compose --version
The output should look something like: The output should look something like:
.. code-block:: bash .. code-block:: bash
Docker version 27.3.1, build ce12230 Docker version 19.03.12, build 48a66213fe
Docker Compose version v2.29.7 docker-compose version 1.26.2, build eefe0d31
.. note:: .. note::
If you don't have ``Docker`` or ``docker compose`` you need to install them before proceeding. If you don't have ``Docker`` or ``docker-compose`` you need to install them before proceeding.
Follow these installation guides: Follow these installation guides:
- `Install Docker <https://docs.docker.com/get-docker/>`_ - `Install Docker <https://docs.docker.com/get-docker/>`_
- `Install docker compose <https://docs.docker.com/compose/install/>`_ - `Install docker-compose <https://docs.docker.com/compose/install/>`_
The prerequisites are satisfied. Let's get started with the project layout. The prerequisites are satisfied. Let's get started with the project layout.
@ -78,8 +79,8 @@ Create the project root folder and set it as a working directory:
.. code-block:: bash .. code-block:: bash
mkdir asyncio-daemon-tutorial mkdir monitoring-daemon-tutorial
cd asyncio-daemon-tutorial cd monitoring-daemon-tutorial
Now we need to create the initial project structure. Create the files and folders following next 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. layout. All files should be empty for now. We will fill them later.
@ -128,13 +129,13 @@ Put next lines into the ``requirements.txt`` file:
pytest-cov pytest-cov
Second, we need to create the ``Dockerfile``. It will describe the daemon's build process and Second, we need to create the ``Dockerfile``. It will describe the daemon's build process and
specify how to run it. We will use ``python:3.13-bookworm`` as a base image. specify how to run it. We will use ``python:3.8-buster`` as a base image.
Put next lines into the ``Dockerfile`` file: Put next lines into the ``Dockerfile`` file:
.. code-block:: bash .. code-block:: bash
FROM python:3.13-bookworm FROM python:3.8-buster
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
@ -154,6 +155,8 @@ Put next lines into the ``docker-compose.yml`` file:
.. code-block:: yaml .. code-block:: yaml
version: "3.7"
services: services:
monitor: monitor:
@ -168,7 +171,7 @@ Run in the terminal:
.. code-block:: bash .. code-block:: bash
docker compose build docker-compose build
The build process may take a couple of minutes. You should see something like this in the end: The build process may take a couple of minutes. You should see something like this in the end:
@ -181,16 +184,16 @@ After the build is done run the container:
.. code-block:: bash .. code-block:: bash
docker compose up docker-compose up
The output should look like: The output should look like:
.. code-block:: bash .. code-block:: bash
Creating network "asyncio-daemon-tutorial_default" with the default driver Creating network "monitoring-daemon-tutorial_default" with the default driver
Creating asyncio-daemon-tutorial_monitor_1 ... done Creating monitoring-daemon-tutorial_monitor_1 ... done
Attaching to asyncio-daemon-tutorial_monitor_1 Attaching to monitoring-daemon-tutorial_monitor_1
asyncio-daemon-tutorial_monitor_1 exited with code 0 monitoring-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``. The environment is ready. The application does not do any work and just exits with a code ``0``.
@ -201,17 +204,17 @@ Logging and configuration
In this section we will configure the logging and configuration file parsing. In this section we will configure the logging and configuration file parsing.
Let's start with the the main part of our application the container. Container will keep all of Let's start with the the main part of our application - the container. Container will keep all of
the application components and their dependencies. the application components and their dependencies.
First two components that we're going to add are the configuration provider and the resource provider First two components that we're going to add are the config object and the provider for
for configuring the logging. configuring the logging.
Put next lines into the ``containers.py`` file: Put next lines into the ``containers.py`` file:
.. code-block:: python .. code-block:: python
"""Containers module.""" """Application containers module."""
import logging import logging
import sys import sys
@ -219,18 +222,28 @@ Put next lines into the ``containers.py`` file:
from dependency_injector import containers, providers from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer): class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
config = providers.Configuration(yaml_files=["config.yml"]) config = providers.Configuration()
logging = providers.Resource( configure_logging = providers.Callable(
logging.basicConfig, logging.basicConfig,
stream=sys.stdout, stream=sys.stdout,
level=config.log.level, level=config.log.level,
format=config.log.format, format=config.log.format,
) )
The configuration file will keep the logging settings. Put next lines into the ``config.yml`` file: .. note::
We have used the configuration value before it was defined. That's the principle how the
``Configuration`` provider works.
Use first, define later.
The configuration file will keep the logging settings.
Put next lines into the ``config.yml`` file:
.. code-block:: yaml .. code-block:: yaml
@ -238,35 +251,37 @@ The configuration file will keep the logging settings. Put next lines into the `
level: "INFO" level: "INFO"
format: "[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s" format: "[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s"
Now let's create the function that will run our daemon. It's traditionally called ``main()``. Now let's create the function that will run our daemon. It's traditionally called
The ``main()`` function will start the dispatcher, but we will keep it empty for now. ``main()``. The ``main()`` function will create the container. Then it will use the container
We will create the container instance before calling ``main()`` in ``if __name__ == "__main__"``. to parse the ``config.yml`` file and call the logging configuration provider.
Container instance will parse ``config.yml`` and then we will call the logging configuration provider.
Put next lines into the ``__main__.py`` file: Put next lines into the ``__main__.py`` file:
.. code-block:: python .. code-block:: python
"""Main module.""" """Main module."""
from .containers import Container from .containers import ApplicationContainer
def main() -> None: def main() -> None:
... """Run the application."""
container = ApplicationContainer()
container.config.from_yaml('config.yml')
container.configure_logging()
if __name__ == "__main__": if __name__ == '__main__':
container = Container() main()
container.init_resources()
main()
.. note:: .. note::
Container is the first object in the application. Container is the first object in the application.
Logging and configuration parsing part is done. In next section we will create the monitoring 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
checks dispatcher. checks dispatcher.
Dispatcher Dispatcher
@ -278,7 +293,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 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. create different monitors by subclassing it and implementing the ``check()`` method.
.. image:: asyncio-images/classes-01.png .. image:: asyncio_images/class_1.png
Let's create dispatcher and the monitor base classes. Let's create dispatcher and the monitor base classes.
@ -321,7 +336,7 @@ and next into the ``dispatcher.py``:
.. code-block:: python .. code-block:: python
"""Dispatcher module.""" """"Dispatcher module."""
import asyncio import asyncio
import logging import logging
@ -344,7 +359,7 @@ and next into the ``dispatcher.py``:
asyncio.run(self.start()) asyncio.run(self.start())
async def start(self) -> None: async def start(self) -> None:
self._logger.info("Starting up") self._logger.info('Starting up')
for monitor in self._monitors: for monitor in self._monitors:
self._monitor_tasks.append( self._monitor_tasks.append(
@ -364,11 +379,10 @@ and next into the ``dispatcher.py``:
self._stopping = True self._stopping = True
self._logger.info("Shutting down") self._logger.info('Shutting down')
for task, monitor in zip(self._monitor_tasks, self._monitors): for task, monitor in zip(self._monitor_tasks, self._monitors):
task.cancel() task.cancel()
self._monitor_tasks.clear() self._logger.info('Shutdown finished successfully')
self._logger.info("Shutdown finished successfully")
@staticmethod @staticmethod
async def _run_monitor(monitor: Monitor) -> None: async def _run_monitor(monitor: Monitor) -> None:
@ -384,7 +398,7 @@ and next into the ``dispatcher.py``:
except asyncio.CancelledError: except asyncio.CancelledError:
break break
except Exception: except Exception:
monitor.logger.exception("Error executing monitor check") monitor.logger.exception('Error executing monitor check')
await asyncio.sleep(_until_next(last=time_start)) await asyncio.sleep(_until_next(last=time_start))
@ -393,9 +407,9 @@ Now we need to add the dispatcher to the container.
Edit ``containers.py``: Edit ``containers.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 8,22-27 :emphasize-lines: 8,23-28
"""Containers module.""" """Application containers module."""
import logging import logging
import sys import sys
@ -405,11 +419,12 @@ Edit ``containers.py``:
from . import dispatcher from . import dispatcher
class Container(containers.DeclarativeContainer): class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
config = providers.Configuration(yaml_files=["config.yml"]) config = providers.Configuration()
logging = providers.Resource( configure_logging = providers.Callable(
logging.basicConfig, logging.basicConfig,
stream=sys.stdout, stream=sys.stdout,
level=config.log.level, level=config.log.level,
@ -423,33 +438,35 @@ Edit ``containers.py``:
), ),
) )
At the last we will inject dispatcher into the ``main()`` function .. note::
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``: Edit ``__main__.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 3-5,9-11,17 :emphasize-lines: 13-14
"""Main module.""" """Main module."""
from dependency_injector.wiring import Provide, inject from .containers import ApplicationContainer
from .dispatcher import Dispatcher
from .containers import Container
@inject def main() -> None:
def main(dispatcher: Dispatcher = Provide[Container.dispatcher]) -> None: """Run the application."""
container = ApplicationContainer()
container.config.from_yaml('config.yml')
container.configure_logging()
dispatcher = container.dispatcher()
dispatcher.run() dispatcher.run()
if __name__ == "__main__": if __name__ == '__main__':
container = Container()
container.init_resources()
container.wire(modules=[__name__])
main() main()
Finally let's start the daemon to check that all works. Finally let's start the daemon to check that all works.
@ -458,28 +475,28 @@ Run in the terminal:
.. code-block:: bash .. code-block:: bash
docker compose up docker-compose up
The output should look like: The output should look like:
.. code-block:: bash .. code-block:: bash
Starting asyncio-daemon-tutorial_monitor_1 ... done Starting monitoring-daemon-tutorial_monitor_1 ... done
Attaching to asyncio-daemon-tutorial_monitor_1 Attaching to monitoring-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,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]: Shutting down
monitor_1 | [2020-08-08 16:12:35,774] [INFO] [Dispatcher]: Shutdown finished successfully monitor_1 | [2020-08-08 16:12:35,774] [INFO] [Dispatcher]: Shutdown finished successfully
asyncio-daemon-tutorial_monitor_1 exited with code 0 monitoring-daemon-tutorial_monitor_1 exited with code 0
Everything works properly. Dispatcher starts up and exits because there are no monitoring tasks. 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 next section will will By the end of this section we have the application skeleton ready. In the next section will will
add first monitoring task. add first monitoring task.
Example.com monitor Example.com monitor
------------------- -------------------
In this section we will add a monitoring task that will check the availability of the In this section we will add the monitoring task that will check the availability of the
`http://example.com <http://example.com>`_. `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 We will start from the extending of our class model with a new type of the monitoring check, the
@ -489,9 +506,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 will send the HTTP request to the specified URL. The http request sending will be delegated to
the ``HttpClient``. the ``HttpClient``.
.. image:: asyncio-images/classes-02.png .. image:: asyncio_images/class_2.png
First we need to create the ``HttpClient``. First, we need to create the ``HttpClient``.
Create ``http.py`` in the ``monitoringdaemon`` package: Create ``http.py`` in the ``monitoringdaemon`` package:
@ -532,9 +549,9 @@ Now we need to add the ``HttpClient`` to the container.
Edit ``containers.py``: Edit ``containers.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 8,22 :emphasize-lines: 8, 23
"""Containers module.""" """Application containers module."""
import logging import logging
import sys import sys
@ -544,11 +561,12 @@ Edit ``containers.py``:
from . import http, dispatcher from . import http, dispatcher
class Container(containers.DeclarativeContainer): class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
config = providers.Configuration(yaml_files=["config.yml"]) config = providers.Configuration()
logging = providers.Resource( configure_logging = providers.Callable(
logging.basicConfig, logging.basicConfig,
stream=sys.stdout, stream=sys.stdout,
level=config.log.level, level=config.log.level,
@ -569,7 +587,7 @@ Now we're ready to add the ``HttpMonitor``. We will add it to the ``monitors`` m
Edit ``monitors.py``: Edit ``monitors.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 4-7,20-56 :emphasize-lines: 4-5,7,20-56
"""Monitors module.""" """Monitors module."""
@ -598,10 +616,10 @@ Edit ``monitors.py``:
options: Dict[str, Any], options: Dict[str, Any],
) -> None: ) -> None:
self._client = http_client self._client = http_client
self._method = options.pop("method") self._method = options.pop('method')
self._url = options.pop("url") self._url = options.pop('url')
self._timeout = options.pop("timeout") self._timeout = options.pop('timeout')
super().__init__(check_every=options.pop("check_every")) super().__init__(check_every=options.pop('check_every'))
async def check(self) -> None: async def check(self) -> None:
time_start = time.time() time_start = time.time()
@ -616,11 +634,11 @@ Edit ``monitors.py``:
time_took = time_end - time_start time_took = time_end - time_start
self.logger.info( self.logger.info(
"Check\n" 'Check\n'
" %s %s\n" ' %s %s\n'
" response code: %s\n" ' response code: %s\n'
" content length: %s\n" ' content length: %s\n'
" request took: %s seconds", ' request took: %s seconds\n',
self._method, self._method,
self._url, self._url,
response.status, response.status,
@ -637,9 +655,9 @@ We make two changes in the container:
Edit ``containers.py``: Edit ``containers.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 8,24-28,33 :emphasize-lines: 8,25-29,34
"""Containers module.""" """Application containers module."""
import logging import logging
import sys import sys
@ -649,11 +667,12 @@ Edit ``containers.py``:
from . import http, monitors, dispatcher from . import http, monitors, dispatcher
class Container(containers.DeclarativeContainer): class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
config = providers.Configuration(yaml_files=["config.yml"]) config = providers.Configuration()
logging = providers.Resource( configure_logging = providers.Callable(
logging.basicConfig, logging.basicConfig,
stream=sys.stdout, stream=sys.stdout,
level=config.log.level, level=config.log.level,
@ -702,20 +721,21 @@ Run in the terminal:
.. code-block:: bash .. code-block:: bash
docker compose up docker-compose up
You should see: You should see:
.. code-block:: bash .. code-block:: bash
Starting asyncio-daemon-tutorial_monitor_1 ... done Starting monitoring-daemon-tutorial_monitor_1 ... done
Attaching to asyncio-daemon-tutorial_monitor_1 Attaching to monitoring-daemon-tutorial_monitor_1
monitor_1 | [2020-08-08 17:06:41,965] [INFO] [Dispatcher]: Starting up 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 | [2020-08-08 17:06:42,033] [INFO] [HttpMonitor]: Check
monitor_1 | GET http://example.com monitor_1 | GET http://example.com
monitor_1 | response code: 200 monitor_1 | response code: 200
monitor_1 | content length: 648 monitor_1 | content length: 648
monitor_1 | request took: 0.067 seconds monitor_1 | request took: 0.067 seconds
monitor_1 |
monitor_1 | [2020-08-08 17:06:47,040] [INFO] [HttpMonitor]: Check monitor_1 | [2020-08-08 17:06:47,040] [INFO] [HttpMonitor]: Check
monitor_1 | GET http://example.com monitor_1 | GET http://example.com
monitor_1 | response code: 200 monitor_1 | response code: 200
@ -724,21 +744,21 @@ You should see:
Our daemon can monitor `http://example.com <http://example.com>`_ availability. Our daemon can monitor `http://example.com <http://example.com>`_ availability.
Let's add a monitor for the `https://httpbin.org <https://httpbin.org>`_. Let's add the monitor for the `http://httpbin.org <http://httpbin.org>`_.
Httpbin.org monitor Httpbin.org monitor
------------------- -------------------
Adding of a monitor for the `https://httpbin.org <https://httpbin.org>`_ will be much Adding of the monitor for the `httpbin.org`_ will be much easier because we have all the
easier because we have all the components ready. We just need to create a new provider components ready. We just need to create a new provider in the container and update the
in the container and update the configuration. configuration.
Edit ``containers.py``: Edit ``containers.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 30-34,40 :emphasize-lines: 31-35,41
"""Containers module.""" """Application containers module."""
import logging import logging
import sys import sys
@ -748,11 +768,12 @@ Edit ``containers.py``:
from . import http, monitors, dispatcher from . import http, monitors, dispatcher
class Container(containers.DeclarativeContainer): class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
config = providers.Configuration(yaml_files=["config.yml"]) config = providers.Configuration()
logging = providers.Resource( configure_logging = providers.Callable(
logging.basicConfig, logging.basicConfig,
stream=sys.stdout, stream=sys.stdout,
level=config.log.level, level=config.log.level,
@ -810,30 +831,33 @@ Run in the terminal:
.. code-block:: bash .. code-block:: bash
docker compose up docker-compose up
You should see: You should see:
.. code-block:: bash .. code-block:: bash
Starting asyncio-daemon-tutorial_monitor_1 ... done Starting monitoring-daemon-tutorial_monitor_1 ... done
Attaching to asyncio-daemon-tutorial_monitor_1 Attaching to monitoring-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,540] [INFO] [Dispatcher]: Starting up
monitor_1 | [2020-08-08 18:09:08,618] [INFO] [HttpMonitor]: Check monitor_1 | [2020-08-08 18:09:08,618] [INFO] [HttpMonitor]: Check
monitor_1 | GET http://example.com monitor_1 | GET http://example.com
monitor_1 | response code: 200 monitor_1 | response code: 200
monitor_1 | content length: 648 monitor_1 | content length: 648
monitor_1 | request took: 0.077 seconds monitor_1 | request took: 0.077 seconds
monitor_1 |
monitor_1 | [2020-08-08 18:09:08,722] [INFO] [HttpMonitor]: Check monitor_1 | [2020-08-08 18:09:08,722] [INFO] [HttpMonitor]: Check
monitor_1 | GET https://httpbin.org/get monitor_1 | GET https://httpbin.org/get
monitor_1 | response code: 200 monitor_1 | response code: 200
monitor_1 | content length: 310 monitor_1 | content length: 310
monitor_1 | request took: 0.18 seconds monitor_1 | request took: 0.18 seconds
monitor_1 |
monitor_1 | [2020-08-08 18:09:13,619] [INFO] [HttpMonitor]: Check monitor_1 | [2020-08-08 18:09:13,619] [INFO] [HttpMonitor]: Check
monitor_1 | GET http://example.com monitor_1 | GET http://example.com
monitor_1 | response code: 200 monitor_1 | response code: 200
monitor_1 | content length: 648 monitor_1 | content length: 648
monitor_1 | request took: 0.066 seconds monitor_1 | request took: 0.066 seconds
monitor_1 |
monitor_1 | [2020-08-08 18:09:13,681] [INFO] [HttpMonitor]: Check monitor_1 | [2020-08-08 18:09:13,681] [INFO] [HttpMonitor]: Check
monitor_1 | GET https://httpbin.org/get monitor_1 | GET https://httpbin.org/get
monitor_1 | response code: 200 monitor_1 | response code: 200
@ -843,12 +867,12 @@ You should see:
The functional part is done. Daemon monitors `http://example.com <http://example.com>`_ and The functional part is done. Daemon monitors `http://example.com <http://example.com>`_ and
`https://httpbin.org <https://httpbin.org>`_. `https://httpbin.org <https://httpbin.org>`_.
In next section we will add some tests. In the next section we will add some tests.
Tests Tests
----- -----
In this section we will add some tests. It would be nice to add some tests. Let's do it.
We will use `pytest <https://docs.pytest.org/en/stable/>`_ and We will use `pytest <https://docs.pytest.org/en/stable/>`_ and
`coverage <https://coverage.readthedocs.io/>`_. `coverage <https://coverage.readthedocs.io/>`_.
@ -875,7 +899,7 @@ Create ``tests.py`` in the ``monitoringdaemon`` package:
and put next into it: and put next into it:
.. code-block:: python .. code-block:: python
:emphasize-lines: 54,70-73 :emphasize-lines: 54,70-71
"""Tests module.""" """Tests module."""
@ -885,7 +909,7 @@ and put next into it:
import pytest import pytest
from .containers import Container from .containers import ApplicationContainer
@dataclasses.dataclass @dataclasses.dataclass
@ -896,33 +920,33 @@ and put next into it:
@pytest.fixture @pytest.fixture
def container(): def container():
return Container( container = ApplicationContainer()
config={ container.config.from_dict({
"log": { 'log': {
"level": "INFO", 'level': 'INFO',
"formant": "[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s", 'formant': '[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s',
},
'monitors': {
'example': {
'method': 'GET',
'url': 'http://fake-example.com',
'timeout': 1,
'check_every': 1,
}, },
"monitors": { 'httpbin': {
"example": { 'method': 'GET',
"method": "GET", 'url': 'https://fake-httpbin.org/get',
"url": "http://fake-example.com", 'timeout': 1,
"timeout": 1, 'check_every': 1,
"check_every": 1,
},
"httpbin": {
"method": "GET",
"url": "https://fake-httpbin.org/get",
"timeout": 1,
"check_every": 1,
},
}, },
} },
) })
return container
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_example_monitor(container, caplog): async def test_example_monitor(container, caplog):
caplog.set_level("INFO") caplog.set_level('INFO')
http_client_mock = mock.AsyncMock() http_client_mock = mock.AsyncMock()
http_client_mock.request.return_value = RequestStub( http_client_mock.request.return_value = RequestStub(
@ -934,22 +958,21 @@ and put next into it:
example_monitor = container.example_monitor() example_monitor = container.example_monitor()
await example_monitor.check() await example_monitor.check()
assert "http://fake-example.com" in caplog.text assert 'http://fake-example.com' in caplog.text
assert "response code: 200" in caplog.text assert 'response code: 200' in caplog.text
assert "content length: 635" in caplog.text assert 'content length: 635' in caplog.text
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_dispatcher(container, caplog, event_loop): async def test_dispatcher(container, caplog, event_loop):
caplog.set_level("INFO") caplog.set_level('INFO')
example_monitor_mock = mock.AsyncMock() example_monitor_mock = mock.AsyncMock()
httpbin_monitor_mock = mock.AsyncMock() httpbin_monitor_mock = mock.AsyncMock()
with container.override_providers( with container.example_monitor.override(example_monitor_mock), \
example_monitor=example_monitor_mock, container.httpbin_monitor.override(httpbin_monitor_mock):
httpbin_monitor=httpbin_monitor_mock,
):
dispatcher = container.dispatcher() dispatcher = container.dispatcher()
event_loop.create_task(dispatcher.start()) event_loop.create_task(dispatcher.start())
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
@ -962,32 +985,31 @@ Run in the terminal:
.. code-block:: bash .. code-block:: bash
docker compose run --rm monitor py.test monitoringdaemon/tests.py --cov=monitoringdaemon docker-compose run --rm monitor py.test monitoringdaemon/tests.py --cov=monitoringdaemon
You should see: You should see:
.. code-block:: bash .. code-block:: bash
platform linux -- Python 3.13.1, pytest-8.3.4, pluggy-1.5.0 platform linux -- Python 3.8.3, pytest-6.0.1, py-1.9.0, pluggy-0.13.1
rootdir: /code rootdir: /code
plugins: cov-6.0.0, asyncio-0.24.0 plugins: asyncio-0.14.0, cov-2.10.0
asyncio: mode=Mode.STRICT, default_loop_scope=None
collected 2 items collected 2 items
monitoringdaemon/tests.py .. [100%] monitoringdaemon/tests.py .. [100%]
---------- coverage: platform linux, python 3.10.0-final-0 ----------- ----------- coverage: platform linux, python 3.8.3-final-0 -----------
Name Stmts Miss Cover Name Stmts Miss Cover
---------------------------------------------------- ----------------------------------------------------
monitoringdaemon/__init__.py 0 0 100% monitoringdaemon/__init__.py 0 0 100%
monitoringdaemon/__main__.py 11 11 0% monitoringdaemon/__main__.py 9 9 0%
monitoringdaemon/containers.py 11 0 100% monitoringdaemon/containers.py 11 0 100%
monitoringdaemon/dispatcher.py 45 5 89% monitoringdaemon/dispatcher.py 43 5 88%
monitoringdaemon/http.py 6 3 50% monitoringdaemon/http.py 6 3 50%
monitoringdaemon/monitors.py 23 1 96% monitoringdaemon/monitors.py 23 1 96%
monitoringdaemon/tests.py 35 0 100% monitoringdaemon/tests.py 37 0 100%
---------------------------------------------------- ----------------------------------------------------
TOTAL 131 20 85% TOTAL 129 18 86%
.. note:: .. note::
@ -1006,19 +1028,55 @@ In this tutorial we've built an ``asyncio`` monitoring daemon following the dep
injection principle. injection principle.
We've used the ``Dependency Injector`` as a dependency injection framework. We've used the ``Dependency Injector`` as a dependency injection framework.
With a help of :ref:`containers` and :ref:`providers` we have defined how to assemble application components. 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:
``List`` provider helped to inject a list of monitors into dispatcher. .. code-block:: python
:ref:`configuration-provider` helped to deal with reading YAML file.
We used :ref:`wiring` feature to inject dispatcher into the ``main()`` function. """Application containers module."""
:ref:`provider-overriding` feature helped in testing.
We kept all the dependencies injected explicitly. This will help when you need to add or import logging
change something in future. import sys
You can find complete project on the from dependency_injector import containers, providers
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/asyncio-daemon>`_.
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,
),
)
What's next? What's next?
@ -1026,6 +1084,4 @@ What's next?
- Know more about the :ref:`providers` - Know more about the :ref:`providers`
- Go to the :ref:`contents` - Go to the :ref:`contents`
.. include:: ../sponsor.rst
.. disqus:: .. disqus::

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

@ -46,11 +46,11 @@ How does Movie Lister work?
Movie Lister is a naive example from Martin Fowler's article about the dependency injection and Movie Lister is a naive example from Martin Fowler's article about the dependency injection and
inversion of control: inversion of control:
https://www.martinfowler.com/articles/injection.html http://www.martinfowler.com/articles/injection.html
Here is a class diagram of the Movie Lister application: 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: The responsibilities are split next way:
@ -63,18 +63,18 @@ Prepare the environment
Let's create the environment for the project. Let's create the environment for the project.
First we need to create a project folder: First we need to create a project folder and the virtual environment:
.. code-block:: bash .. code-block:: bash
mkdir movie-lister-tutorial mkdir movie-lister-tutorial
cd movie-lister-tutorial cd movie-lister-tutorial
python3 -m venv venv
Now let's create and activate virtual environment: Now let's activate the virtual environment:
.. code-block:: bash .. code-block:: bash
python3 -m venv venv
. venv/bin/activate . venv/bin/activate
Project layout Project layout
@ -84,7 +84,7 @@ Create next structure in the project root directory. All files are empty. That's
Initial project layout: Initial project layout:
.. code-block:: text .. code-block:: bash
./ ./
├── movies/ ├── movies/
@ -109,7 +109,7 @@ Now it's time to install the project requirements. We will use next packages:
Put next lines into the ``requirements.txt`` file: Put next lines into the ``requirements.txt`` file:
.. code-block:: text .. code-block:: bash
dependency-injector dependency-injector
pyyaml pyyaml
@ -134,7 +134,7 @@ We will create a script that creates database files.
First add the folder ``data/`` in the root of the project and then add the file First add the folder ``data/`` in the root of the project and then add the file
``fixtures.py`` inside of it: ``fixtures.py`` inside of it:
.. code-block:: text .. code-block:: bash
:emphasize-lines: 2-3 :emphasize-lines: 2-3
./ ./
@ -160,19 +160,19 @@ Second put next in the ``fixtures.py``:
SAMPLE_DATA = [ SAMPLE_DATA = [
("The Hunger Games: Mockingjay - Part 2", 2015, "Francis Lawrence"), ('The Hunger Games: Mockingjay - Part 2', 2015, 'Francis Lawrence'),
("Rogue One: A Star Wars Story", 2016, "Gareth Edwards"), ('Rogue One: A Star Wars Story', 2016, 'Gareth Edwards'),
("The Jungle Book", 2016, "Jon Favreau"), ('The Jungle Book', 2016, 'Jon Favreau'),
] ]
FILE = pathlib.Path(__file__) FILE = pathlib.Path(__file__)
DIR = FILE.parent DIR = FILE.parent
CSV_FILE = DIR / "movies.csv" CSV_FILE = DIR / 'movies.csv'
SQLITE_FILE = DIR / "movies.db" SQLITE_FILE = DIR / 'movies.db'
def create_csv(movies_data, path): def create_csv(movies_data, path):
with open(path, "w") as opened_file: with open(path, 'w') as opened_file:
writer = csv.writer(opened_file) writer = csv.writer(opened_file)
for row in movies_data: for row in movies_data:
writer.writerow(row) writer.writerow(row)
@ -181,20 +181,20 @@ Second put next in the ``fixtures.py``:
def create_sqlite(movies_data, path): def create_sqlite(movies_data, path):
with sqlite3.connect(path) as db: with sqlite3.connect(path) as db:
db.execute( db.execute(
"CREATE TABLE IF NOT EXISTS movies " 'CREATE TABLE IF NOT EXISTS movies '
"(title text, year int, director text)" '(title text, year int, director text)'
) )
db.execute("DELETE FROM movies") db.execute('DELETE FROM movies')
db.executemany("INSERT INTO movies VALUES (?,?,?)", movies_data) db.executemany('INSERT INTO movies VALUES (?,?,?)', movies_data)
def main(): def main():
create_csv(SAMPLE_DATA, CSV_FILE) create_csv(SAMPLE_DATA, CSV_FILE)
create_sqlite(SAMPLE_DATA, SQLITE_FILE) create_sqlite(SAMPLE_DATA, SQLITE_FILE)
print("OK") print('OK')
if __name__ == "__main__": if __name__ == '__main__':
main() main()
Now run in the terminal: Now run in the terminal:
@ -205,13 +205,13 @@ Now run in the terminal:
You should see: You should see:
.. code-block:: text .. code-block:: bash
OK OK
Check that files ``movies.csv`` and ``movies.db`` have appeared in the ``data/`` folder: Check that files ``movies.csv`` and ``movies.db`` have appeared in the ``data/`` folder:
.. code-block:: text .. code-block:: bash
:emphasize-lines: 4-5 :emphasize-lines: 4-5
./ ./
@ -245,13 +245,13 @@ Edit ``containers.py``:
from dependency_injector import containers from dependency_injector import containers
class Container(containers.DeclarativeContainer): class ApplicationContainer(containers.DeclarativeContainer):
... ...
Container is empty for now. We will add the providers in the following sections. 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 Let's also create the ``main()`` function. Its responsibility is to run our application. For now
it will just do nothing. it will just create the container.
Edit ``__main__.py``: Edit ``__main__.py``:
@ -259,18 +259,22 @@ Edit ``__main__.py``:
"""Main module.""" """Main module."""
from .containers import Container from .containers import ApplicationContainer
def main() -> None: def main():
... container = ApplicationContainer()
if __name__ == "__main__": if __name__ == '__main__':
container = Container()
main() main()
.. note::
Container is the first object in the application.
The container is used to create all other objects.
Csv finder Csv finder
---------- ----------
@ -285,11 +289,11 @@ We will add:
After each step we will add the provider to the container. 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: Create the ``entities.py`` in the ``movies`` package:
.. code-block:: text .. code-block:: bash
:emphasize-lines: 10 :emphasize-lines: 10
./ ./
@ -321,7 +325,7 @@ and put next into it:
self.director = str(director) self.director = str(director)
def __repr__(self): def __repr__(self):
return "{0}(title={1}, year={2}, director={3})".format( return '{0}(title={1}, year={2}, director={3})'.format(
self.__class__.__name__, self.__class__.__name__,
repr(self.title), repr(self.title),
repr(self.year), repr(self.year),
@ -334,7 +338,7 @@ Now we need to add the ``Movie`` factory to the container. We need to add import
Edit ``containers.py``: Edit ``containers.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 3,5,10 :emphasize-lines: 3,5,9
"""Containers module.""" """Containers module."""
@ -342,8 +346,7 @@ Edit ``containers.py``:
from . import entities from . import entities
class ApplicationContainer(containers.DeclarativeContainer):
class Container(containers.DeclarativeContainer):
movie = providers.Factory(entities.Movie) movie = providers.Factory(entities.Movie)
@ -356,7 +359,7 @@ Let's move on to the finders.
Create the ``finders.py`` in the ``movies`` package: Create the ``finders.py`` in the ``movies`` package:
.. code-block:: text .. code-block:: bash
:emphasize-lines: 11 :emphasize-lines: 11
./ ./
@ -417,7 +420,7 @@ Now let's add the csv finder into the container.
Edit ``containers.py``: Edit ``containers.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 5,10,14-19 :emphasize-lines: 5,9,13-18
"""Containers module.""" """Containers module."""
@ -425,10 +428,9 @@ Edit ``containers.py``:
from . import finders, entities from . import finders, entities
class ApplicationContainer(containers.DeclarativeContainer):
class Container(containers.DeclarativeContainer): config = providers.Configuration()
config = providers.Configuration(yaml_files=["config.yml"])
movie = providers.Factory(entities.Movie) movie = providers.Factory(entities.Movie)
@ -445,9 +447,15 @@ This is also called the delegation of the provider. If we just pass the movie fa
as the dependency, it will be called when csv finder is created and the ``Movie`` instance will as the dependency, it will be called when csv finder is created and the ``Movie`` instance will
be injected. With the ``.provider`` attribute the provider itself will be injected. be injected. With the ``.provider`` attribute the provider itself will be injected.
The csv finder also has a few dependencies on the configuration options. We added a configuration The csv finder also has a few dependencies on the configuration options. We added configuration
provider to provide these dependencies and specified the location of the configuration file. provider to provide these dependencies.
The configuration provider will parse the configuration file when we create a container instance.
.. note::
We have used the configuration value before it was defined. That's the principle how the
Configuration provider works.
Use first, define later.
Not let's define the configuration values. Not let's define the configuration values.
@ -461,11 +469,32 @@ Edit ``config.yml``:
path: "data/movies.csv" path: "data/movies.csv"
delimiter: "," delimiter: ","
The configuration file is ready. Move on to the lister. The configuration file is ready. Now let's update the ``main()`` function to specify its location.
Edit ``__main__.py``:
.. code-block:: python
:emphasize-lines: 9
"""Main module."""
from .containers import ApplicationContainer
def main():
container = ApplicationContainer()
container.config.from_yaml('config.yml')
if __name__ == '__main__':
main()
Move on to the lister.
Create the ``listers.py`` in the ``movies`` package: Create the ``listers.py`` in the ``movies`` package:
.. code-block:: text .. code-block:: bash
:emphasize-lines: 12 :emphasize-lines: 12
./ ./
@ -513,7 +542,7 @@ and put next into it:
and edit ``containers.py``: and edit ``containers.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 5,21-24 :emphasize-lines: 5,20-23
"""Containers module.""" """Containers module."""
@ -521,10 +550,9 @@ and edit ``containers.py``:
from . import finders, listers, entities from . import finders, listers, entities
class ApplicationContainer(containers.DeclarativeContainer):
class Container(containers.DeclarativeContainer): config = providers.Configuration()
config = providers.Configuration(yaml_files=["config.yml"])
movie = providers.Factory(entities.Movie) movie = providers.Factory(entities.Movie)
@ -542,65 +570,36 @@ and edit ``containers.py``:
All the components are created and added to the container. All the components are created and added to the container.
Let's inject the ``lister`` into the ``main()`` function. Finally let's update the ``main()`` function.
Edit ``__main__.py``: Edit ``__main__.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 3-5,9-10,16 :emphasize-lines: 11-20
"""Main module.""" """Main module."""
from dependency_injector.wiring import Provide, inject from .containers import ApplicationContainer
from .listers import MovieLister
from .containers import Container
@inject def main():
def main(lister: MovieLister = Provide[Container.lister]) -> None: 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),
)
if __name__ == "__main__": if __name__ == '__main__':
container = Container()
container.wire(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: 11-17
"""Main module."""
from dependency_injector.wiring import Provide, inject
from .listers import MovieLister
from .containers import Container
@inject
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.wire(modules=[__name__])
main() main()
All set. Now we run the application. All set. Now we run the application.
@ -613,15 +612,12 @@ Run in the terminal:
You should see: You should see:
.. code-block:: text .. code-block:: bash
Francis Lawrence movies: Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]
- 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')]
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 want to support Our application can work with the movies database in the csv format. We also need to support
the sqlite format. We will deal with it in the next section. the sqlite format. We will deal with it in the next section.
Sqlite finder Sqlite finder
@ -684,7 +680,7 @@ Edit ``finders.py``:
def find_all(self) -> List[Movie]: def find_all(self) -> List[Movie]:
with self._database as db: with self._database as db:
rows = db.execute("SELECT title, year, director FROM movies") rows = db.execute('SELECT title, year, director FROM movies')
return [self._movie_factory(*row) for row in rows] return [self._movie_factory(*row) for row in rows]
Now we need to add the sqlite finder to the container and update lister's dependency to use it. Now we need to add the sqlite finder to the container and update lister's dependency to use it.
@ -692,7 +688,7 @@ Now we need to add the sqlite finder to the container and update lister's depend
Edit ``containers.py``: Edit ``containers.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 21-25,29 :emphasize-lines: 20-24,28
"""Containers module.""" """Containers module."""
@ -700,10 +696,9 @@ Edit ``containers.py``:
from . import finders, listers, entities from . import finders, listers, entities
class ApplicationContainer(containers.DeclarativeContainer):
class Container(containers.DeclarativeContainer): config = providers.Configuration()
config = providers.Configuration(yaml_files=["config.yml"])
movie = providers.Factory(entities.Movie) movie = providers.Factory(entities.Movie)
@ -752,13 +747,10 @@ Run in the terminal:
You should see: You should see:
.. code-block:: text .. code-block:: bash
Francis Lawrence movies: Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]
- 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')]
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 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 need to work with the different format we need to make a code change in the container. We will
@ -790,9 +782,9 @@ Edit ``containers.py``:
from . import finders, listers, entities from . import finders, listers, entities
class Container(containers.DeclarativeContainer): class ApplicationContainer(containers.DeclarativeContainer):
config = providers.Configuration(yaml_files=["config.yml"]) config = providers.Configuration()
movie = providers.Factory(entities.Movie) movie = providers.Factory(entities.Movie)
@ -820,7 +812,7 @@ Edit ``containers.py``:
movie_finder=finder, movie_finder=finder,
) )
The switch is the ``config.finder.type`` option. When its value is ``csv``, the provider with the The switch is the ``config.finder.type`` option. When its value is ``csv``, the provider under
``csv`` key is used. The same is for ``sqlite``. ``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 Now we need to read the value of the ``config.finder.type`` option from the environment variable
@ -829,32 +821,32 @@ Now we need to read the value of the ``config.finder.type`` option from the envi
Edit ``__main__.py``: Edit ``__main__.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 22 :emphasize-lines: 10
"""Main module.""" """Main module."""
from dependency_injector.wiring import Provide, inject from .containers import ApplicationContainer
from .listers import MovieLister
from .containers import Container
@inject def main():
def main(lister: MovieLister = Provide[Container.lister]) -> None: container = ApplicationContainer()
print("Francis Lawrence movies:")
for movie in lister.movies_directed_by("Francis Lawrence"):
print("\t-", movie)
print("2016 movies:") container.config.from_yaml('config.yml')
for movie in lister.movies_released_in(2016): container.config.finder.type.from_env('MOVIE_FINDER_TYPE')
print("\t-", movie)
lister = container.lister()
print(
'Francis Lawrence movies:',
lister.movies_directed_by('Francis Lawrence'),
)
print(
'2016 movies:',
lister.movies_released_in(2016),
)
if __name__ == "__main__": if __name__ == '__main__':
container = Container()
container.config.finder.type.from_env("MOVIE_FINDER_TYPE")
container.wire(modules=[sys.modules[__name__]])
main() main()
Done. Done.
@ -866,15 +858,12 @@ Run in the terminal line by line:
MOVIE_FINDER_TYPE=csv python -m movies MOVIE_FINDER_TYPE=csv python -m movies
MOVIE_FINDER_TYPE=sqlite python -m movies MOVIE_FINDER_TYPE=sqlite python -m movies
The output should be similar for each command: The output should be something like this for each command:
.. code-block:: text .. code-block:: bash
Francis Lawrence movies: Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]
- 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')]
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. In the next section we will add some tests.
@ -888,7 +877,7 @@ We will use `pytest <https://docs.pytest.org/en/stable/>`_ and
Create ``tests.py`` in the ``movies`` package: Create ``tests.py`` in the ``movies`` package:
.. code-block:: text .. code-block:: bash
:emphasize-lines: 13 :emphasize-lines: 13
./ ./
@ -911,7 +900,7 @@ Create ``tests.py`` in the ``movies`` package:
and put next into it: and put next into it:
.. code-block:: python .. code-block:: python
:emphasize-lines: 41,50 :emphasize-lines: 35,50
"""Tests module.""" """Tests module."""
@ -919,55 +908,55 @@ and put next into it:
import pytest import pytest
from .containers import Container from .containers import ApplicationContainer
@pytest.fixture @pytest.fixture
def container(): def container():
container = Container( container = ApplicationContainer()
config={ container.config.from_dict({
"finder": { 'finder': {
"type": "csv", 'type': 'csv',
"csv": { 'csv': {
"path": "/fake-movies.csv", 'path': '/fake-movies.csv',
"delimiter": ",", 'delimiter': ',',
}, },
"sqlite": { 'sqlite': {
"path": "/fake-movies.db", 'path': '/fake-movies.db',
},
}, },
}, },
) })
return container return container
@pytest.fixture def test_movies_directed_by(container):
def finder_mock(container):
finder_mock = mock.Mock() finder_mock = mock.Mock()
finder_mock.find_all.return_value = [ finder_mock.find_all.return_value = [
container.movie("The 33", 2015, "Patricia Riggen"), container.movie('The 33', 2015, 'Patricia Riggen'),
container.movie("The Jungle Book", 2016, "Jon Favreau"), container.movie('The Jungle Book', 2016, 'Jon Favreau'),
] ]
return finder_mock
def test_movies_directed_by(container, finder_mock):
with container.finder.override(finder_mock): with container.finder.override(finder_mock):
lister = container.lister() lister = container.lister()
movies = lister.movies_directed_by("Jon Favreau") movies = lister.movies_directed_by('Jon Favreau')
assert len(movies) == 1 assert len(movies) == 1
assert movies[0].title == "The Jungle Book" assert movies[0].title == 'The Jungle Book'
def test_movies_released_in(container, finder_mock): def test_movies_released_in(container):
finder_mock = mock.Mock()
finder_mock.find_all.return_value = [
container.movie('The 33', 2015, 'Patricia Riggen'),
container.movie('The Jungle Book', 2016, 'Jon Favreau'),
]
with container.finder.override(finder_mock): with container.finder.override(finder_mock):
lister = container.lister() lister = container.lister()
movies = lister.movies_released_in(2015) movies = lister.movies_released_in(2015)
assert len(movies) == 1 assert len(movies) == 1
assert movies[0].title == "The 33" assert movies[0].title == 'The 33'
Run in the terminal: Run in the terminal:
@ -977,26 +966,26 @@ Run in the terminal:
You should see: You should see:
.. code-block:: text .. code-block:: bash
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
plugins: cov-3.0.0 plugins: cov-2.10.0
collected 2 items collected 2 items
movies/tests.py .. [100%] movies/tests.py .. [100%]
---------- coverage: platform darwin, python 3.10 ----------- ---------- coverage: platform darwin, python 3.8.3-final-0 -----------
Name Stmts Miss Cover Name Stmts Miss Cover
------------------------------------------ ------------------------------------------
movies/__init__.py 0 0 100% movies/__init__.py 0 0 100%
movies/__main__.py 16 16 0% movies/__main__.py 10 10 0%
movies/containers.py 9 0 100% movies/containers.py 9 0 100%
movies/entities.py 7 1 86% movies/entities.py 7 1 86%
movies/finders.py 26 13 50% movies/finders.py 26 13 50%
movies/listers.py 8 0 100% movies/listers.py 8 0 100%
movies/tests.py 24 0 100% movies/tests.py 24 0 100%
------------------------------------------ ------------------------------------------
TOTAL 90 30 67% TOTAL 84 24 71%
.. note:: .. note::
@ -1013,19 +1002,48 @@ Conclusion
In this tutorial we've built a CLI application following the dependency injection principle. 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. We've used the ``Dependency Injector`` as a dependency injection framework.
With a help of :ref:`containers` and :ref:`providers` we have defined how to assemble application components. 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:
``Selector`` provider served as a switch for selecting the database format based on a configuration. .. code-block:: python
:ref:`configuration-provider` helped to deal with reading a YAML file and environment variables.
We used :ref:`wiring` feature to inject the dependencies into the ``main()`` function. """Containers module."""
:ref:`provider-overriding` feature helped in testing.
We kept all the dependencies injected explicitly. This will help when you need to add or from dependency_injector import containers, providers
change something in future.
You can find complete project on the from . import finders, listers, entities
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/movie-lister>`_.
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,
)
What's next? What's next?
@ -1033,6 +1051,4 @@ What's next?
- Know more about the :ref:`providers` - Know more about the :ref:`providers`
- Go to the :ref:`contents` - Go to the :ref:`contents`
.. include:: ../sponsor.rst
.. disqus:: .. disqus::

Binary file not shown.

Before

Width:  |  Height:  |  Size: 647 KiB

View File

@ -21,7 +21,7 @@ Start from the scratch or jump to the section:
:backlinks: none :backlinks: none
You can find complete project on the You can find complete project on the
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/flask>`_. `Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/ghnav-flask>`_.
What are we going to build? 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 - User can click on the repository, the repository owner or the last commit to open its web page
on the Github. on the Github.
.. image:: flask-images/screen-02.png .. image:: flask_images/screen_02.png
Prepare the environment Prepare the environment
----------------------- -----------------------
Let's create the environment for the project. Let's create the environment for the project.
First we need to create a project folder: First we need to create a project folder and the virtual environment:
.. code-block:: bash .. code-block:: bash
mkdir ghnav-flask-tutorial mkdir ghnav-flask-tutorial
cd ghnav-flask-tutorial cd ghnav-flask-tutorial
python3 -m venv venv
Now let's create and activate virtual environment: Now let's activate the virtual environment:
.. code-block:: bash .. code-block:: bash
python3 -m venv venv
. venv/bin/activate . venv/bin/activate
Project layout Project layout
@ -110,13 +110,13 @@ You should see something like:
.. code-block:: bash .. code-block:: bash
(venv) $ python -c "import dependency_injector; print(dependency_injector.__version__)" (venv) $ python -c "import dependency_injector; print(dependency_injector.__version__)"
4.37.0 3.22.0
(venv) $ python -c "import flask; print(flask.__version__)" (venv) $ python -c "import flask; print(flask.__version__)"
2.0.2 1.1.2
*Versions can be different. That's fine.* *Versions can be different. That's fine.*
Hello World! Hello world!
------------ ------------
Let's create minimal application. Let's create minimal application.
@ -129,29 +129,38 @@ Put next into the ``views.py``:
def index(): def index():
return "Hello, World!" return 'Hello, World!'
Ok, we have the view. Ok, we have the view.
Now let's create a container. Container will keep all of the application components and their dependencies. 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.
Edit ``containers.py``: Put next into the ``containers.py``:
.. code-block:: python .. code-block:: python
"""Containers module.""" """Application containers module."""
from dependency_injector import containers from dependency_injector import containers
from dependency_injector.ext import flask
from flask import Flask
from . import views
class Container(containers.DeclarativeContainer): class ApplicationContainer(containers.DeclarativeContainer):
... """Application container."""
Container is empty for now. We will add the providers in the following sections. app = flask.Application(Flask, __name__)
Finally we need to create Flask application factory. It will create and configure container index_view = flask.View(views.index)
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. 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.
Put next into the ``application.py``: Put next into the ``application.py``:
@ -159,21 +168,26 @@ Put next into the ``application.py``:
"""Application module.""" """Application module."""
from flask import Flask from .containers import ApplicationContainer
from .containers import Container
from . import views
def create_app() -> Flask: def create_app():
container = Container() """Create and return Flask application."""
container = ApplicationContainer()
app = Flask(__name__) app = container.app()
app.container = container app.container = container
app.add_url_rule("/", "index", views.index)
app.add_url_rule('/', view_func=container.index_view.as_view())
return app 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!". Ok. Now we're ready to say "Hello, World!".
Do next in the terminal: Do next in the terminal:
@ -223,34 +237,58 @@ and run in the terminal:
.. code-block:: bash .. code-block:: bash
pip install -r requirements.txt 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)
Let's initialize ``bootstrap-flask`` extension. We will need to modify ``create_app()``. Let's initialize ``bootstrap-flask`` extension. We will need to modify ``create_app()``.
Edit ``application.py``: Edit ``application.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 4,17-18 :emphasize-lines: 13-14
"""Application module.""" """Application module."""
from flask import Flask from .containers import ApplicationContainer
from flask_bootstrap import Bootstrap
from .containers import Container
from . import views
def create_app() -> Flask: def create_app():
container = Container() """Create and return Flask application."""
container = ApplicationContainer()
app = Flask(__name__) app = container.app()
app.container = container app.container = container
app.add_url_rule("/", "index", views.index)
bootstrap = Bootstrap() bootstrap = container.bootstrap()
bootstrap.init_app(app) bootstrap.init_app(app)
app.add_url_rule('/', view_func=container.index_view.as_view())
return app return app
Now we need to add the templates. For doing this we will need to add the folder ``templates/`` to Now we need to add the templates. For doing this we will need to add the folder ``templates/`` to
@ -280,7 +318,7 @@ Now let's fill in the layout.
Put next into the ``base.html``: Put next into the ``base.html``:
.. code-block:: jinja .. code-block:: html
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
@ -313,7 +351,7 @@ And put something to the index page.
Put next into the ``index.html``: Put next into the ``index.html``:
.. code-block:: jinja .. code-block:: html
{% extends "base.html" %} {% extends "base.html" %}
@ -398,13 +436,13 @@ Edit ``views.py``:
def index(): def index():
query = request.args.get("query", "Dependency Injector") query = request.args.get('query', 'Dependency Injector')
limit = request.args.get("limit", 10, int) limit = request.args.get('limit', 10, int)
repositories = [] repositories = []
return render_template( return render_template(
"index.html", 'index.html',
query=query, query=query,
limit=limit, limit=limit,
repositories=repositories, repositories=repositories,
@ -416,7 +454,7 @@ Make sure the app is running or use ``flask run`` and open ``http://127.0.0.1:50
You should see: You should see:
.. image:: flask-images/screen-01.png .. image:: flask_images/screen_01.png
Connect to the GitHub Connect to the GitHub
--------------------- ---------------------
@ -439,30 +477,41 @@ and run in the terminal:
.. code-block:: bash .. code-block:: bash
pip install -r requirements.txt pip install --upgrade -r requirements.txt
Now we need to add Github API client the container. We will need to add two more providers from Now we need to add Github API client the container. We will need to add two more providers from
the ``dependency_injector.providers`` module: the ``dependency_injector.providers`` module:
- ``Factory`` provider. It will create a ``Github`` client. - ``Factory`` provider that will create ``Github`` client.
- ``Configuration`` provider. It will provide an API token and a request timeout for the ``Github`` client. - ``Configuration`` provider that will be used for providing the API token and the request timeout
We will specify the location of the configuration file. The configuration provider will parse for the ``Github`` client.
the configuration file when we create a container instance.
Let's do it.
Edit ``containers.py``: Edit ``containers.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 3-4,9,11-15 :emphasize-lines: 3,7,19,21-25
"""Containers module.""" """Application containers module."""
from dependency_injector import containers, providers 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 github import Github
from . import views
class Container(containers.DeclarativeContainer):
config = providers.Configuration(yaml_files=["config.yml"]) class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = flask.Application(Flask, __name__)
bootstrap = flask.Extension(Bootstrap)
config = providers.Configuration()
github_client = providers.Factory( github_client = providers.Factory(
Github, Github,
@ -470,14 +519,20 @@ Edit ``containers.py``:
timeout=config.github.request_timeout, timeout=config.github.request_timeout,
) )
index_view = flask.View(views.index)
.. note:: .. note::
Don't forget to remove the Ellipsis ``...`` from the container. We don't need it anymore We have used the configuration value before it was defined. That's the principle how
since we container is not empty. ``Configuration`` provider works.
Now let's add the configuration file. We will use YAML. Create an empty file ``config.yml`` Use first, define later.
in the root of the project:
Now let's add the configuration file.
We will use YAML.
Create an empty file ``config.yml`` in the root root of the project:
.. code-block:: bash .. code-block:: bash
:emphasize-lines: 11 :emphasize-lines: 11
@ -520,36 +575,39 @@ and install it:
.. code-block:: bash .. code-block:: bash
pip install -r requirements.txt pip install --upgrade -r requirements.txt
We will use the ``GITHUB_TOKEN`` environment variable to provide the API token. Let's edit We will use environment variable ``GITHUB_TOKEN`` to provide the API token.
``create_app()`` to fetch the token value from it.
Now we need to edit ``create_app()`` to make two things when application starts:
- Load the configuration file the ``config.yml``.
- Load the API token from the ``GITHUB_TOKEN`` environment variable.
Edit ``application.py``: Edit ``application.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 12 :emphasize-lines: 9-10
"""Application module.""" """Application module."""
from flask import Flask from .containers import ApplicationContainer
from flask_bootstrap import Bootstrap
from .containers import Container
from . import views
def create_app() -> Flask: def create_app():
container = Container() """Create and return Flask application."""
container.config.github.auth_token.from_env("GITHUB_TOKEN") container = ApplicationContainer()
container.config.from_yaml('config.yml')
container.config.github.auth_token.from_env('GITHUB_TOKEN')
app = Flask(__name__) app = container.app()
app.container = container app.container = container
app.add_url_rule("/", "index", views.index)
bootstrap = Bootstrap() bootstrap = container.bootstrap()
bootstrap.init_app(app) bootstrap.init_app(app)
app.add_url_rule('/', view_func=container.index_view.as_view())
return app return app
Now we need create an API token. Now we need create an API token.
@ -578,7 +636,7 @@ Github API client setup is done.
Search service Search service
-------------- --------------
Now it's time to add ``SearchService``. It will: Now it's time to add the ``SearchService``. It will:
- Perform the search. - Perform the search.
- Fetch commit extra data for each result. - Fetch commit extra data for each result.
@ -626,7 +684,7 @@ and put next into it:
"""Search for repositories and return formatted data.""" """Search for repositories and return formatted data."""
repositories = self._github_client.search_repositories( repositories = self._github_client.search_repositories(
query=query, query=query,
**{"in": "name"}, **{'in': 'name'},
) )
return [ return [
self._format_repo(repository) self._format_repo(repository)
@ -636,22 +694,22 @@ and put next into it:
def _format_repo(self, repository: Repository): def _format_repo(self, repository: Repository):
commits = repository.get_commits() commits = repository.get_commits()
return { return {
"url": repository.html_url, 'url': repository.html_url,
"name": repository.name, 'name': repository.name,
"owner": { 'owner': {
"login": repository.owner.login, 'login': repository.owner.login,
"url": repository.owner.html_url, 'url': repository.owner.html_url,
"avatar_url": repository.owner.avatar_url, 'avatar_url': repository.owner.avatar_url,
}, },
"latest_commit": self._format_commit(commits[0]) if commits else {}, 'latest_commit': self._format_commit(commits[0]) if commits else {},
} }
def _format_commit(self, commit: Commit): def _format_commit(self, commit: Commit):
return { return {
"sha": commit.sha, 'sha': commit.sha,
"url": commit.html_url, 'url': commit.html_url,
"message": commit.commit.message, 'message': commit.commit.message,
"author_name": commit.commit.author.name, 'author_name': commit.commit.author.name,
} }
Now let's add ``SearchService`` to the container. Now let's add ``SearchService`` to the container.
@ -659,19 +717,27 @@ Now let's add ``SearchService`` to the container.
Edit ``containers.py``: Edit ``containers.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 6,19-22 :emphasize-lines: 9,27-30
"""Containers module.""" """Application containers module."""
from dependency_injector import containers, providers 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 github import Github
from . import services from . import services, views
class Container(containers.DeclarativeContainer): class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
config = providers.Configuration(yaml_files=["config.yml"]) app = flask.Application(Flask, __name__)
bootstrap = flask.Extension(Bootstrap)
config = providers.Configuration()
github_client = providers.Factory( github_client = providers.Factory(
Github, Github,
@ -684,63 +750,64 @@ Edit ``containers.py``:
github_client=github_client, github_client=github_client,
) )
Inject search service into view index_view = flask.View(views.index)
-------------------------------
Now we are ready to make the search work. Make the search work
--------------------
Let's inject ``SearchService`` into the ``index`` view. We will use :ref:`Wiring` feature. Now we are ready to make the search work. Let's use the ``SearchService`` in the ``index`` view.
Edit ``views.py``: Edit ``views.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 4,6-7,10-11,15 :emphasize-lines: 5,8,12
"""Views module.""" """Views module."""
from flask import request, render_template from flask import request, render_template
from dependency_injector.wiring import inject, Provide
from .services import SearchService from .services import SearchService
from .containers import Container
@inject def index(search_service: SearchService):
def index(search_service: SearchService = Provide[Container.search_service]): query = request.args.get('query', 'Dependency Injector')
query = request.args.get("query", "Dependency Injector") limit = request.args.get('limit', 10, int)
limit = request.args.get("limit", 10, int)
repositories = search_service.search_repositories(query, limit) repositories = search_service.search_repositories(query, limit)
return render_template( return render_template(
"index.html", 'index.html',
query=query, query=query,
limit=limit, limit=limit,
repositories=repositories, repositories=repositories,
) )
To make the injection work we need to wire the container with the ``views`` module. Now let's inject the ``SearchService`` dependency into the ``index`` view.
Let's configure the container to automatically make wiring with the ``views`` module when we
create a container instance.
Edit ``containers.py``: Edit ``containers.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 11 :emphasize-lines: 32-35
"""Containers module.""" """Application containers module."""
from dependency_injector import containers, providers 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 github import Github
from . import services from . import services, views
class Container(containers.DeclarativeContainer): class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
wiring_config = containers.WiringConfiguration(modules=[".views"]) app = flask.Application(Flask, __name__)
config = providers.Configuration(yaml_files=["config.yml"]) bootstrap = flask.Extension(Bootstrap)
config = providers.Configuration()
github_client = providers.Factory( github_client = providers.Factory(
Github, Github,
@ -753,11 +820,16 @@ Edit ``containers.py``:
github_client=github_client, github_client=github_client,
) )
index_view = flask.View(
views.index,
search_service=search_service,
)
Make sure the app is running or use ``flask run`` and open ``http://127.0.0.1:5000/``. Make sure the app is running or use ``flask run`` and open ``http://127.0.0.1:5000/``.
You should see: You should see:
.. image:: flask-images/screen-02.png .. image:: flask_images/screen_02.png
Make some refactoring Make some refactoring
--------------------- ---------------------
@ -772,35 +844,79 @@ Let's make some refactoring. We will move these values to the config.
Edit ``views.py``: Edit ``views.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 11-17 :emphasize-lines: 8-14
"""Views module.""" """Views module."""
from flask import request, render_template from flask import request, render_template
from dependency_injector.wiring import inject, Provide
from .services import SearchService from .services import SearchService
from .containers import Container
@inject
def index( def index(
search_service: SearchService = Provide[Container.search_service], search_service: SearchService,
default_query: str = Provide[Container.config.default.query], default_query: str,
default_limit: int = Provide[Container.config.default.limit.as_int()], default_limit: int,
): ):
query = request.args.get("query", default_query) query = request.args.get('query', default_query)
limit = request.args.get("limit", default_limit, int) limit = request.args.get('limit', default_limit, int)
repositories = search_service.search_repositories(query, limit) repositories = search_service.search_repositories(query, limit)
return render_template( return render_template(
"index.html", 'index.html',
query=query, query=query,
limit=limit, limit=limit,
repositories=repositories, 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``: Edit ``config.yml``:
.. code-block:: yaml .. code-block:: yaml
@ -808,18 +924,20 @@ Edit ``config.yml``:
github: github:
request_timeout: 10 request_timeout: 10
default: search:
query: "Dependency Injector" default_query: "Dependency Injector"
limit: 10 default_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 Tests
----- -----
In this section we will add some tests. It would be nice to add some tests. Let's do this.
We will use `pytest <https://docs.pytest.org/en/stable/>`_ with its Flask extension and We will use `pytest <https://docs.pytest.org/en/stable/>`_ and
`coverage <https://coverage.readthedocs.io/>`_. `coverage <https://coverage.readthedocs.io/>`_.
Edit ``requirements.txt``: Edit ``requirements.txt``:
@ -835,7 +953,7 @@ Edit ``requirements.txt``:
pytest-flask pytest-flask
pytest-cov pytest-cov
And install added packages: And let's install it:
.. code-block:: bash .. code-block:: bash
@ -864,7 +982,7 @@ Create empty file ``tests.py`` in the ``githubnavigator`` package:
and put next into it: and put next into it:
.. code-block:: python .. code-block:: python
:emphasize-lines: 44,67 :emphasize-lines: 42,65
"""Tests module.""" """Tests module."""
@ -879,53 +997,51 @@ and put next into it:
@pytest.fixture @pytest.fixture
def app(): def app():
app = create_app() return create_app()
yield app
app.container.unwire()
def test_index(client, app): def test_index(client, app):
github_client_mock = mock.Mock(spec=Github) github_client_mock = mock.Mock(spec=Github)
github_client_mock.search_repositories.return_value = [ github_client_mock.search_repositories.return_value = [
mock.Mock( mock.Mock(
html_url="repo1-url", html_url='repo1-url',
name="repo1-name", name='repo1-name',
owner=mock.Mock( owner=mock.Mock(
login="owner1-login", login='owner1-login',
html_url="owner1-url", html_url='owner1-url',
avatar_url="owner1-avatar-url", avatar_url='owner1-avatar-url',
), ),
get_commits=mock.Mock(return_value=[mock.Mock()]), get_commits=mock.Mock(return_value=[mock.Mock()]),
), ),
mock.Mock( mock.Mock(
html_url="repo2-url", html_url='repo2-url',
name="repo2-name", name='repo2-name',
owner=mock.Mock( owner=mock.Mock(
login="owner2-login", login='owner2-login',
html_url="owner2-url", html_url='owner2-url',
avatar_url="owner2-avatar-url", avatar_url='owner2-avatar-url',
), ),
get_commits=mock.Mock(return_value=[mock.Mock()]), get_commits=mock.Mock(return_value=[mock.Mock()]),
), ),
] ]
with app.container.github_client.override(github_client_mock): with app.container.github_client.override(github_client_mock):
response = client.get(url_for("index")) response = client.get(url_for('index'))
assert response.status_code == 200 assert response.status_code == 200
assert b"Results found: 2" in response.data assert b'Results found: 2' in response.data
assert b"repo1-url" in response.data assert b'repo1-url' in response.data
assert b"repo1-name" in response.data assert b'repo1-name' in response.data
assert b"owner1-login" in response.data assert b'owner1-login' in response.data
assert b"owner1-url" in response.data assert b'owner1-url' in response.data
assert b"owner1-avatar-url" in response.data assert b'owner1-avatar-url' in response.data
assert b"repo2-url" in response.data assert b'repo2-url' in response.data
assert b"repo2-name" in response.data assert b'repo2-name' in response.data
assert b"owner2-login" in response.data assert b'owner2-login' in response.data
assert b"owner2-url" in response.data assert b'owner2-url' in response.data
assert b"owner2-avatar-url" in response.data assert b'owner2-avatar-url' in response.data
def test_index_no_results(client, app): def test_index_no_results(client, app):
@ -933,10 +1049,10 @@ and put next into it:
github_client_mock.search_repositories.return_value = [] github_client_mock.search_repositories.return_value = []
with app.container.github_client.override(github_client_mock): with app.container.github_client.override(github_client_mock):
response = client.get(url_for("index")) response = client.get(url_for('index'))
assert response.status_code == 200 assert response.status_code == 200
assert b"Results found: 0" in response.data assert b'Results found: 0' in response.data
Now let's run it and check the coverage: Now let's run it and check the coverage:
@ -948,23 +1064,23 @@ You should see:
.. code-block:: bash .. code-block:: bash
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
plugins: cov-3.0.0, flask-1.2.0 plugins: flask-1.0.0, cov-2.10.0
collected 2 items collected 2 items
githubnavigator/tests.py .. [100%] githubnavigator/tests.py .. [100%]
---------- coverage: platform darwin, python 3.10.0-final-0 ---------- ---------- coverage: platform darwin, python 3.8.3-final-0 -----------
Name Stmts Miss Cover Name Stmts Miss Cover
---------------------------------------------------- ----------------------------------------------------
githubnavigator/__init__.py 0 0 100% githubnavigator/__init__.py 0 0 100%
githubnavigator/application.py 13 0 100% githubnavigator/application.py 11 0 100%
githubnavigator/containers.py 8 0 100% githubnavigator/containers.py 13 0 100%
githubnavigator/services.py 14 0 100% githubnavigator/services.py 14 0 100%
githubnavigator/tests.py 34 0 100% githubnavigator/tests.py 32 0 100%
githubnavigator/views.py 10 0 100% githubnavigator/views.py 7 0 100%
---------------------------------------------------- ----------------------------------------------------
TOTAL 79 0 100% TOTAL 77 0 100%
.. note:: .. note::
@ -975,22 +1091,53 @@ You should see:
Conclusion Conclusion
---------- ----------
We are done.
In this tutorial we've built a ``Flask`` application following the dependency injection principle. 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. We've used the ``Dependency Injector`` as a dependency injection framework.
:ref:`containers` and :ref:`providers` helped to specify how to assemble search service and The main part of this application is the container. It keeps all the application components and
integrate it with a 3rd-party library. their dependencies defined explicitly in one place:
:ref:`configuration-provider` helped to deal with reading YAML file and environment variable. .. code-block:: python
We used :ref:`wiring` feature to inject the dependencies into the ``index()`` view. """Application containers module."""
:ref:`provider-overriding` feature helped in testing.
We kept all the dependencies injected explicitly. This will help when you need to add or from dependency_injector import containers, providers
change something in future. from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github
You can find complete project on the from . import services, views
`Github <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/flask>`_.
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,
)
What's next? What's next?
@ -998,6 +1145,5 @@ What's next?
- Know more about the :ref:`providers` - Know more about the :ref:`providers`
- Go to the :ref:`contents` - Go to the :ref:`contents`
.. include:: ../sponsor.rst
.. disqus:: .. disqus::

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

@ -1,698 +0,0 @@
.. _wiring:
Wiring
======
Wiring feature provides a way to inject container providers into the functions and methods.
To use wiring you need:
- **Place @inject decorator**. Decorator ``@inject`` injects the dependencies.
- **Place markers**. Wiring marker specifies what dependency 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-
.. contents::
:local:
:backlinks: none
Decorator @inject
-----------------
Decorator ``@inject`` injects the dependencies. Use it to decorate all functions and methods
with the injections.
.. code-block:: python
from dependency_injector.wiring import inject, Provide
@inject
def foo(bar: Bar = Provide[Container.bar]):
...
Decorator ``@inject`` must be specified as a very first decorator of a function to ensure that
the wiring works appropriately. This will also contribute to the performance of the wiring process.
.. code-block:: python
from dependency_injector.wiring import inject, Provide
@decorator_etc
@decorator_2
@decorator_1
@inject
def foo(bar: Bar = Provide[Container.bar]):
...
Specifying the ``@inject`` as a first decorator is also crucial for FastAPI, other frameworks
using decorators similarly, for closures, and for any types of custom decorators with the injections.
FastAPI example:
.. code-block:: python
app = FastAPI()
@app.api_route("/")
@inject
async def index(service: Annotated[Service, Depends(Provide[Container.service])]):
value = await service.process()
return {"result": value}
Decorators example:
.. code-block:: python
def decorator1(func):
@functools.wraps(func)
@inject
def wrapper(value1: int = Provide[Container.config.value1]):
result = func()
return result + value1
return wrapper
def decorator2(func):
@functools.wraps(func)
@inject
def wrapper(value2: int = Provide[Container.config.value2]):
result = func()
return result + value2
return wrapper
@decorator1
@decorator2
def sample():
...
.. seealso::
`Issue #404 <https://github.com/ets-labs/python-dependency-injector/issues/404#issuecomment-785216978>`_
explains ``@inject`` decorator in a few more details.
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 inject, Provide
@inject
def foo(bar: Bar = Provide[Container.bar]):
...
Specifying an annotation is optional.
To inject the provider itself use ``Provide[foo.provider]``:
.. code-block:: python
from dependency_injector.providers import Factory
from dependency_injector.wiring import inject, Provide
@inject
def foo(bar_provider: Factory[Bar] = Provide[Container.bar.provider]):
bar = bar_provider(argument="baz")
...
You can also use ``Provider[foo]`` for injecting the provider itself:
.. code-block:: python
from dependency_injector.providers import Factory
from dependency_injector.wiring import inject, Provider
@inject
def foo(bar_provider: Factory[Bar] = Provider[Container.bar]):
bar = bar_provider(argument="baz")
...
You can use configuration, provided instance and sub-container providers as you normally do.
.. code-block:: python
@inject
def foo(token: str = Provide[Container.config.api_token]):
...
@inject
def foo(timeout: int = Provide[Container.config.timeout.as_(int)]):
...
@inject
def foo(baz: Baz = Provide[Container.bar.provided.baz]):
...
@inject
def foo(bar: Bar = Provide[Container.subcontainer.bar]):
...
You can compound wiring and ``Resource`` provider to implement per-function execution scope.
See :ref:`Resources, wiring and per-function execution scope <resource-provider-wiring-closing>` for details.
Also you can use ``Provide`` marker to inject a container.
.. literalinclude:: ../examples/wiring/example_container.py
:language: python
:emphasize-lines: 14-17
:lines: 3-
String identifiers
------------------
You can use wiring with string identifiers. String identifier should match provider name in the container:
.. literalinclude:: ../examples/wiring/example_string_id.py
:language: python
:emphasize-lines: 15
:lines: 3-
With string identifiers you don't need to use a container to specify an injection.
To specify an injection from a nested container use point ``.`` as a separator:
.. code-block:: python
@inject
def foo(service: UserService = Provide["services.user"]) -> None:
...
You can also use injection modifiers:
.. code-block:: python
from dependency_injector.wiring import (
inject,
Provide,
as_int,
as_float,
as_,
required,
invariant,
provided,
)
@inject
def foo(value: int = Provide["config.option", as_int()]) -> None:
...
@inject
def foo(value: float = Provide["config.option", as_float()]) -> None:
...
@inject
def foo(value: Decimal = Provide["config.option", as_(Decimal)]) -> None:
...
@inject
def foo(value: str = Provide["config.option", required()]) -> None:
...
@inject
def foo(value: int = Provide["config.option", required().as_int()]) -> None:
...
@inject
def foo(value: int = Provide["config.option", invariant("config.switch")]) -> None:
...
@inject
def foo(value: int = Provide["service", provided().foo["bar"].call()]) -> None:
...
To inject a container use special identifier ``<container>``:
.. code-block:: python
@inject
def foo(container: Container = Provide["<container>"]) -> None:
...
Making injections into modules and class attributes
---------------------------------------------------
You can use wiring to make injections into modules and class attributes. Both the classic marker
syntax and the ``Annotated`` form are supported.
Classic marker syntax:
.. code-block:: python
service: Service = Provide[Container.service]
class Main:
service: Service = Provide[Container.service]
Full example of the classic marker syntax:
.. literalinclude:: ../examples/wiring/example_attribute.py
:language: python
:lines: 3-
:emphasize-lines: 14,19
Annotated form (Python 3.9+):
.. code-block:: python
from typing import Annotated
service: Annotated[Service, Provide[Container.service]]
class Main:
service: Annotated[Service, Provide[Container.service]]
Full example of the annotated form:
.. literalinclude:: ../examples/wiring/example_attribute_annotated.py
:language: python
:lines: 3-
:emphasize-lines: 16,21
You could also use string identifiers to avoid a dependency on a container:
.. code-block:: python
:emphasize-lines: 1,6
service: Service = Provide["service"]
class Main:
service: Service = Provide["service"]
Wiring with modules and packages
--------------------------------
To wire a container with the modules you need to call ``container.wire()`` method:
.. code-block:: python
container.wire(
modules=[
"yourapp.module1",
"yourapp.module2",
],
)
Method ``container.wire()`` can resolve relative imports:
.. code-block:: python
# In module "yourapp.main":
container.wire(
modules=[
".module1", # Resolved to: "yourapp.module1"
".module2", # Resolved to: "yourapp.module2"
],
)
You can also manually specify a base package for resolving relative imports with
the ``from_package`` argument:
.. code-block:: python
# In module "yourapp.main":
container.wire(
modules=[
".module1", # Resolved to: "anotherapp.module1"
".module2", # Resolved to: "anotherapp.module2"
],
from_package="anotherapp",
)
Argument ``modules`` can also take already imported modules:
.. 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 the package modules:
.. code-block:: python
container.wire(
packages=[
"yourapp.package1",
"yourapp.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
@inject
def foo(bar: Bar = Provide[Container.bar]):
...
container = Container()
container.wire(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=["yourapp.module1", "yourapp.module2"])
self.addCleanup(self.container.unwire)
.. code-block:: python
import pytest
@pytest.fixture
def container():
container = Container()
container.wire(modules=["yourapp.module1", "yourapp.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 individually imported functions. To protect from errors
prefer importing modules to importing individual functions or make sure imports happen
after the wiring:
.. code-block:: python
# Potential error:
from .module import fn
fn()
Instead use next:
.. code-block:: python
# Always works:
from . import module
module.fn()
Wiring configuration
--------------------
You can specify wiring configuration in the container. When wiring configuration is defined,
container will call method ``.wire()`` automatically when you create an instance:
.. code-block:: python
class Container(containers.DeclarativeContainer):
wiring_config = containers.WiringConfiguration(
modules=[
"yourapp.module1",
"yourapp.module2",
],
packages=[
"yourapp.package1",
"yourapp.package2",
],
)
...
if __name__ == "__main__":
container = Container() # container.wire() is called automatically
...
You can also use relative imports. Container will resolve them corresponding
to the module of the container class:
.. code-block:: python
# In module "yourapp.container":
class Container(containers.DeclarativeContainer):
wiring_config = containers.WiringConfiguration(
modules=[
".module1", # Resolved to: "yourapp.module1"
".module2", # Resolved to: "yourapp.module2"
],
)
)
# In module "yourapp.foo.bar.main":
if __name__ == "__main__":
container = Container() # wire to "yourapp.module1" and "yourapp.module2"
...
To use wiring configuration and call method ``.wire()`` manually, set flag ``auto_wire=False``:
.. code-block:: python
:emphasize-lines: 5
class Container(containers.DeclarativeContainer):
wiring_config = containers.WiringConfiguration(
modules=["yourapp.module1"],
auto_wire=False,
)
if __name__ == "__main__":
container = Container() # container.wire() is NOT called automatically
container.wire() # wire to "yourapp.module1"
...
.. _async-injections-wiring:
Asynchronous injections
-----------------------
Wiring feature supports asynchronous injections:
.. code-block:: python
class Container(containers.DeclarativeContainer):
db = providers.Resource(init_async_db_client)
cache = providers.Resource(init_async_cache_client)
@inject
async def main(
db: Database = Provide[Container.db],
cache: Cache = Provide[Container.cache],
):
...
When you call asynchronous function wiring prepares injections asynchronously.
Here is what it does for previous example:
.. code-block:: python
db, cache = await asyncio.gather(
container.db(),
container.cache(),
)
await main(db=db, cache=cache)
You can also use ``Closing`` marker with the asynchronous ``Resource`` providers:
.. code-block:: python
@inject
async def main(
db: Database = Closing[Provide[Container.db]],
cache: Cache = Closing[Provide[Container.cache]],
):
...
Wiring does closing asynchronously:
.. code-block:: python
db, cache = await asyncio.gather(
container.db(),
container.cache(),
)
await main(db=db, cache=cache)
await asyncio.gather(
container.db.shutdown(),
container.cache.shutdown(),
)
See :ref:`Resources, wiring and per-function execution scope <resource-provider-wiring-closing>` for
details on ``Closing`` marker.
.. note::
Wiring does not not convert asynchronous injections to synchronous.
It handles asynchronous injections only for ``async def`` functions. Asynchronous injections into
synchronous ``def`` function still work, but you need to take care of awaitables by your own.
See also:
- Provider :ref:`async-injections`
- Resource provider :ref:`resource-async-initializers`
- :ref:`fastapi-redis-example`
Wiring of dynamically imported modules
--------------------------------------
You can install an import hook that automatically wires containers to the imported modules.
This is useful when you import modules dynamically.
.. code-block:: python
import importlib
from dependency_injector.wiring import register_loader_containers
from .containers import Container
if __name__ == "__main__":
container = Container()
register_loader_containers(container) # <--- installs import hook
module = importlib.import_module("package.module")
module.foo()
You can register multiple containers in the import hook. For doing this call register function
with multiple containers ``register_loader_containers(container1, container2, ...)``
or with a single container ``register_loader_containers(container)`` multiple times.
To unregister a container use ``unregister_loader_containers(container)``.
Wiring module will uninstall the import hook when unregister last container.
Few notes on performance
------------------------
``.wire()`` utilize caching to speed up the wiring process. At the end it clears the cache to avoid memory leaks.
But this may not always be desirable, when you want to keep the cache for the next wiring
(e.g. due to usage of multiple containers or during unit tests).
To keep the cache after wiring, you can set flag ``keep_cache=True`` (works with ``WiringConfiguration`` too):
.. code-block:: python
container1.wire(
modules=["yourapp.module1", "yourapp.module2"],
keep_cache=True,
)
container2.wire(
modules=["yourapp.module2", "yourapp.module3"],
keep_cache=True,
)
...
and then clear it manually when you need it:
.. code-block:: python
from dependency_injector.wiring import clear_cache
clear_cache()
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:`boto3-example`
- :ref:`django-example`
- :ref:`flask-example`
- :ref:`flask-blueprints-example`
- :ref:`aiohttp-example`
- :ref:`sanic-example`
- :ref:`fastapi-example`
- :ref:`fastapi-redis-example`
- :ref:`fastapi-sqlalchemy-example`
- :ref:`fastdepends-example`
.. disqus::

View File

@ -1,15 +0,0 @@
"""Container dependencies check example."""
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
service1 = providers.Dependency()
service2 = providers.Dependency()
if __name__ == "__main__":
container = Container()
container.check_dependencies() # <-- raises error:
# Container has undefined dependencies: "Container.service1", "Container.service2"

View File

@ -10,7 +10,7 @@ class Container(containers.DeclarativeContainer):
factory2 = providers.Factory(object) factory2 = providers.Factory(object)
if __name__ == "__main__": if __name__ == '__main__':
container = Container() container = Container()
object1 = container.factory1() object1 = container.factory1()
@ -18,6 +18,6 @@ if __name__ == "__main__":
print(container.providers) print(container.providers)
# { # {
# "factory1": <dependency_injector.providers.Factory(...), # 'factory1': <dependency_injector.providers.Factory(...),
# "factory2": <dependency_injector.providers.Factory(...), # 'factory2': <dependency_injector.providers.Factory(...),
# } # }

View File

@ -1,31 +0,0 @@
"""Declarative container provider copying with ``@copy()`` decorator."""
import sqlite3
from unittest import mock
from dependency_injector import containers, providers
class Service:
def __init__(self, db):
self.db = db
class SourceContainer(containers.DeclarativeContainer):
database = providers.Singleton(sqlite3.connect, ":memory:")
service = providers.Factory(Service, db=database)
# Copy ``SourceContainer`` providers into ``DestinationContainer``:
@containers.copy(SourceContainer)
class DestinationContainer(SourceContainer):
database = providers.Singleton(mock.Mock)
if __name__ == "__main__":
container = DestinationContainer()
service = container.service()
assert isinstance(service.db, mock.Mock)

View File

@ -1,33 +0,0 @@
"""Declarative container provider copying with ``@copy()`` decorator."""
from dependency_injector import containers, providers
class Service:
def __init__(self, dependency: str):
self.dependency = dependency
class Base(containers.DeclarativeContainer):
dependency = providers.Dependency(instance_of=str, default="Default value")
service = providers.Factory(Service, dependency=dependency)
@containers.copy(Base)
class Derived1(Base):
dependency = providers.Dependency(instance_of=str, default="Derived 1")
# @containers.copy(Base) # <-- No @copy decorator
class Derived2(Base):
dependency = providers.Dependency(instance_of=str, default="Derived 2")
if __name__ == "__main__":
container1 = Derived1()
service1 = container1.service()
print(service1.dependency) # Derived 1
container2 = Derived2()
service2 = container2.service()
print(service2.dependency) # Default value

View File

@ -14,21 +14,21 @@ class ContainerB(ContainerA):
assert ContainerA.providers == { assert ContainerA.providers == {
"provider1": ContainerA.provider1, 'provider1': ContainerA.provider1,
} }
assert ContainerB.providers == { assert ContainerB.providers == {
"provider1": ContainerA.provider1, 'provider1': ContainerA.provider1,
"provider2": ContainerB.provider2, 'provider2': ContainerB.provider2,
} }
assert ContainerA.cls_providers == { assert ContainerA.cls_providers == {
"provider1": ContainerA.provider1, 'provider1': ContainerA.provider1,
} }
assert ContainerB.cls_providers == { assert ContainerB.cls_providers == {
"provider2": ContainerB.provider2, 'provider2': ContainerB.provider2,
} }
assert ContainerA.inherited_providers == {} assert ContainerA.inherited_providers == {}
assert ContainerB.inherited_providers == { assert ContainerB.inherited_providers == {
"provider1": ContainerA.provider1, 'provider1': ContainerA.provider1,
} }

View File

@ -18,7 +18,7 @@ class AuthService:
class Container(containers.DeclarativeContainer): class Container(containers.DeclarativeContainer):
database = providers.Singleton(sqlite3.connect, ":memory:") database = providers.Singleton(sqlite3.connect, ':memory:')
user_service = providers.Factory( user_service = providers.Factory(
UserService, UserService,
@ -32,7 +32,7 @@ class Container(containers.DeclarativeContainer):
) )
if __name__ == "__main__": if __name__ == '__main__':
container = Container() container = Container()
user_service = container.user_service() user_service = container.user_service()

View File

@ -1,25 +0,0 @@
"""Declarative container provider overriding with ``@override()`` decorator."""
import sqlite3
from unittest import mock
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
database = providers.Singleton(sqlite3.connect, ":memory:")
# Overriding ``Container`` with ``OverridingContainer``:
@containers.override(Container)
class OverridingContainer(containers.DeclarativeContainer):
database = providers.Singleton(mock.Mock)
if __name__ == "__main__":
container = Container()
database = container.database()
assert isinstance(database, mock.Mock)

View File

@ -8,10 +8,10 @@ from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer): class Container(containers.DeclarativeContainer):
database = providers.Singleton(sqlite3.connect, ":memory:") database = providers.Singleton(sqlite3.connect, ':memory:')
if __name__ == "__main__": if __name__ == '__main__':
container = Container(database=mock.Mock(sqlite3.Connection)) container = Container(database=mock.Mock(sqlite3.Connection))
database = container.database() database = container.database()

View File

@ -3,7 +3,7 @@
from dependency_injector import containers, providers from dependency_injector import containers, providers
if __name__ == "__main__": if __name__ == '__main__':
container = containers.DynamicContainer() container = containers.DynamicContainer()
container.factory1 = providers.Factory(object) container.factory1 = providers.Factory(object)
container.factory2 = providers.Factory(object) container.factory2 = providers.Factory(object)
@ -13,6 +13,6 @@ if __name__ == "__main__":
print(container.providers) print(container.providers)
# { # {
# "factory1": <dependency_injector.providers.Factory(...), # 'factory1': <dependency_injector.providers.Factory(...),
# "factory2": <dependency_injector.providers.Factory(...), # 'factory2': <dependency_injector.providers.Factory(...),
# } # }

View File

@ -13,20 +13,20 @@ class AuthService:
def populate_container(container, providers_config): def populate_container(container, providers_config):
for provider_name, provider_info in providers_config.items(): for provider_name, provider_info in providers_config.items():
provided_cls = globals().get(provider_info["class"]) provided_cls = globals().get(provider_info['class'])
provider_cls = getattr(providers, provider_info["provider_class"]) provider_cls = getattr(providers, provider_info['provider_class'])
setattr(container, provider_name, provider_cls(provided_cls)) setattr(container, provider_name, provider_cls(provided_cls))
if __name__ == "__main__": if __name__ == '__main__':
services_config = { services_config = {
"user": { 'user': {
"class": "UserService", 'class': 'UserService',
"provider_class": "Factory", 'provider_class': 'Factory',
}, },
"auth": { 'auth': {
"class": "AuthService", 'class': 'AuthService',
"provider_class": "Factory", 'provider_class': 'Factory',
}, },
} }
services = containers.DynamicContainer() services = containers.DynamicContainer()

View File

@ -1,36 +0,0 @@
"""Container injecting ``self`` example."""
from dependency_injector import containers, providers
class Service:
def __init__(self, name: str):
self.name = name
class ServiceDispatcher:
def __init__(self, container: containers.Container):
self.container = container
def get_services(self):
for provider in self.container.traverse(types=[providers.Factory]):
yield provider()
class Container(containers.DeclarativeContainer):
__self__ = providers.Self()
service1 = providers.Factory(Service, name="Service 1")
service2 = providers.Factory(Service, name="Service 2")
service3 = providers.Factory(Service, name="Service 3")
dispatcher = providers.Singleton(ServiceDispatcher, __self__)
if __name__ == "__main__":
container = Container()
dispatcher = container.dispatcher()
for service in dispatcher.get_services():
print(service.name)

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