Re-design Flask integration
This commit is contained in:
parent
359dce2978
commit
72147b664e
63
README.rst
63
README.rst
|
@ -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>`_
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ follows `Semantic versioning`_
|
|||
|
||||
Development version
|
||||
-------------------
|
||||
- Re-design ``Flask`` integration.
|
||||
- Make cosmetic fixes for ``Selector`` provider docs.
|
||||
|
||||
3.20.1
|
||||
|
|
|
@ -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%
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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()
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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__()
|
||||
|
|
Loading…
Reference in New Issue