Merge branch 'release/3.21.0' into master

This commit is contained in:
Roman Mogylatov 2020-07-13 22:50:22 -04:00
commit 0c77e73d51
12 changed files with 168 additions and 142 deletions

View File

@ -65,20 +65,23 @@ This place is called **the container**. You use the container to manage all the
*The container is like a map of your application. You always know what depends on what.* *The container is like a map of your application. You always know what depends on what.*
``Flask`` + ``Dependency Injector`` example: ``Flask`` + ``Dependency Injector`` example application container:
.. code-block:: python .. code-block:: python
from dependency_injector import containers, providers from dependency_injector import containers, providers
from dependency_injector.ext import flask from dependency_injector.ext import flask
from flask import Flask
from github import Github from github import Github
from . import services, views from . import views, services
class Application(containers.DeclarativeContainer): class ApplicationContainer(containers.DeclarativeContainer):
"""Application container.""" """Application container."""
app = flask.Application(Flask, __name__)
config = providers.Configuration() config = providers.Configuration()
github_client = providers.Factory( github_client = providers.Factory(
@ -92,21 +95,59 @@ This place is called **the container**. You use the container to manage all the
github_client=github_client, github_client=github_client,
) )
index_view = providers.Callable( index_view = flask.View(
views.index, views.index,
search_service=search_service, search_service=search_service,
default_search_term=config.search.default_term, default_search_term=config.search.default_term,
default_search_limit=config.search.default_limit, default_search_limit=config.search.default_limit,
) )
app = providers.Factory( Running such container looks like this:
flask.create_app,
name=__name__,
routes=[
flask.Route('/', view_provider=index_view),
],
)
.. code-block:: python
from .containers import ApplicationContainer
def create_app():
"""Create and return Flask application."""
container = ApplicationContainer()
container.config.from_yaml('config.yml')
app = container.app()
app.container = container
app.add_url_rule('/', view_func=container.index_view.as_view())
return app
And testing looks like:
.. code-block:: python
from unittest import mock
import pytest
from github import Github
from flask import url_for
from .application import create_app
@pytest.fixture
def app():
return create_app()
def test_index(client, app):
github_client_mock = mock.Mock(spec=Github)
# Configure mock
with app.container.github_client.override(github_client_mock):
response = client.get(url_for('index'))
assert response.status_code == 200
# Do more asserts
See complete example here - `Flask + Dependency Injector Example <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/ghnav-flask>`_ See complete example here - `Flask + Dependency Injector Example <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/ghnav-flask>`_

View File

@ -7,6 +7,11 @@ 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`_
3.21.0
------
- Re-design ``Flask`` integration.
- Make cosmetic fixes for ``Selector`` provider docs.
3.20.1 3.20.1
------ ------
- Hotfix Windows builds. - Hotfix Windows builds.

View File

@ -15,7 +15,7 @@ Selector providers
The ``selector`` callable is provided as a first positional argument. It can be The ``selector`` callable is provided as a first positional argument. It can be
:py:class:`Configuration` provider or any other callable. It has to return a string value. :py:class:`Configuration` provider or any other callable. It has to return a string value.
That value is used as a key for selecting the provider from the dictionary of providers. This value is used as a key for selecting the provider from the dictionary of providers.
The providers are provided as keyword arguments. Argument name is used as a key for The providers are provided as keyword arguments. Argument name is used as a key for
selecting the provider. selecting the provider.

View File

@ -7,6 +7,13 @@ Application ``githubnavigator`` is a `Flask <https://flask.palletsprojects.com/>
Run Run
--- ---
Create virtual environment:
.. code-block:: bash
virtualenv venv
. venv/bin/activate
Install requirements: Install requirements:
.. code-block:: bash .. code-block:: bash
@ -17,7 +24,7 @@ To run the application do:
.. code-block:: bash .. code-block:: bash
export FLASK_APP=githubnavigator.entrypoint export FLASK_APP=githubnavigator.application
export FLASK_ENV=development export FLASK_ENV=development
flask run flask run
@ -25,7 +32,7 @@ The output should be something like:
.. code-block:: .. code-block::
* Serving Flask app "githubnavigator.entrypoint" (lazy loading) * Serving Flask app "githubnavigator.application" (lazy loading)
* Environment: development * Environment: development
* Debug mode: on * Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
@ -81,10 +88,10 @@ The output should be something like:
Name Stmts Miss Cover Name Stmts Miss Cover
---------------------------------------------------- ----------------------------------------------------
githubnavigator/__init__.py 0 0 100% githubnavigator/__init__.py 0 0 100%
githubnavigator/application.py 10 0 100% githubnavigator/application.py 8 0 100%
githubnavigator/entrypoint.py 5 5 0% githubnavigator/containers.py 11 0 100%
githubnavigator/services.py 13 0 100% githubnavigator/services.py 14 0 100%
githubnavigator/tests.py 38 0 100% githubnavigator/tests.py 33 0 100%
githubnavigator/views.py 7 0 100% githubnavigator/views.py 7 0 100%
---------------------------------------------------- ----------------------------------------------------
TOTAL 73 5 93% TOTAL 73 0 100%

