Merge branch 'release/4.4.0' into master

This commit is contained in:
Roman Mogylatov 2020-11-15 18:20:48 -05:00
commit 262c035bc1
53 changed files with 750 additions and 109 deletions

3
.gitignore vendored
View File

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

View File

@ -78,7 +78,7 @@ Key features of the ``Dependency Injector``:
.. code-block:: python .. code-block:: python
from dependency_injector import containers, providers from dependency_injector import containers, providers
from dependency_injector.wiring import Provide from dependency_injector.wiring import inject, Provide
class Container(containers.DeclarativeContainer): class Container(containers.DeclarativeContainer):
@ -97,6 +97,7 @@ Key features of the ``Dependency Injector``:
) )
@inject
def main(service: Service = Provide[Container.service]): def main(service: Service = Provide[Container.service]):
... ...

View File

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

View File

@ -15,6 +15,7 @@ Explore the examples to see the ``Dependency Injector`` in action.
decoupled-packages decoupled-packages
django django
flask flask
flask-blueprints
aiohttp aiohttp
sanic sanic
fastapi fastapi

View File

@ -85,7 +85,7 @@ Key features of the ``Dependency Injector``:
.. code-block:: python .. code-block:: python
from dependency_injector import containers, providers from dependency_injector import containers, providers
from dependency_injector.wiring import Provide from dependency_injector.wiring import inject, Provide
class Container(containers.DeclarativeContainer): class Container(containers.DeclarativeContainer):
@ -104,6 +104,7 @@ Key features of the ``Dependency Injector``:
) )
@inject
def main(service: Service = Provide[Container.service]): def main(service: Service = Provide[Container.service]):
... ...

View File

@ -162,7 +162,7 @@ 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 from dependency_injector.wiring import inject, Provide
class Container(containers.DeclarativeContainer): class Container(containers.DeclarativeContainer):
@ -181,6 +181,7 @@ the dependency.
) )
@inject
def main(service: Service = Provide[Container.service]): def main(service: Service = Provide[Container.service]):
... ...
@ -282,6 +283,7 @@ Choose one of the following as a next step:
- :ref:`decoupled-packages` - :ref:`decoupled-packages`
- :ref:`django-example` - :ref:`django-example`
- :ref:`flask-example` - :ref:`flask-example`
- :ref:`flask-blueprints-example`
- :ref:`aiohttp-example` - :ref:`aiohttp-example`
- :ref:`sanic-example` - :ref:`sanic-example`
- :ref:`fastapi-example` - :ref:`fastapi-example`

View File

@ -7,6 +7,17 @@ that were made in every particular version.
From version 0.7.6 *Dependency Injector* framework strictly From version 0.7.6 *Dependency Injector* framework strictly
follows `Semantic versioning`_ follows `Semantic versioning`_
4.4.0
-----
- Add ``@inject`` decorator. It helps to fix a number of wiring bugs and make wiring be more resilient.
- Refactor ``wiring`` module.
- Update documentation and examples to use ``@inject`` decorator.
- Add ``Flask`` blueprints example.
- Fix wiring bug when wiring doesn't work with the class-based decorators.
- Fix wiring bug when wiring doesn't work with the decorators that doesn't use ``functools.wraps(...)``.
- Fix wiring bug with ``@app.route(...)`` -style decorators (Flask, Sanic, FastAPI, etc.).
- Fix wiring bug when wiring doesn't work with Flask blueprints.
4.3.9 4.3.9
----- -----
- Add ``FastAPI`` example. - Add ``FastAPI`` example.

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 .. literalinclude:: ../../examples/wiring/flask_resource_closing.py
:language: python :language: python
:lines: 3- :lines: 3-
:emphasize-lines: 23 :emphasize-lines: 24
Framework initializes and injects the resource into the function. With the ``Closing`` marker Framework initializes and injects the resource into the function. With the ``Closing`` marker
framework calls resource ``shutdown()`` method when function execution is over. 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``: Edit ``handlers.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 4-7,10-13,17 :emphasize-lines: 4-7,10-14,18
"""Handlers module.""" """Handlers module."""
from aiohttp import web from aiohttp import web
from dependency_injector.wiring import Provide from dependency_injector.wiring import inject, Provide
from .services import SearchService from .services import SearchService
from .containers import Container 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 = Provide[Container.search_service],
@ -645,17 +646,18 @@ Let's make some refactoring. We will move these values to the config.
Edit ``handlers.py``: Edit ``handlers.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 13-14,16-17 :emphasize-lines: 14-15,17-18
"""Handlers module.""" """Handlers module."""
from aiohttp import web from aiohttp import web
from dependency_injector.wiring import Provide from dependency_injector.wiring import inject, Provide
from .services import SearchService from .services import SearchService
from .containers import Container 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 = Provide[Container.search_service],
@ -821,11 +823,11 @@ You should see:
giphynavigator/application.py 12 0 100% giphynavigator/application.py 12 0 100%
giphynavigator/containers.py 6 0 100% giphynavigator/containers.py 6 0 100%
giphynavigator/giphy.py 14 9 36% 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/services.py 9 1 89%
giphynavigator/tests.py 37 0 100% giphynavigator/tests.py 37 0 100%
--------------------------------------------------- ---------------------------------------------------
TOTAL 87 10 89% TOTAL 88 10 89%
.. note:: .. note::

