Re-design Flask integration

This commit is contained in:
Roman Mogylatov 2020-07-13 22:45:15 -04:00
parent 359dce2978
commit 72147b664e
10 changed files with 162 additions and 140 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.*
``Flask`` + ``Dependency Injector`` example:
``Flask`` + ``Dependency Injector`` example application container:
.. code-block:: python
from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from github import Github
from . import services, views
from . import views, services
class Application(containers.DeclarativeContainer):
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = flask.Application(Flask, __name__)
config = providers.Configuration()
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,
)
index_view = providers.Callable(
index_view = flask.View(
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),
],
)
Running such container looks like this:
.. 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>`_

View File

@ -9,6 +9,7 @@ follows `Semantic versioning`_
Development version
-------------------
- Re-design ``Flask`` integration.
- Make cosmetic fixes for ``Selector`` provider docs.
3.20.1

View File

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

View File

@ -1,39 +1,16 @@
"""Application module."""
from dependency_injector import containers, providers
from dependency_injector.ext import flask
from github import Github
from . import services, views
from .containers import ApplicationContainer
class Application(containers.DeclarativeContainer):
"""Application container."""
def create_app():
"""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(
Github,
login_or_token=config.github.auth_token,
timeout=config.github.request_timeout,
)
app.add_url_rule('/', view_func=container.index_view.as_view())
search_service = providers.Factory(
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),
],
)
return app

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):
commits = repository.get_commits()
return {
'url': repository.html_url,
'name': repository.name,
@ -29,7 +30,7 @@ class SearchService:
'avatar_url': repository.owner.avatar_url,
},
'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):

View File

@ -6,33 +6,15 @@ import pytest
from github import Github
from flask import url_for
from .application import Application
from .application import create_app
@pytest.fixture
def application():
application = Application()
application.config.from_dict(
{
'github': {
'auth_token': 'test-token',
'request_timeout': 10,
},
'search': {
'default_term': 'Dependency Injector',
'default_limit': 5,
},
}
)
return application
def app():
return create_app()
@pytest.fixture()
def app(application: Application):
return application.app()
def test_index(client, application: Application):
def test_index(client, app):
github_client_mock = mock.Mock(spec=Github)
github_client_mock.search_repositories.return_value = [
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'))
assert response.status_code == 200
@ -79,11 +61,11 @@ def test_index(client, application: Application):
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.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'))
assert response.status_code == 200

View File

@ -2,17 +2,36 @@
from __future__ import absolute_import
from flask import Flask
from flask import request as flask_request
from dependency_injector import providers, errors
def create_app(name, routes, **kwargs):
"""Create Flask app and add routes."""
app = Flask(name, **kwargs)
for route in routes:
app.add_url_rule(*route.args, **route.options)
return app
request = providers.Object(flask_request)
class Application(providers.Singleton):
"""Flask application provider."""
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):
@ -49,35 +68,3 @@ def as_view(provider, name=None):
view.provide_automatic_options = provider.provides.provide_automatic_options
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."""
import unittest2 as unittest
from flask import url_for
from flask import Flask, url_for
from flask.views import MethodView
from dependency_injector import containers, providers
@ -21,28 +21,29 @@ class Test(MethodView):
return 'Test class-based!'
class Application(containers.DeclarativeContainer):
class ApplicationContainer(containers.DeclarativeContainer):
index_view = providers.Callable(index)
test_view = providers.Callable(test)
test_class_view = providers.Factory(Test)
app = flask.Application(Flask, __name__)
app = providers.Factory(
flask.create_app,
name=__name__,
routes=[
flask.Route('/', view_provider=index_view),
flask.Route('/test', 'test-test', test_view),
flask.Route('/test-class', 'test-class', test_class_view)
],
)
index_view = flask.View(index)
test_view = flask.View(test)
test_class_view = flask.ClassBasedView(Test)
def create_app():
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):
def setUp(self):
application = Application()
self.app = application.app()
self.app = create_app()
self.app.config['SERVER_NAME'] = 'test-server.com'
self.client = self.app.test_client()
self.client.__enter__()