View File

@ -1,39 +1,16 @@
"""Application module.""" """Application module."""
from dependency_injector import containers, providers from .containers import ApplicationContainer
from dependency_injector.ext import flask
from github import Github
from . import services, views
class Application(containers.DeclarativeContainer): def create_app():
"""Application container.""" """Create and return Flask application."""
container = ApplicationContainer()
container.config.from_yaml('config.yml')
config = providers.Configuration() app = container.app()
app.container = container
github_client = providers.Factory( app.add_url_rule('/', view_func=container.index_view.as_view())
Github,
login_or_token=config.github.auth_token,
timeout=config.github.request_timeout,
)
search_service = providers.Factory( return app
services.SearchService,
github_client=github_client,
)
index_view = providers.Callable(
views.index,
search_service=search_service,
default_search_term=config.search.default_term,
default_search_limit=config.search.default_limit,
)
app = providers.Factory(
flask.create_app,
name=__name__,
routes=[
flask.Route('/', view_provider=index_view),
],
)

View File

@ -0,0 +1,34 @@
"""Application containers module."""
from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from github import Github
from . import views, services
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = flask.Application(Flask, __name__)
config = providers.Configuration()
github_client = providers.Factory(
Github,
login_or_token=config.github.auth_token,
timeout=config.github.request_timeout,
)
search_service = providers.Factory(
services.SearchService,
github_client=github_client,
)
index_view = flask.View(
views.index,
search_service=search_service,
default_search_term=config.search.default_term,
default_search_limit=config.search.default_limit,
)

View File

@ -1,9 +0,0 @@
"""Entrypoint module."""
from .application import Application
application = Application()
application.config.from_yaml('config.yml')
application.config.github.auth_token.from_env('GITHUB_TOKEN')
app = application.app()

View File

@ -20,6 +20,7 @@ class SearchService:
] ]
def _format_repo(self, repository: Repository): def _format_repo(self, repository: Repository):
commits = repository.get_commits()
return { return {
'url': repository.html_url, 'url': repository.html_url,
'name': repository.name, 'name': repository.name,
@ -29,7 +30,7 @@ class SearchService:
'avatar_url': repository.owner.avatar_url, 'avatar_url': repository.owner.avatar_url,
}, },
'created_at': repository.created_at, 'created_at': repository.created_at,
'latest_commit': self._format_commit(repository.get_commits()[0]), 'latest_commit': self._format_commit(commits[0]) if commits else {},
} }
def _format_commit(self, commit: Commit): def _format_commit(self, commit: Commit):

View File

@ -6,33 +6,15 @@ import pytest
from github import Github from github import Github
from flask import url_for from flask import url_for
from .application import Application from .application import create_app
@pytest.fixture @pytest.fixture
def application(): def app():
application = Application() return create_app()
application.config.from_dict(
{
'github': {
'auth_token': 'test-token',
'request_timeout': 10,
},
'search': {
'default_term': 'Dependency Injector',
'default_limit': 5,
},
}
)
return application
@pytest.fixture() def test_index(client, app):
def app(application: Application):
return application.app()
def test_index(client, application: Application):
github_client_mock = mock.Mock(spec=Github) github_client_mock = mock.Mock(spec=Github)
github_client_mock.search_repositories.return_value = [ github_client_mock.search_repositories.return_value = [
mock.Mock( mock.Mock(
@ -59,7 +41,7 @@ def test_index(client, application: Application):
), ),
] ]
with application.github_client.override(github_client_mock): with app.container.github_client.override(github_client_mock):
response = client.get(url_for('index')) response = client.get(url_for('index'))
assert response.status_code == 200 assert response.status_code == 200
@ -79,11 +61,11 @@ def test_index(client, application: Application):
assert b'repo2-created-at' in response.data assert b'repo2-created-at' in response.data
def test_index_no_results(client, application: Application): def test_index_no_results(client, app):
github_client_mock = mock.Mock(spec=Github) github_client_mock = mock.Mock(spec=Github)
github_client_mock.search_repositories.return_value = [] github_client_mock.search_repositories.return_value = []
with application.github_client.override(github_client_mock): with app.container.github_client.override(github_client_mock):
response = client.get(url_for('index')) response = client.get(url_for('index'))
assert response.status_code == 200 assert response.status_code == 200