View File

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

View File

@ -575,18 +575,19 @@ Let's inject the ``lister`` into the ``main()`` function.
Edit ``__main__.py``: Edit ``__main__.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 3-7,11,18 :emphasize-lines: 3-7,11-12,19
"""Main module.""" """Main module."""
import sys import sys
from dependency_injector.wiring import Provide from dependency_injector.wiring import inject, Provide
from .listers import MovieLister from .listers import MovieLister
from .containers import Container from .containers import Container
@inject
def main(lister: MovieLister = Provide[Container.lister]) -> None: def main(lister: MovieLister = Provide[Container.lister]) -> None:
... ...
@ -606,18 +607,19 @@ Francis Lawrence and movies released in 2016.
Edit ``__main__.py``: Edit ``__main__.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 12-18 :emphasize-lines: 13-19
"""Main module.""" """Main module."""
import sys import sys
from dependency_injector.wiring import Provide from dependency_injector.wiring import inject, Provide
from .listers import MovieLister from .listers import MovieLister
from .containers import Container from .containers import Container
@inject
def main(lister: MovieLister = Provide[Container.lister]) -> None: def main(lister: MovieLister = Provide[Container.lister]) -> None:
print('Francis Lawrence movies:') print('Francis Lawrence movies:')
for movie in lister.movies_directed_by('Francis Lawrence'): 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``: Edit ``__main__.py``:
.. code-block:: python .. code-block:: python
:emphasize-lines: 24 :emphasize-lines: 25
"""Main module.""" """Main module."""
import sys import sys
from dependency_injector.wiring import Provide from dependency_injector.wiring import inject, Provide
from .listers import MovieLister from .listers import MovieLister
from .containers import Container from .containers import Container
@inject
def main(lister: MovieLister = Provide[Container.lister]) -> None: def main(lister: MovieLister = Provide[Container.lister]) -> None:
print('Francis Lawrence movies:') print('Francis Lawrence movies:')
for movie in lister.movies_directed_by('Francis Lawrence'): for movie in lister.movies_directed_by('Francis Lawrence'):
@ -1023,14 +1026,14 @@ You should see:
Name Stmts Miss Cover Name Stmts Miss Cover
------------------------------------------ ------------------------------------------
movies/__init__.py 0 0 100% 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/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 91 31 66% TOTAL 92 32 65%
.. note:: .. note::

View File

