Wiring reengineering (#324)

* Bump version to 4.3.9: FastAPI example

* Reengineer wiring

* Add @inject decorator

* Add .workspace dir to gitignore

* Add generic typing for @inject

* Add type cast for @inject

* Update movie lister example

* Update cli application tutorial

* Update demo example

* Update wiring docs and examples

* Update aiohttp example and tutorial

* Update multiple containers example

* Update single container example

* Update decoupled packages example

* Update django example

* Update asyncio daemon example and tutorial

* Update FastAPI example

* Update flask example and tutorial

* Update sanic example

* Add wiring registry

* Add new line to .gitignore

* Add @inject to the test samples

* Fix flake8 errors
This commit is contained in:
Roman Mogylatov 2020-11-15 16:06:42 -05:00 committed by GitHub
parent bece33fc21
commit ae3024588c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 336 additions and 112 deletions

3
.gitignore vendored
View File

@ -69,3 +69,6 @@ src/dependency_injector/containers/*.h
src/dependency_injector/containers/*.so
src/dependency_injector/providers/*.h
src/dependency_injector/providers/*.so
# Workspace for samples
.workspace/

View File

@ -68,7 +68,7 @@ Key features of the ``Dependency Injector``:
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>`_.
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with
other frameworks: Django, Flask, Aiohttp, etc.
other frameworks: Django, Flask, Aiohttp, Sanic, FastAPI, etc.
See `Wiring <https://python-dependency-injector.ets-labs.org/wiring.html>`_.
- **Typing**. Provides typing stubs, ``mypy``-friendly.
See `Typing and mypy <https://python-dependency-injector.ets-labs.org/providers/typing_mypy.html>`_.
@ -78,7 +78,7 @@ Key features of the ``Dependency Injector``:
.. code-block:: python
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
class Container(containers.DeclarativeContainer):
@ -97,6 +97,7 @@ Key features of the ``Dependency Injector``:
)
@inject
def main(service: Service = Provide[Container.service]):
...

79
docs/examples/fastapi.rst Normal file
View File

@ -0,0 +1,79 @@
.. _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>`_.
.. disqus::

View File

@ -17,5 +17,6 @@ Explore the examples to see the ``Dependency Injector`` in action.
flask
aiohttp
sanic
fastapi
.. disqus::

View File

@ -77,7 +77,7 @@ Key features of the ``Dependency Injector``:
See :ref:`resource-provider`.
- **Containers**. Provides declarative and dynamic containers. See :ref:`containers`.
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with
other frameworks: Django, Flask, Aiohttp, etc. See :ref:`wiring`.
other frameworks: Django, Flask, Aiohttp, Sanic, FastAPI, etc. See :ref:`wiring`.
- **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`.
- **Performance**. Fast. Written in ``Cython``.
- **Maturity**. Mature and production-ready. Well-tested, documented and supported.
@ -85,7 +85,7 @@ Key features of the ``Dependency Injector``:
.. code-block:: python
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
class Container(containers.DeclarativeContainer):
@ -104,6 +104,7 @@ Key features of the ``Dependency Injector``:
)
@inject
def main(service: Service = Provide[Container.service]):
...

View File

@ -162,7 +162,7 @@ the dependency.
.. code-block:: python
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
class Container(containers.DeclarativeContainer):
@ -181,6 +181,7 @@ the dependency.
)
@inject
def main(service: Service = Provide[Container.service]):
...
@ -284,6 +285,7 @@ Choose one of the following as a next step:
- :ref:`flask-example`
- :ref:`aiohttp-example`
- :ref:`sanic-example`
- :ref:`fastapi-example`
- Pass the tutorials:
- :ref:`flask-tutorial`
- :ref:`aiohttp-tutorial`

View File

@ -23,7 +23,7 @@ Key features of the ``Dependency Injector``:
See :ref:`resource-provider`.
- **Containers**. Provides declarative and dynamic containers. See :ref:`containers`.
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with
other frameworks: Django, Flask, Aiohttp, etc. See :ref:`wiring`.
other frameworks: Django, Flask, Aiohttp, Sanic, FastAPI, etc. See :ref:`wiring`.
- **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`.
- **Performance**. Fast. Written in ``Cython``.
- **Maturity**. Mature and production-ready. Well-tested, documented and supported.

View File

@ -7,6 +7,10 @@ that were made in every particular version.
From version 0.7.6 *Dependency Injector* framework strictly
follows `Semantic versioning`_
4.3.9
-----
- Add ``FastAPI`` example.
4.3.8
-----
- Add a hotfix to support wiring for ``FastAPI`` endpoints.

View File

@ -216,7 +216,7 @@ execution scope. For doing this you need to use additional ``Closing`` marker fr
.. literalinclude:: ../../examples/wiring/flask_resource_closing.py
:language: python
:lines: 3-
:emphasize-lines: 23
:emphasize-lines: 24
Framework initializes and injects the resource into the function. With the ``Closing`` marker
framework calls resource ``shutdown()`` method when function execution is over.

View File

@ -526,17 +526,18 @@ the ``index`` handler. We will use :ref:`wiring` feature.
Edit ``handlers.py``:
.. code-block:: python
:emphasize-lines: 4-7,10-13,17
:emphasize-lines: 4-7,10-14,18
"""Handlers module."""
from aiohttp import web
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from .services import SearchService
from .containers import Container
@inject
async def index(
request: web.Request,
search_service: SearchService = Provide[Container.search_service],
@ -645,17 +646,18 @@ Let's make some refactoring. We will move these values to the config.
Edit ``handlers.py``:
.. code-block:: python
:emphasize-lines: 13-14,16-17
:emphasize-lines: 14-15,17-18
"""Handlers module."""
from aiohttp import web
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from .services import SearchService
from .containers import Container
@inject
async def index(
request: web.Request,
search_service: SearchService = Provide[Container.search_service],
@ -821,11 +823,11 @@ You should see:
giphynavigator/application.py 12 0 100%
giphynavigator/containers.py 6 0 100%
giphynavigator/giphy.py 14 9 36%
giphynavigator/handlers.py 9 0 100%
giphynavigator/handlers.py 10 0 100%
giphynavigator/services.py 9 1 89%
giphynavigator/tests.py 37 0 100%
---------------------------------------------------
TOTAL 87 10 89%
TOTAL 88 10 89%
.. note::

View File

@ -442,18 +442,19 @@ and call the ``run()`` method. We will use :ref:`wiring` feature.
Edit ``__main__.py``:
.. code-block:: python
:emphasize-lines: 3-7,11-12,19
:emphasize-lines: 3-7,11-13,20
"""Main module."""
import sys
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from .dispatcher import Dispatcher
from .containers import Container
@inject
def main(dispatcher: Dispatcher = Provide[Container.dispatcher]) -> None:
dispatcher.run()
@ -992,14 +993,14 @@ You should see:
Name Stmts Miss Cover
----------------------------------------------------
monitoringdaemon/__init__.py 0 0 100%
monitoringdaemon/__main__.py 12 12 0%
monitoringdaemon/__main__.py 13 13 0%
monitoringdaemon/containers.py 11 0 100%
monitoringdaemon/dispatcher.py 44 5 89%
monitoringdaemon/http.py 6 3 50%
monitoringdaemon/monitors.py 23 1 96%
monitoringdaemon/tests.py 37 0 100%
----------------------------------------------------
TOTAL 133 21 84%
TOTAL 134 22 84%
.. note::

View File

@ -575,18 +575,19 @@ Let's inject the ``lister`` into the ``main()`` function.
Edit ``__main__.py``:
.. code-block:: python
:emphasize-lines: 3-7,11,18
:emphasize-lines: 3-7,11-12,19
"""Main module."""
import sys
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from .listers import MovieLister
from .containers import Container
@inject
def main(lister: MovieLister = Provide[Container.lister]) -> None:
...
@ -606,18 +607,19 @@ Francis Lawrence and movies released in 2016.
Edit ``__main__.py``:
.. code-block:: python
:emphasize-lines: 12-18
:emphasize-lines: 13-19
"""Main module."""
import sys
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
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'):
@ -861,18 +863,19 @@ Now we need to read the value of the ``config.finder.type`` option from the envi
Edit ``__main__.py``:
.. code-block:: python
:emphasize-lines: 24
:emphasize-lines: 25
"""Main module."""
import sys
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
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'):
@ -1023,14 +1026,14 @@ You should see:
Name Stmts Miss Cover
------------------------------------------
movies/__init__.py 0 0 100%
movies/__main__.py 17 17 0%
movies/__main__.py 18 18 0%
movies/containers.py 9 0 100%
movies/entities.py 7 1 86%
movies/finders.py 26 13 50%
movies/listers.py 8 0 100%
movies/tests.py 24 0 100%
------------------------------------------
TOTAL 91 31 66%
TOTAL 92 32 65%
.. note::

View File

@ -707,17 +707,19 @@ Let's inject ``SearchService`` into the ``index`` view. We will use :ref:`Wiring
Edit ``views.py``:
.. code-block:: python
:emphasize-lines: 4,6-7,10,14
:emphasize-lines: 4,6-7,10-11,15
:emphasize-lines: 4,6-7,10-11,15
"""Views module."""
from flask import request, render_template
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from .services import SearchService
from .containers import Container
@inject
def index(search_service: SearchService = Provide[Container.search_service]):
query = request.args.get('query', 'Dependency Injector')
limit = request.args.get('limit', 10, int)
@ -783,17 +785,18 @@ Let's make some refactoring. We will move these values to the config.
Edit ``views.py``:
.. code-block:: python
:emphasize-lines: 10-16
:emphasize-lines: 11-17
"""Views module."""
from flask import request, render_template
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from .services import SearchService
from .containers import Container
@inject
def index(
search_service: SearchService = Provide[Container.search_service],
default_query: str = Provide[Container.config.default.query],
@ -972,9 +975,9 @@ You should see:
githubnavigator/containers.py 7 0 100%
githubnavigator/services.py 14 0 100%
githubnavigator/tests.py 34 0 100%
githubnavigator/views.py 9 0 100%
githubnavigator/views.py 10 0 100%
----------------------------------------------------
TOTAL 79 0 100%
TOTAL 80 0 100%
.. note::

View File

@ -7,7 +7,8 @@ Wiring feature provides a way to inject container providers into the functions a
To use wiring you need:
- **Place markers in the code**. Wiring marker specifies what provider to inject,
- **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.
@ -25,9 +26,10 @@ a function or method argument:
.. code-block:: python
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
@inject
def foo(bar: Bar = Provide[Container.bar]):
...
@ -40,9 +42,10 @@ There are two types of markers:
.. code-block:: python
from dependency_injector.wiring import Provider
from dependency_injector.wiring import inject, Provider
@inject
def foo(bar_provider: Callable[..., Bar] = Provider[Container.bar]):
bar = bar_provider()
...
@ -51,18 +54,22 @@ You can use configuration, provided instance and sub-container providers as you
.. 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]):
...
@ -100,6 +107,7 @@ When wiring is done functions and methods with the markers are patched to provid
.. code-block:: python
@inject
def foo(bar: Bar = Provide[Container.bar]):
...
@ -201,5 +209,6 @@ Take a look at other application examples:
- :ref:`flask-example`
- :ref:`aiohttp-example`
- :ref:`sanic-example`
- :ref:`fastapi-example`
.. disqus::

View File

@ -2,7 +2,7 @@ import sys
from unittest import mock
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from after import ApiClient, Service
@ -23,6 +23,7 @@ class Container(containers.DeclarativeContainer):
)
@inject
def main(service: Service = Provide[Container.service]):
...

View File

@ -111,8 +111,8 @@ The output should be something like:
giphynavigator/application.py 12 0 100%
giphynavigator/containers.py 6 0 100%
giphynavigator/giphy.py 14 9 36%
giphynavigator/handlers.py 9 0 100%
giphynavigator/handlers.py 10 0 100%
giphynavigator/services.py 9 1 89%
giphynavigator/tests.py 37 0 100%
---------------------------------------------------
TOTAL 87 10 89%
TOTAL 88 10 89%

View File

@ -1,12 +1,13 @@
"""Handlers module."""
from aiohttp import web
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from .services import SearchService
from .containers import Container
@inject
async def index(
request: web.Request,
search_service: SearchService = Provide[Container.search_service],

View File

@ -2,12 +2,13 @@
import sys
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from .services import UserService, AuthService, PhotoService
from .containers import Application
@inject
def main(
email: str,
password: str,

View File

@ -2,12 +2,13 @@
import sys
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from .services import UserService, AuthService, PhotoService
from .containers import Container
@inject
def main(
email: str,
password: str,

View File

@ -76,11 +76,11 @@ The output should be something like:
Name Stmts Miss Cover
----------------------------------------------------
monitoringdaemon/__init__.py 0 0 100%
monitoringdaemon/__main__.py 12 12 0%
monitoringdaemon/__main__.py 13 13 0%
monitoringdaemon/containers.py 11 0 100%
monitoringdaemon/dispatcher.py 44 5 89%
monitoringdaemon/http.py 6 3 50%
monitoringdaemon/monitors.py 23 1 96%
monitoringdaemon/tests.py 37 0 100%
----------------------------------------------------
TOTAL 133 21 84%
TOTAL 134 22 84%

View File

@ -2,12 +2,13 @@
import sys
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from .dispatcher import Dispatcher
from .containers import Container
@inject
def main(dispatcher: Dispatcher = Provide[Container.dispatcher]) -> None:
dispatcher.run()

View File

@ -2,7 +2,7 @@
import sys
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from .user.repositories import UserRepository
from .photo.repositories import PhotoRepository
@ -10,6 +10,7 @@ from .analytics.services import AggregationService
from .containers import ApplicationContainer
@inject
def main(
user_repository: UserRepository = Provide[
ApplicationContainer.user_package.user_repository

View File

@ -108,6 +108,6 @@ The output should be something like:
web/apps.py 7 0 100%
web/tests.py 28 0 100%
web/urls.py 3 0 100%
web/views.py 11 0 100%
web/views.py 12 0 100%
---------------------------------------------------
TOTAL 120 10 92%
TOTAL 121 10 92%

View File

@ -4,12 +4,13 @@ from typing import List
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from githubnavigator.containers import Container
from githubnavigator.services import SearchService
@inject
def index(
request: HttpRequest,
search_service: SearchService = Provide[Container.search_service],

View File

@ -113,9 +113,9 @@ The output should be something like:
giphynavigator/__init__.py 0 0 100%
giphynavigator/application.py 13 0 100%
giphynavigator/containers.py 6 0 100%
giphynavigator/endpoints.py 5 0 100%
giphynavigator/endpoints.py 6 0 100%
giphynavigator/giphy.py 14 9 36%
giphynavigator/services.py 9 1 89%
giphynavigator/tests.py 38 0 100%
---------------------------------------------------
TOTAL 85 10 88%
TOTAL 86 10 88%

View File

@ -1,10 +1,11 @@
"""Endpoints module."""
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from .containers import Container
@inject
async def index(
query: str = Provide[Container.config.default.query],
limit: int = Provide[Container.config.default.limit.as_int()],

View File

@ -95,6 +95,6 @@ The output should be something like:
githubnavigator/containers.py 7 0 100%
githubnavigator/services.py 14 0 100%
githubnavigator/tests.py 34 0 100%
githubnavigator/views.py 9 0 100%
githubnavigator/views.py 10 0 100%
----------------------------------------------------
TOTAL 79 0 100%
TOTAL 80 0 100%

View File

@ -1,12 +1,13 @@
"""Views module."""
from flask import request, render_template
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from .services import SearchService
from .containers import Container
@inject
def index(
search_service: SearchService = Provide[Container.search_service],
default_query: str = Provide[Container.config.default.query],

View File

@ -68,11 +68,11 @@ The output should be something like:
Name Stmts Miss Cover
------------------------------------------
movies/__init__.py 0 0 100%
movies/__main__.py 17 17 0%
movies/__main__.py 18 18 0%
movies/containers.py 9 0 100%
movies/entities.py 7 1 86%
movies/finders.py 26 13 50%
movies/listers.py 8 0 100%
movies/tests.py 24 0 100%
------------------------------------------
TOTAL 91 31 66%
TOTAL 92 32 65%

View File

@ -2,12 +2,13 @@
import sys
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
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'):

View File

@ -112,8 +112,8 @@ The output should be something like:
giphynavigator/application.py 12 0 100%
giphynavigator/containers.py 6 0 100%
giphynavigator/giphy.py 14 9 36%
giphynavigator/handlers.py 10 0 100%
giphynavigator/handlers.py 11 0 100%
giphynavigator/services.py 9 1 89%
giphynavigator/tests.py 34 0 100%
---------------------------------------------------
TOTAL 89 14 84%
TOTAL 90 14 84%

View File

@ -2,12 +2,13 @@
from sanic.request import Request
from sanic.response import HTTPResponse, json
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from .services import SearchService
from .containers import Container
@inject
async def index(
request: Request,
search_service: SearchService = Provide[Container.search_service],

View File

@ -3,7 +3,7 @@
import sys
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
class Service:
@ -15,6 +15,7 @@ class Container(containers.DeclarativeContainer):
service = providers.Factory(Service)
@inject
def main(service: Service = Provide[Container.service]) -> None:
...

View File

@ -3,7 +3,7 @@
import sys
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from flask import Flask, json
@ -16,6 +16,7 @@ class Container(containers.DeclarativeContainer):
service = providers.Factory(Service)
@inject
def index_view(service: Service = Provide[Container.service]) -> str:
return json.dumps({'service_id': id(service)})

View File

@ -3,7 +3,7 @@
import sys
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, Closing
from dependency_injector.wiring import inject, Provide, Closing
from flask import Flask, current_app
@ -22,6 +22,7 @@ class Container(containers.DeclarativeContainer):
service = providers.Resource(init_service)
@inject
def index_view(service: Service = Closing[Provide[Container.service]]):
assert service is current_app.container.service()
return 'Hello World!'

View File

@ -1,6 +1,6 @@
"""Top-level package."""
__version__ = '4.3.8'
__version__ = '4.3.9'
"""Version number.
:type: str

View File

@ -6,7 +6,19 @@ import importlib
import pkgutil
import sys
from types import ModuleType
from typing import Optional, Iterable, Callable, Any, Tuple, Dict, Generic, TypeVar, Type, cast
from typing import (
Optional,
Iterable,
Iterator,
Callable,
Any,
Tuple,
Dict,
Generic,
TypeVar,
Type,
cast,
)
if sys.version_info < (3, 7):
from typing import GenericMeta
@ -21,15 +33,35 @@ from . import providers
__all__ = (
'wire',
'unwire',
'inject',
'Provide',
'Provider',
'Closing',
)
T = TypeVar('T')
F = TypeVar('F', bound=Callable[..., Any])
Container = Any
class Registry:
def __init__(self):
self._storage = set()
def add(self, patched: Callable[..., Any]) -> None:
self._storage.add(patched)
def get_from_module(self, module: ModuleType) -> Iterator[Callable[..., Any]]:
for patched in self._storage:
if patched.__module__ != module.__name__:
continue
yield patched
_patched_registry = Registry()
class ProvidersMap:
def __init__(self, container):
@ -152,7 +184,7 @@ class ProvidersMap:
return providers_map
def wire(
def wire( # noqa: C901
container: Container,
*,
modules: Optional[Iterable[ModuleType]] = None,
@ -179,6 +211,9 @@ def wire(
for method_name, method in inspect.getmembers(member, _is_method):
_patch_method(member, method_name, method, providers_map)
for patched in _patched_registry.get_from_module(module):
_bind_injections(patched, providers_map)
def unwire(
*,
@ -201,6 +236,17 @@ def unwire(
for method_name, method in inspect.getmembers(member, inspect.isfunction):
_unpatch(member, method_name, method)
for patched in _patched_registry.get_from_module(module):
_unbind_injections(patched)
def inject(fn: F) -> F:
"""Decorate callable with injecting decorator."""
reference_injections, reference_closing = _fetch_reference_injections(fn)
patched = _get_patched(fn, reference_injections, reference_closing)
_patched_registry.add(patched)
return cast(F, patched)
def _patch_fn(
module: ModuleType,
@ -208,11 +254,16 @@ def _patch_fn(
fn: Callable[..., Any],
providers_map: ProvidersMap,
) -> None:
injections, closing = _resolve_injections(fn, providers_map)
if not injections:
if not _is_patched(fn):
reference_injections, reference_closing = _fetch_reference_injections(fn)
if not reference_injections:
return
patched = _patch_with_injections(fn, injections, closing)
setattr(module, name, _wrap_patched(patched, fn, injections, closing))
fn = _get_patched(fn, reference_injections, reference_closing)
_patched_registry.add(fn)
_bind_injections(fn, providers_map)
setattr(module, name, fn)
def _patch_method(
@ -221,28 +272,27 @@ def _patch_method(
method: Callable[..., Any],
providers_map: ProvidersMap,
) -> None:
injections, closing = _resolve_injections(method, providers_map)
if not injections:
return
if hasattr(cls, '__dict__') \
and name in cls.__dict__ \
and isinstance(cls.__dict__[name], (classmethod, staticmethod)):
method = cls.__dict__[name]
patched = _patch_with_injections(method.__func__, injections, closing)
patched = type(method)(patched)
fn = method.__func__
else:
patched = _patch_with_injections(method, injections, closing)
fn = method
setattr(cls, name, _wrap_patched(patched, method, injections, closing))
if not _is_patched(fn):
reference_injections, reference_closing = _fetch_reference_injections(fn)
if not reference_injections:
return
fn = _get_patched(fn, reference_injections, reference_closing)
_patched_registry.add(fn)
_bind_injections(fn, providers_map)
def _wrap_patched(patched: Callable[..., Any], original, injections, closing):
patched.__wired__ = True
patched.__original__ = original
patched.__injections__ = injections
patched.__closing__ = closing
return patched
if isinstance(method, (classmethod, staticmethod)):
fn = type(method)(fn)
setattr(cls, name, fn)
def _unpatch(
@ -250,14 +300,20 @@ def _unpatch(
name: str,
fn: Callable[..., Any],
) -> None:
if hasattr(module, '__dict__') \
and name in module.__dict__ \
and isinstance(module.__dict__[name], (classmethod, staticmethod)):
method = module.__dict__[name]
fn = method.__func__
if not _is_patched(fn):
return
setattr(module, name, _get_original_from_patched(fn))
_unbind_injections(fn)
def _resolve_injections(
def _fetch_reference_injections(
fn: Callable[..., Any],
providers_map: ProvidersMap,
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
signature = inspect.signature(fn)
@ -268,24 +324,33 @@ def _resolve_injections(
continue
marker = parameter.default
closing_modifier = False
if isinstance(marker, Closing):
closing_modifier = True
marker = marker.provider
closing[parameter_name] = marker
injections[parameter_name] = marker
return injections, closing
def _bind_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> None:
for injection, marker in fn.__reference_injections__.items():
provider = providers_map.resolve_provider(marker.provider)
if provider is None:
continue
if closing_modifier:
closing[parameter_name] = provider
if isinstance(marker, Provide):
injections[parameter_name] = provider
fn.__injections__[injection] = provider
elif isinstance(marker, Provider):
injections[parameter_name] = provider.provider
fn.__injections__[injection] = provider.provider
return injections, closing
if injection in fn.__reference_closing__:
fn.__closing__[injection] = provider
def _unbind_injections(fn: Callable[..., Any]) -> None:
fn.__injections__ = {}
fn.__closing__ = {}
def _fetch_modules(package):
@ -303,26 +368,34 @@ def _is_method(member):
return inspect.ismethod(member) or inspect.isfunction(member)
def _patch_with_injections(fn, injections, closing):
def _get_patched(fn, reference_injections, reference_closing):
if inspect.iscoroutinefunction(fn):
_patched = _get_async_patched(fn, injections, closing)
patched = _get_async_patched(fn)
else:
_patched = _get_patched(fn, injections, closing)
return _patched
patched = _get_sync_patched(fn)
patched.__wired__ = True
patched.__original__ = fn
patched.__injections__ = {}
patched.__reference_injections__ = reference_injections
patched.__closing__ = {}
patched.__reference_closing__ = reference_closing
return patched
def _get_patched(fn, injections, closing):
def _get_sync_patched(fn):
@functools.wraps(fn)
def _patched(*args, **kwargs):
to_inject = kwargs.copy()
for injection, provider in injections.items():
for injection, provider in _patched.__injections__.items():
if injection not in kwargs \
or _is_fastapi_default_arg_injection(injection, kwargs):
to_inject[injection] = provider()
result = fn(*args, **to_inject)
for injection, provider in closing.items():
for injection, provider in _patched.__closing__.items():
if injection in kwargs \
and not _is_fastapi_default_arg_injection(injection, kwargs):
continue
@ -334,18 +407,18 @@ def _get_patched(fn, injections, closing):
return _patched
def _get_async_patched(fn, injections, closing):
def _get_async_patched(fn):
@functools.wraps(fn)
async def _patched(*args, **kwargs):
to_inject = kwargs.copy()
for injection, provider in injections.items():
for injection, provider in _patched.__injections__.items():
if injection not in kwargs \
or _is_fastapi_default_arg_injection(injection, kwargs):
to_inject[injection] = provider()
result = await fn(*args, **to_inject)
for injection, provider in closing.items():
for injection, provider in _patched.__closing__.items():
if injection in kwargs \
and not _is_fastapi_default_arg_injection(injection, kwargs):
continue
@ -366,10 +439,6 @@ def _is_patched(fn):
return getattr(fn, '__wired__', False) is True
def _get_original_from_patched(fn):
return getattr(fn, '__original__')
def _is_declarative_container_instance(instance: Any) -> bool:
return (not isinstance(instance, type)
and getattr(instance, '__IS_CONTAINER__', False) is True

View File

@ -3,7 +3,7 @@
from decimal import Decimal
from typing import Callable
from dependency_injector.wiring import Provide, Provider
from dependency_injector.wiring import inject, Provide, Provider
from .container import Container, SubContainer
from .service import Service
@ -11,30 +11,37 @@ from .service import Service
class TestClass:
@inject
def __init__(self, service: Service = Provide[Container.service]):
self.service = service
@inject
def method(self, service: Service = Provide[Container.service]):
return service
@classmethod
@inject
def class_method(cls, service: Service = Provide[Container.service]):
return service
@staticmethod
@inject
def static_method(service: Service = Provide[Container.service]):
return service
@inject
def test_function(service: Service = Provide[Container.service]):
return service
@inject
def test_function_provider(service_provider: Callable[..., Service] = Provider[Container.service]):
service = service_provider()
return service
@inject
def test_config_value(
some_value_int: int = Provide[Container.config.a.b.c.as_int()],
some_value_str: str = Provide[Container.config.a.b.c.as_(str)],
@ -43,25 +50,44 @@ def test_config_value(
return some_value_int, some_value_str, some_value_decimal
@inject
def test_provide_provider(service_provider: Callable[..., Service] = Provider[Container.service.provider]):
service = service_provider()
return service
@inject
def test_provided_instance(some_value: int = Provide[Container.service.provided.foo['bar'].call()]):
return some_value
@inject
def test_subcontainer_provider(some_value: int = Provide[Container.sub.int_object]):
return some_value
@inject
def test_config_invariant(some_value: int = Provide[Container.config.option[Container.config.switch]]):
return some_value
@inject
def test_provide_from_different_containers(
service: Service = Provide[Container.service],
some_value: int = Provide[SubContainer.int_object],
):
return service, some_value
class ClassDecorator:
def __init__(self, fn):
self._fn = fn
def __call__(self, *args, **kwargs):
return self._fn(*args, **kwargs)
@ClassDecorator
@inject
def test_class_decorator(service: Service = Provide[Container.service]):
return service

View File

@ -1,8 +1,9 @@
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from ...container import Container
from ...service import Service
@inject
def test_function(service: Service = Provide[Container.service]):
return service

View File

@ -1,5 +1,5 @@
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, Closing
from dependency_injector.wiring import inject, Provide, Closing
class Service:
@ -32,5 +32,6 @@ class Container(containers.DeclarativeContainer):
service = providers.Resource(init_service)
@inject
def test_function(service: Service = Closing[Provide[Container.service]]):
return service

View File

@ -226,6 +226,10 @@ class WiringTest(unittest.TestCase):
self.assertEqual(result_2.init_counter, 0)
self.assertEqual(result_2.shutdown_counter, 0)
def test_class_decorator(self):
service = module.test_class_decorator()
self.assertIsInstance(service, Service)
class WiringAndFastAPITest(unittest.TestCase):