View File

@ -1,6 +1,6 @@
"""Dependency injector top-level package.""" """Dependency injector top-level package."""
__version__ = '3.20.1' __version__ = '3.21.0'
"""Version number that follows semantic versioning. """Version number that follows semantic versioning.
:type: str :type: str

View File

@ -2,17 +2,36 @@
from __future__ import absolute_import from __future__ import absolute_import
from flask import Flask from flask import request as flask_request
from dependency_injector import providers, errors from dependency_injector import providers, errors
def create_app(name, routes, **kwargs): request = providers.Object(flask_request)
"""Create Flask app and add routes."""
app = Flask(name, **kwargs)
for route in routes: class Application(providers.Singleton):
app.add_url_rule(*route.args, **route.options) """Flask application provider."""
return app
class Extension(providers.Singleton):
"""Flask extension provider."""
class View(providers.Callable):
"""Flask view provider."""
def as_view(self):
"""Return Flask view function."""
return as_view(self)
class ClassBasedView(providers.Factory):
"""Flask class-based view provider."""
def as_view(self, name):
"""Return Flask view function."""
return as_view(self, name)
def as_view(provider, name=None): def as_view(provider, name=None):
@ -49,35 +68,3 @@ def as_view(provider, name=None):
view.provide_automatic_options = provider.provides.provide_automatic_options view.provide_automatic_options = provider.provides.provide_automatic_options
return view return view
class Route:
"""Route is a glue for Dependency Injector providers and Flask views."""
def __init__(
self,
rule,
endpoint=None,
view_provider=None,
provide_automatic_options=None,
**options):
"""Initialize route."""
self.view_provider = view_provider
self.args = (rule, endpoint, as_view(view_provider, endpoint), provide_automatic_options)
self.options = options
def __deepcopy__(self, memo):
"""Create and return full copy of provider."""
copied = memo.get(id(self))
if copied is not None:
return copied
rule, endpoint, _, provide_automatic_options = self.args
view_provider = providers.deepcopy(self.view_provider, memo)
return self.__class__(
rule,
endpoint,
view_provider,
provide_automatic_options,
**self.options)

View File

@ -1,7 +1,7 @@
"""Dependency injector Flask extension unit tests.""" """Dependency injector Flask extension unit tests."""
import unittest2 as unittest import unittest2 as unittest
from flask import url_for from flask import Flask, url_for
from flask.views import MethodView from flask.views import MethodView
from dependency_injector import containers, providers from dependency_injector import containers, providers
@ -21,28 +21,29 @@ class Test(MethodView):
return 'Test class-based!' return 'Test class-based!'
class Application(containers.DeclarativeContainer): class ApplicationContainer(containers.DeclarativeContainer):
index_view = providers.Callable(index) app = flask.Application(Flask, __name__)
test_view = providers.Callable(test)
test_class_view = providers.Factory(Test)
app = providers.Factory( index_view = flask.View(index)
flask.create_app, test_view = flask.View(test)
name=__name__, test_class_view = flask.ClassBasedView(Test)
routes=[
flask.Route('/', view_provider=index_view),
flask.Route('/test', 'test-test', test_view), def create_app():
flask.Route('/test-class', 'test-class', test_class_view) container = ApplicationContainer()
], app = container.app()
) app.container = container
app.add_url_rule('/', view_func=container.index_view.as_view())
app.add_url_rule('/test', 'test-test', view_func=container.test_view.as_view())
app.add_url_rule('/test-class', view_func=container.test_class_view.as_view('test-class'))
return app
class ApplicationTests(unittest.TestCase): class ApplicationTests(unittest.TestCase):
def setUp(self): def setUp(self):
application = Application() self.app = create_app()
self.app = application.app()
self.app.config['SERVER_NAME'] = 'test-server.com' self.app.config['SERVER_NAME'] = 'test-server.com'
self.client = self.app.test_client() self.client = self.app.test_client()
self.client.__enter__() self.client.__enter__()