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/providers/*.h
src/dependency_injector/providers/*.so
# Workspace for samples
.workspace/

View File

@ -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]):
...

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
django
flask
flask-blueprints
aiohttp
sanic
fastapi

View File

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

View File

@ -162,7 +162,7 @@ the dependency.
.. code-block:: python
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
class Container(containers.DeclarativeContainer):
@ -181,6 +181,7 @@ the dependency.
)
@inject
def main(service: Service = Provide[Container.service]):
...
@ -282,6 +283,7 @@ Choose one of the following as a next step:
- :ref:`decoupled-packages`
- :ref:`django-example`
- :ref:`flask-example`
- :ref:`flask-blueprints-example`
- :ref:`aiohttp-example`
- :ref:`sanic-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
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
-----
- 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
:language: python
:lines: 3-
:emphasize-lines: 23
:emphasize-lines: 24
Framework initializes and injects the resource into the function. With the ``Closing`` marker
framework calls resource ``shutdown()`` method when function execution is over.

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,8 @@ Wiring feature provides a way to inject container providers into the functions a
To use wiring you need:
- **Place markers in the code**. Wiring marker specifies what provider to inject,
- **Place @inject decorator**. Decorator ``@inject`` injects the dependencies.
- **Place markers**. Wiring marker specifies what dependency to inject,
e.g. ``Provide[Container.bar]``. This helps container to find the injections.
- **Wire the container with the markers in the code**. Call ``container.wire()``
specifying modules and packages you would like to wire it with.
@ -25,9 +26,10 @@ a function or method argument:
.. code-block:: python
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
@inject
def foo(bar: Bar = Provide[Container.bar]):
...
@ -40,9 +42,10 @@ There are two types of markers:
.. code-block:: python
from dependency_injector.wiring import Provider
from dependency_injector.wiring import inject, Provider
@inject
def foo(bar_provider: Callable[..., Bar] = Provider[Container.bar]):
bar = bar_provider()
...
@ -51,18 +54,22 @@ You can use configuration, provided instance and sub-container providers as you
.. code-block:: python
@inject
def foo(token: str = Provide[Container.config.api_token]):
...
@inject
def foo(timeout: int = Provide[Container.config.timeout.as_(int)]):
...
@inject
def foo(baz: Baz = Provide[Container.bar.provided.baz]):
...
@inject
def foo(bar: Bar = Provide[Container.subcontainer.bar]):
...
@ -100,6 +107,7 @@ When wiring is done functions and methods with the markers are patched to provid
.. code-block:: python
@inject
def foo(bar: Bar = Provide[Container.bar]):
...
@ -199,6 +207,7 @@ Take a look at other application examples:
- :ref:`decoupled-packages`
- :ref:`django-example`
- :ref:`flask-example`
- :ref:`flask-blueprints-example`
- :ref:`aiohttp-example`
- :ref:`sanic-example`
- :ref:`fastapi-example`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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/services.py 14 0 100%
githubnavigator/tests.py 34 0 100%
githubnavigator/views.py 9 0 100%
githubnavigator/views.py 10 0 100%
----------------------------------------------------
TOTAL 79 0 100%
TOTAL 80 0 100%

View File

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

View File

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

View File

@ -2,12 +2,13 @@
import sys
from dependency_injector.wiring import Provide
from dependency_injector.wiring import inject, Provide
from .listers import MovieLister
from .containers import Container
@inject
def main(lister: MovieLister = Provide[Container.lister]) -> None:
print('Francis Lawrence movies:')
for movie in lister.movies_directed_by('Francis Lawrence'):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,19 @@ import importlib
import pkgutil
import sys
from types import ModuleType
from typing import Optional, Iterable, Callable, Any, Tuple, Dict, Generic, TypeVar, Type, cast
from typing import (
Optional,
Iterable,
Iterator,
Callable,
Any,
Tuple,
Dict,
Generic,
TypeVar,
Type,
cast,
)
if sys.version_info < (3, 7):
from typing import GenericMeta
@ -21,15 +33,35 @@ from . import providers
__all__ = (
'wire',
'unwire',
'inject',
'Provide',
'Provider',
'Closing',
)
T = TypeVar('T')
F = TypeVar('F', bound=Callable[..., Any])
Container = Any
class Registry:
def __init__(self):
self._storage = set()
def add(self, patched: Callable[..., Any]) -> None:
self._storage.add(patched)
def get_from_module(self, module: ModuleType) -> Iterator[Callable[..., Any]]:
for patched in self._storage:
if patched.__module__ != module.__name__:
continue
yield patched
_patched_registry = Registry()
class ProvidersMap:
def __init__(self, container):
@ -152,7 +184,7 @@ class ProvidersMap:
return providers_map
def wire(
def wire( # noqa: C901
container: Container,
*,
modules: Optional[Iterable[ModuleType]] = None,
@ -179,6 +211,9 @@ def wire(
for method_name, method in inspect.getmembers(member, _is_method):
_patch_method(member, method_name, method, providers_map)
for patched in _patched_registry.get_from_module(module):
_bind_injections(patched, providers_map)
def unwire(
*,
@ -201,6 +236,17 @@ def unwire(
for method_name, method in inspect.getmembers(member, inspect.isfunction):
_unpatch(member, method_name, method)
for patched in _patched_registry.get_from_module(module):
_unbind_injections(patched)
def inject(fn: F) -> F:
"""Decorate callable with injecting decorator."""
reference_injections, reference_closing = _fetch_reference_injections(fn)
patched = _get_patched(fn, reference_injections, reference_closing)
_patched_registry.add(patched)
return cast(F, patched)
def _patch_fn(
module: ModuleType,
@ -208,11 +254,16 @@ def _patch_fn(
fn: Callable[..., Any],
providers_map: ProvidersMap,
) -> None:
injections, closing = _resolve_injections(fn, providers_map)
if not injections:
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

View File

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

View File

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

View File

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

View File

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