From ae3024588c50c702c4b52fb05d13110fdb551da3 Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Sun, 15 Nov 2020 16:06:42 -0500 Subject: [PATCH] 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 --- .gitignore | 3 + README.rst | 5 +- docs/examples/fastapi.rst | 79 +++++++++ docs/examples/index.rst | 1 + docs/index.rst | 5 +- docs/introduction/di_in_python.rst | 4 +- docs/introduction/key_features.rst | 2 +- docs/main/changelog.rst | 4 + docs/providers/resource.rst | 2 +- docs/tutorials/aiohttp.rst | 14 +- docs/tutorials/asyncio-daemon.rst | 9 +- docs/tutorials/cli.rst | 19 ++- docs/tutorials/flask.rst | 15 +- docs/wiring.rst | 15 +- examples/demo/with_di.py | 3 +- examples/miniapps/aiohttp/README.rst | 4 +- .../aiohttp/giphynavigator/handlers.py | 3 +- .../example/__main__.py | 3 +- .../example/__main__.py | 3 +- examples/miniapps/asyncio-daemon/README.rst | 4 +- .../monitoringdaemon/__main__.py | 3 +- .../decoupled-packages/example/__main__.py | 3 +- examples/miniapps/django/README.rst | 4 +- examples/miniapps/django/web/views.py | 3 +- examples/miniapps/fastapi/README.rst | 4 +- .../fastapi/giphynavigator/endpoints.py | 3 +- examples/miniapps/flask/README.rst | 4 +- .../miniapps/flask/githubnavigator/views.py | 3 +- examples/miniapps/movie-lister/README.rst | 4 +- .../miniapps/movie-lister/movies/__main__.py | 3 +- examples/miniapps/sanic/README.rst | 4 +- .../miniapps/sanic/giphynavigator/handlers.py | 3 +- examples/wiring/example.py | 3 +- examples/wiring/flask_example.py | 3 +- examples/wiring/flask_resource_closing.py | 3 +- src/dependency_injector/__init__.py | 2 +- src/dependency_injector/wiring.py | 161 +++++++++++++----- tests/unit/samples/wiringsamples/module.py | 28 ++- .../package/subpackage/submodule.py | 3 +- .../samples/wiringsamples/resourceclosing.py | 3 +- tests/unit/wiring/test_wiring_py36.py | 4 + 41 files changed, 336 insertions(+), 112 deletions(-) create mode 100644 docs/examples/fastapi.rst diff --git a/.gitignore b/.gitignore index 8337b46c..e2195edf 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/README.rst b/README.rst index b1722e63..4af89beb 100644 --- a/README.rst +++ b/README.rst @@ -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 `_. - **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 `_. - **Typing**. Provides typing stubs, ``mypy``-friendly. See `Typing and mypy `_. @@ -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]): ... diff --git a/docs/examples/fastapi.rst b/docs/examples/fastapi.rst new file mode 100644 index 00000000..eaa51b78 --- /dev/null +++ b/docs/examples/fastapi.rst @@ -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 `_. + +The example application is a REST API that searches for funny GIFs on the `Giphy `_. + +The source code is available on the `Github `_. + +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 `_. + +.. disqus:: diff --git a/docs/examples/index.rst b/docs/examples/index.rst index 16b7fc0a..9b7a1134 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -17,5 +17,6 @@ Explore the examples to see the ``Dependency Injector`` in action. flask aiohttp sanic + fastapi .. disqus:: diff --git a/docs/index.rst b/docs/index.rst index 4527a6af..f45e2472 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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]): ... diff --git a/docs/introduction/di_in_python.rst b/docs/introduction/di_in_python.rst index d3e58a10..e7a5f81e 100644 --- a/docs/introduction/di_in_python.rst +++ b/docs/introduction/di_in_python.rst @@ -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` diff --git a/docs/introduction/key_features.rst b/docs/introduction/key_features.rst index e586514a..23aa6822 100644 --- a/docs/introduction/key_features.rst +++ b/docs/introduction/key_features.rst @@ -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. diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 87259385..2ab56e1d 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -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. diff --git a/docs/providers/resource.rst b/docs/providers/resource.rst index 95ec164e..ebc98336 100644 --- a/docs/providers/resource.rst +++ b/docs/providers/resource.rst @@ -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. diff --git a/docs/tutorials/aiohttp.rst b/docs/tutorials/aiohttp.rst index 9b505c18..105daf24 100644 --- a/docs/tutorials/aiohttp.rst +++ b/docs/tutorials/aiohttp.rst @@ -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:: diff --git a/docs/tutorials/asyncio-daemon.rst b/docs/tutorials/asyncio-daemon.rst index 71aeecf7..f8004239 100644 --- a/docs/tutorials/asyncio-daemon.rst +++ b/docs/tutorials/asyncio-daemon.rst @@ -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:: diff --git a/docs/tutorials/cli.rst b/docs/tutorials/cli.rst index d488cd9c..3264caf9 100644 --- a/docs/tutorials/cli.rst +++ b/docs/tutorials/cli.rst @@ -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:: diff --git a/docs/tutorials/flask.rst b/docs/tutorials/flask.rst index 54ceb73c..5034d787 100644 --- a/docs/tutorials/flask.rst +++ b/docs/tutorials/flask.rst @@ -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:: diff --git a/docs/wiring.rst b/docs/wiring.rst index 412ba770..0db27a5d 100644 --- a/docs/wiring.rst +++ b/docs/wiring.rst @@ -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:: diff --git a/examples/demo/with_di.py b/examples/demo/with_di.py index 9fb5218f..094fdf52 100644 --- a/examples/demo/with_di.py +++ b/examples/demo/with_di.py @@ -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]): ... diff --git a/examples/miniapps/aiohttp/README.rst b/examples/miniapps/aiohttp/README.rst index dd8c00d8..9341e5c3 100644 --- a/examples/miniapps/aiohttp/README.rst +++ b/examples/miniapps/aiohttp/README.rst @@ -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% diff --git a/examples/miniapps/aiohttp/giphynavigator/handlers.py b/examples/miniapps/aiohttp/giphynavigator/handlers.py index ff720d17..5a829e82 100644 --- a/examples/miniapps/aiohttp/giphynavigator/handlers.py +++ b/examples/miniapps/aiohttp/giphynavigator/handlers.py @@ -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], diff --git a/examples/miniapps/application-multiple-containers/example/__main__.py b/examples/miniapps/application-multiple-containers/example/__main__.py index 91ea6267..86396b6f 100644 --- a/examples/miniapps/application-multiple-containers/example/__main__.py +++ b/examples/miniapps/application-multiple-containers/example/__main__.py @@ -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, diff --git a/examples/miniapps/application-single-container/example/__main__.py b/examples/miniapps/application-single-container/example/__main__.py index 011cb039..87ccf715 100644 --- a/examples/miniapps/application-single-container/example/__main__.py +++ b/examples/miniapps/application-single-container/example/__main__.py @@ -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, diff --git a/examples/miniapps/asyncio-daemon/README.rst b/examples/miniapps/asyncio-daemon/README.rst index 5bb73022..b6a45c79 100644 --- a/examples/miniapps/asyncio-daemon/README.rst +++ b/examples/miniapps/asyncio-daemon/README.rst @@ -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% diff --git a/examples/miniapps/asyncio-daemon/monitoringdaemon/__main__.py b/examples/miniapps/asyncio-daemon/monitoringdaemon/__main__.py index fe43d1ac..c15757ad 100644 --- a/examples/miniapps/asyncio-daemon/monitoringdaemon/__main__.py +++ b/examples/miniapps/asyncio-daemon/monitoringdaemon/__main__.py @@ -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() diff --git a/examples/miniapps/decoupled-packages/example/__main__.py b/examples/miniapps/decoupled-packages/example/__main__.py index 3ee153e3..61804d02 100644 --- a/examples/miniapps/decoupled-packages/example/__main__.py +++ b/examples/miniapps/decoupled-packages/example/__main__.py @@ -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 diff --git a/examples/miniapps/django/README.rst b/examples/miniapps/django/README.rst index 114f77cb..2ff42581 100644 --- a/examples/miniapps/django/README.rst +++ b/examples/miniapps/django/README.rst @@ -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% diff --git a/examples/miniapps/django/web/views.py b/examples/miniapps/django/web/views.py index 6a48217d..6d8f11f5 100644 --- a/examples/miniapps/django/web/views.py +++ b/examples/miniapps/django/web/views.py @@ -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], diff --git a/examples/miniapps/fastapi/README.rst b/examples/miniapps/fastapi/README.rst index 7e99ab8b..20dbf7e5 100644 --- a/examples/miniapps/fastapi/README.rst +++ b/examples/miniapps/fastapi/README.rst @@ -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% diff --git a/examples/miniapps/fastapi/giphynavigator/endpoints.py b/examples/miniapps/fastapi/giphynavigator/endpoints.py index 4cbb9f10..1d52b2f8 100644 --- a/examples/miniapps/fastapi/giphynavigator/endpoints.py +++ b/examples/miniapps/fastapi/giphynavigator/endpoints.py @@ -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()], diff --git a/examples/miniapps/flask/README.rst b/examples/miniapps/flask/README.rst index 118c4e52..d825a81e 100644 --- a/examples/miniapps/flask/README.rst +++ b/examples/miniapps/flask/README.rst @@ -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% diff --git a/examples/miniapps/flask/githubnavigator/views.py b/examples/miniapps/flask/githubnavigator/views.py index 22a3d35d..5cccdad2 100644 --- a/examples/miniapps/flask/githubnavigator/views.py +++ b/examples/miniapps/flask/githubnavigator/views.py @@ -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], diff --git a/examples/miniapps/movie-lister/README.rst b/examples/miniapps/movie-lister/README.rst index dcc85066..cdcbc20c 100644 --- a/examples/miniapps/movie-lister/README.rst +++ b/examples/miniapps/movie-lister/README.rst @@ -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% diff --git a/examples/miniapps/movie-lister/movies/__main__.py b/examples/miniapps/movie-lister/movies/__main__.py index df39fdc5..975618f3 100644 --- a/examples/miniapps/movie-lister/movies/__main__.py +++ b/examples/miniapps/movie-lister/movies/__main__.py @@ -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'): diff --git a/examples/miniapps/sanic/README.rst b/examples/miniapps/sanic/README.rst index 9550fccc..adce2810 100644 --- a/examples/miniapps/sanic/README.rst +++ b/examples/miniapps/sanic/README.rst @@ -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% diff --git a/examples/miniapps/sanic/giphynavigator/handlers.py b/examples/miniapps/sanic/giphynavigator/handlers.py index 6b319a25..3537827d 100644 --- a/examples/miniapps/sanic/giphynavigator/handlers.py +++ b/examples/miniapps/sanic/giphynavigator/handlers.py @@ -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], diff --git a/examples/wiring/example.py b/examples/wiring/example.py index fe8f27ee..7a62e830 100644 --- a/examples/wiring/example.py +++ b/examples/wiring/example.py @@ -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: ... diff --git a/examples/wiring/flask_example.py b/examples/wiring/flask_example.py index 3ec0963f..033d0ee9 100644 --- a/examples/wiring/flask_example.py +++ b/examples/wiring/flask_example.py @@ -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)}) diff --git a/examples/wiring/flask_resource_closing.py b/examples/wiring/flask_resource_closing.py index dec40140..05b62c37 100644 --- a/examples/wiring/flask_resource_closing.py +++ b/examples/wiring/flask_resource_closing.py @@ -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!' diff --git a/src/dependency_injector/__init__.py b/src/dependency_injector/__init__.py index 754868e9..06ffecd0 100644 --- a/src/dependency_injector/__init__.py +++ b/src/dependency_injector/__init__.py @@ -1,6 +1,6 @@ """Top-level package.""" -__version__ = '4.3.8' +__version__ = '4.3.9' """Version number. :type: str diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index 1c671bb1..1ba73639 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -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: - return - patched = _patch_with_injections(fn, injections, closing) - setattr(module, name, _wrap_patched(patched, fn, 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) + + 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 diff --git a/tests/unit/samples/wiringsamples/module.py b/tests/unit/samples/wiringsamples/module.py index c39d1414..7a3ae557 100644 --- a/tests/unit/samples/wiringsamples/module.py +++ b/tests/unit/samples/wiringsamples/module.py @@ -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 diff --git a/tests/unit/samples/wiringsamples/package/subpackage/submodule.py b/tests/unit/samples/wiringsamples/package/subpackage/submodule.py index 8f8e0d7f..e99c131e 100644 --- a/tests/unit/samples/wiringsamples/package/subpackage/submodule.py +++ b/tests/unit/samples/wiringsamples/package/subpackage/submodule.py @@ -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 diff --git a/tests/unit/samples/wiringsamples/resourceclosing.py b/tests/unit/samples/wiringsamples/resourceclosing.py index f7f35bd1..8dfee241 100644 --- a/tests/unit/samples/wiringsamples/resourceclosing.py +++ b/tests/unit/samples/wiringsamples/resourceclosing.py @@ -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 diff --git a/tests/unit/wiring/test_wiring_py36.py b/tests/unit/wiring/test_wiring_py36.py index b1886ea7..7c061ad9 100644 --- a/tests/unit/wiring/test_wiring_py36.py +++ b/tests/unit/wiring/test_wiring_py36.py @@ -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):