@ -707,17 +707,19 @@ Let's inject ``SearchService`` into the ``index`` view. We will use :ref:`Wiring
Edit ``views.py``: Edit ``views.py``:
.. code-block:: python .. 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.""" """Views module."""
from flask import request, render_template from flask import request, render_template
from dependency_injector.wiring import Provide from dependency_injector.wiring import inject, Provide
from .services import SearchService from .services import SearchService
from .containers import Container from .containers import Container
@inject
def index(search_service: SearchService = Provide[Container.search_service]): 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)
@ -783,17 +785,18 @@ 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: 10-16 :emphasize-lines: 11-17
"""Views module.""" """Views module."""
from flask import request, render_template from flask import request, render_template
from dependency_injector.wiring import Provide from dependency_injector.wiring import inject, Provide
from .services import SearchService from .services import SearchService
from .containers import Container from .containers import Container
@inject
def index( def index(
search_service: SearchService = Provide[Container.search_service], search_service: SearchService = Provide[Container.search_service],
default_query: str = Provide[Container.config.default.query], default_query: str = Provide[Container.config.default.query],
@ -972,9 +975,9 @@ You should see:
githubnavigator/containers.py 7 0 100% githubnavigator/containers.py 7 0 100%
githubnavigator/services.py 14 0 100% githubnavigator/services.py 14 0 100%
githubnavigator/tests.py 34 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:: .. 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: 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. e.g. ``Provide[Container.bar]``. This helps container to find the injections.
- **Wire the container with the markers in the code**. Call ``container.wire()`` - **Wire the container with the markers in the code**. Call ``container.wire()``
specifying modules and packages you would like to wire it with. specifying modules and packages you would like to wire it with.
@ -25,9 +26,10 @@ a function or method argument:
.. code-block:: python .. code-block:: python
from dependency_injector.wiring import Provide from dependency_injector.wiring import inject, Provide
@inject
def foo(bar: Bar = Provide[Container.bar]): def foo(bar: Bar = Provide[Container.bar]):
... ...
@ -40,9 +42,10 @@ There are two types of markers:
.. code-block:: python .. 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]): def foo(bar_provider: Callable[..., Bar] = Provider[Container.bar]):
bar = bar_provider() bar = bar_provider()
... ...
@ -51,18 +54,22 @@ You can use configuration, provided instance and sub-container providers as you
.. code-block:: python .. code-block:: python
@inject
def foo(token: str = Provide[Container.config.api_token]): def foo(token: str = Provide[Container.config.api_token]):
... ...
@inject
def foo(timeout: int = Provide[Container.config.timeout.as_(int)]): def foo(timeout: int = Provide[Container.config.timeout.as_(int)]):
... ...
@inject
def foo(baz: Baz = Provide[Container.bar.provided.baz]): def foo(baz: Baz = Provide[Container.bar.provided.baz]):
... ...
@inject
def foo(bar: Bar = Provide[Container.subcontainer.bar]): 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 .. code-block:: python
@inject
def foo(bar: Bar = Provide[Container.bar]): def foo(bar: Bar = Provide[Container.bar]):
... ...
@ -199,6 +207,7 @@ Take a look at other application examples:
- :ref:`decoupled-packages` - :ref:`decoupled-packages`
- :ref:`django-example` - :ref:`django-example`
- :ref:`flask-example` - :ref:`flask-example`
- :ref:`flask-blueprints-example`
- :ref:`aiohttp-example` - :ref:`aiohttp-example`
- :ref:`sanic-example` - :ref:`sanic-example`
- :ref:`fastapi-example` - :ref:`fastapi-example`

View File

@ -2,7 +2,7 @@ import sys
from unittest import mock from unittest import mock
from dependency_injector import containers, providers from dependency_injector import containers, providers
from dependency_injector.wiring import Provide from dependency_injector.wiring import inject, Provide
from after import ApiClient, Service from after import ApiClient, Service
@ -23,6 +23,7 @@ class Container(containers.DeclarativeContainer):
) )
@inject
def main(service: Service = Provide[Container.service]): 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/application.py 12 0 100%
giphynavigator/containers.py 6 0 100% giphynavigator/containers.py 6 0 100%
giphynavigator/giphy.py 14 9 36% 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/services.py 9 1 89%
giphynavigator/tests.py 37 0 100% giphynavigator/tests.py 37 0 100%
--------------------------------------------------- ---------------------------------------------------
TOTAL 87 10 89% TOTAL 88 10 89%

View File

@ -1,12 +1,13 @@
"""Handlers module.""" """Handlers module."""
from aiohttp import web from aiohttp import web
from dependency_injector.wiring import Provide from dependency_injector.wiring import inject, Provide
from .services import SearchService from .services import SearchService
from .containers import Container 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 = Provide[Container.search_service],

View File

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

View File

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

View File

@ -76,11 +76,11 @@ The output should be something like:
Name Stmts Miss Cover Name Stmts Miss Cover
---------------------------------------------------- ----------------------------------------------------
monitoringdaemon/__init__.py 0 0 100% 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/containers.py 11 0 100%
monitoringdaemon/dispatcher.py 44 5 89% monitoringdaemon/dispatcher.py 44 5 89%
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 37 0 100% monitoringdaemon/tests.py 37 0 100%
---------------------------------------------------- ----------------------------------------------------
TOTAL 133 21 84% TOTAL 134 22 84%

View File

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

View File

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

View File

@ -108,6 +108,6 @@ The output should be something like:
web/apps.py 7 0 100% web/apps.py 7 0 100%
web/tests.py 28 0 100% web/tests.py 28 0 100%
web/urls.py 3 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.http import HttpRequest, HttpResponse
from django.shortcuts import render 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.containers import Container
from githubnavigator.services import SearchService from githubnavigator.services import SearchService
@inject
def index( def index(
request: HttpRequest, request: HttpRequest,
search_service: SearchService = Provide[Container.search_service], 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/__init__.py 0 0 100%
giphynavigator/application.py 13 0 100% giphynavigator/application.py 13 0 100%
giphynavigator/containers.py 6 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/giphy.py 14 9 36%
giphynavigator/services.py 9 1 89% giphynavigator/services.py 9 1 89%
giphynavigator/tests.py 38 0 100% giphynavigator/tests.py 38 0 100%
--------------------------------------------------- ---------------------------------------------------
TOTAL 85 10 88% TOTAL 86 10 88%

View File

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

View File

@ -0,0 +1,100 @@
Flask Blueprints + Dependency Injector Example
==============================================
This is a `Flask <https://flask.palletsprojects.com/>`_ Blueprints +
`Dependency Injector <https://python-dependency-injector.ets-labs.org/>`_ example application.
The example application helps to search for repositories on the Github.
.. image:: screenshot.png
Run
---
Create virtual environment:
.. code-block:: bash
virtualenv venv
. venv/bin/activate
Install requirements:
.. code-block:: bash
pip install -r requirements.txt
To run the application do:
.. code-block:: bash
export FLASK_APP=githubnavigator.application
export FLASK_ENV=development
flask run
The output should be something like:
.. code-block::
* Serving Flask app "githubnavigator.application" (lazy loading)
* Environment: development
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with fsevents reloader
* Debugger is active!
* Debugger PIN: 473-587-859
After that visit http://127.0.0.1:5000/ in your browser.
.. note::
Github has a rate limit. When the rate limit is exceed you will see an exception
``github.GithubException.RateLimitExceededException``. For unauthenticated requests, the rate
limit allows for up to 60 requests per hour. To extend the limit to 5000 requests per hour you
need to set personal access token.
It's easy:
- Follow this `guide <https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token>`_ to create a token.
- Set a token to the environment variable:
.. code-block:: bash
export GITHUB_TOKEN=<your token>
- Restart the app with ``flask run``
`Read more on Github rate limit <https://developer.github.com/v3/#rate-limiting>`_
Test
----
This application comes with the unit tests.
To run the tests do:
.. code-block:: bash
py.test githubnavigator/tests.py --cov=githubnavigator
The output should be something like:
.. code-block::
platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
plugins: flask-1.0.0, cov-2.10.0
collected 2 items
githubnavigator/tests.py .. [100%]
---------- coverage: platform darwin, python 3.8.3-final-0 -----------
Name Stmts Miss Cover
----------------------------------------------------
githubnavigator/__init__.py 0 0 100%
githubnavigator/application.py 15 0 100%
githubnavigator/blueprints/example.py 12 0 100%
githubnavigator/containers.py 7 0 100%
githubnavigator/services.py 14 0 100%
githubnavigator/tests.py 34 0 100%
-----------------------------------------------------------
TOTAL 82 0 100%

View File

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

View File

@ -0,0 +1 @@
"""Top-level package."""

View File

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

View File

@ -0,0 +1 @@
"""Blueprints package."""

View File

@ -0,0 +1,30 @@
"""Example blueprint."""
from flask import Blueprint, request, render_template
from dependency_injector.wiring import inject, Provide
from githubnavigator.services import SearchService
from githubnavigator.containers import Container
blueprint = Blueprint('example', __name__, template_folder='templates/')
@blueprint.route('/')
@inject
def index(
search_service: SearchService = Provide[Container.search_service],
default_query: str = Provide[Container.config.default.query],
default_limit: int = Provide[Container.config.default.limit.as_int()],
):
query = request.args.get('query', default_query)
limit = request.args.get('limit', default_limit, int)
repositories = search_service.search_repositories(query, limit)
return render_template(
'index.html',
query=query,
limit=limit,
repositories=repositories,
)

View File

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

View File

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

View File

@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
{% block head %}
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{% block styles %}
<!-- Bootstrap CSS -->
{{ bootstrap.load_css() }}
{% endblock %}
<title>{% block title %}{% endblock %}</title>
{% endblock %}
</head>
<body>
<!-- Your page content -->
{% block content %}{% endblock %}
{% block scripts %}
<!-- Optional JavaScript -->
{{ bootstrap.load_js() }}
{% endblock %}
</body>
</html>

View File

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

View File

@ -0,0 +1,71 @@
"""Tests module."""
from unittest import mock
import pytest
from github import Github
from flask import url_for
from .application import create_app
@pytest.fixture
def app():
app = create_app()
yield app
app.container.unwire()
def test_index(client, app):
github_client_mock = mock.Mock(spec=Github)
github_client_mock.search_repositories.return_value = [
mock.Mock(
html_url='repo1-url',
name='repo1-name',
owner=mock.Mock(
login='owner1-login',
html_url='owner1-url',
avatar_url='owner1-avatar-url',
),
get_commits=mock.Mock(return_value=[mock.Mock()]),
),
mock.Mock(
html_url='repo2-url',
name='repo2-name',
owner=mock.Mock(
login='owner2-login',
html_url='owner2-url',
avatar_url='owner2-avatar-url',
),
get_commits=mock.Mock(return_value=[mock.Mock()]),
),
]
with app.container.github_client.override(github_client_mock):
response = client.get(url_for('example.index'))
assert response.status_code == 200
assert b'Results found: 2' in response.data
assert b'repo1-url' in response.data
assert b'repo1-name' in response.data
assert b'owner1-login' in response.data
assert b'owner1-url' in response.data
assert b'owner1-avatar-url' in response.data
assert b'repo2-url' in response.data
assert b'repo2-name' in response.data
assert b'owner2-login' in response.data
assert b'owner2-url' in response.data
assert b'owner2-avatar-url' in response.data
def test_index_no_results(client, app):
github_client_mock = mock.Mock(spec=Github)
github_client_mock.search_repositories.return_value = []
with app.container.github_client.override(github_client_mock):
response = client.get(url_for('example.index'))
assert response.status_code == 200
assert b'Results found: 0' in response.data

View File

@ -0,0 +1,7 @@
dependency-injector
flask
bootstrap-flask
pygithub
pyyaml
pytest-flask
pytest-cov

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 KiB

View File

@ -95,6 +95,6 @@ The output should be something like:
githubnavigator/containers.py 7 0 100% githubnavigator/containers.py 7 0 100%
githubnavigator/services.py 14 0 100% githubnavigator/services.py 14 0 100%
githubnavigator/tests.py 34 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.""" """Views module."""
from flask import request, render_template from flask import request, render_template
from dependency_injector.wiring import Provide from dependency_injector.wiring import inject, Provide
from .services import SearchService from .services import SearchService
from .containers import Container from .containers import Container
@inject
def index( def index(
search_service: SearchService = Provide[Container.search_service], search_service: SearchService = Provide[Container.search_service],
default_query: str = Provide[Container.config.default.query], default_query: str = Provide[Container.config.default.query],

View File

@ -68,11 +68,11 @@ The output should be something like:
Name Stmts Miss Cover Name Stmts Miss Cover
------------------------------------------ ------------------------------------------
movies/__init__.py 0 0 100% 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/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 91 31 66% TOTAL 92 32 65%

View File

@ -2,12 +2,13 @@
import sys import sys
from dependency_injector.wiring import Provide from dependency_injector.wiring import inject, Provide
from .listers import MovieLister from .listers import MovieLister
from .containers import Container from .containers import Container
@inject
def main(lister: MovieLister = Provide[Container.lister]) -> None: def main(lister: MovieLister = Provide[Container.lister]) -> None:
print('Francis Lawrence movies:') print('Francis Lawrence movies:')
for movie in lister.movies_directed_by('Francis Lawrence'): 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/application.py 12 0 100%
giphynavigator/containers.py 6 0 100% giphynavigator/containers.py 6 0 100%
giphynavigator/giphy.py 14 9 36% 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/services.py 9 1 89%
giphynavigator/tests.py 34 0 100% 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.request import Request
from sanic.response import HTTPResponse, json from sanic.response import HTTPResponse, json
from dependency_injector.wiring import Provide from dependency_injector.wiring import inject, Provide
from .services import SearchService from .services import SearchService
from .containers import Container from .containers import Container
@inject
async def index( async def index(
request: Request, request: Request,
search_service: SearchService = Provide[Container.search_service], search_service: SearchService = Provide[Container.search_service],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
from decimal import Decimal from decimal import Decimal
from typing import Callable 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 .container import Container, SubContainer
from .service import Service from .service import Service
@ -11,30 +11,37 @@ from .service import Service
class TestClass: class TestClass:
@inject
def __init__(self, service: Service = Provide[Container.service]): def __init__(self, service: Service = Provide[Container.service]):
self.service = service self.service = service
@inject
def method(self, service: Service = Provide[Container.service]): def method(self, service: Service = Provide[Container.service]):
return service return service
@classmethod @classmethod
@inject
def class_method(cls, service: Service = Provide[Container.service]): def class_method(cls, service: Service = Provide[Container.service]):
return service return service
@staticmethod @staticmethod
@inject
def static_method(service: Service = Provide[Container.service]): def static_method(service: Service = Provide[Container.service]):
return service return service
@inject
def test_function(service: Service = Provide[Container.service]): def test_function(service: Service = Provide[Container.service]):
return service return service
@inject
def test_function_provider(service_provider: Callable[..., Service] = Provider[Container.service]): def test_function_provider(service_provider: Callable[..., Service] = Provider[Container.service]):
service = service_provider() service = service_provider()
return service return service
@inject
def test_config_value( def test_config_value(
some_value_int: int = Provide[Container.config.a.b.c.as_int()], some_value_int: int = Provide[Container.config.a.b.c.as_int()],
some_value_str: str = Provide[Container.config.a.b.c.as_(str)], 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 return some_value_int, some_value_str, some_value_decimal
@inject
def test_provide_provider(service_provider: Callable[..., Service] = Provider[Container.service.provider]): def test_provide_provider(service_provider: Callable[..., Service] = Provider[Container.service.provider]):
service = service_provider() service = service_provider()
return service return service
@inject
def test_provided_instance(some_value: int = Provide[Container.service.provided.foo['bar'].call()]): def test_provided_instance(some_value: int = Provide[Container.service.provided.foo['bar'].call()]):
return some_value return some_value
@inject
def test_subcontainer_provider(some_value: int = Provide[Container.sub.int_object]): def test_subcontainer_provider(some_value: int = Provide[Container.sub.int_object]):
return some_value return some_value
@inject
def test_config_invariant(some_value: int = Provide[Container.config.option[Container.config.switch]]): def test_config_invariant(some_value: int = Provide[Container.config.option[Container.config.switch]]):
return some_value return some_value
@inject
def test_provide_from_different_containers( def test_provide_from_different_containers(
service: Service = Provide[Container.service], service: Service = Provide[Container.service],
some_value: int = Provide[SubContainer.int_object], some_value: int = Provide[SubContainer.int_object],
): ):
return service, some_value 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 ...container import Container
from ...service import Service from ...service import Service
@inject
def test_function(service: Service = Provide[Container.service]): def test_function(service: Service = Provide[Container.service]):
return service return service

View File

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

View File

@ -226,6 +226,10 @@ class WiringTest(unittest.TestCase):
self.assertEqual(result_2.init_counter, 0) self.assertEqual(result_2.init_counter, 0)
self.assertEqual(result_2.shutdown_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): class WiringAndFastAPITest(unittest.TestCase):