diff --git a/README.rst b/README.rst index 84c639ff..70de14e9 100644 --- a/README.rst +++ b/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 `_ diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index af80e0bb..a56c23cb 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -9,6 +9,7 @@ follows `Semantic versioning`_ Development version ------------------- +- Re-design ``Flask`` integration. - Make cosmetic fixes for ``Selector`` provider docs. 3.20.1 diff --git a/examples/miniapps/ghnav-flask/README.rst b/examples/miniapps/ghnav-flask/README.rst index 699e420a..89f6f64f 100644 --- a/examples/miniapps/ghnav-flask/README.rst +++ b/examples/miniapps/ghnav-flask/README.rst @@ -7,6 +7,13 @@ Application ``githubnavigator`` is a `Flask 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% diff --git a/examples/miniapps/ghnav-flask/githubnavigator/application.py b/examples/miniapps/ghnav-flask/githubnavigator/application.py index 1bffa314..b7cc4cfc 100644 --- a/examples/miniapps/ghnav-flask/githubnavigator/application.py +++ b/examples/miniapps/ghnav-flask/githubnavigator/application.py @@ -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 diff --git a/examples/miniapps/ghnav-flask/githubnavigator/containers.py b/examples/miniapps/ghnav-flask/githubnavigator/containers.py new file mode 100644 index 00000000..40f4d435 --- /dev/null +++ b/examples/miniapps/ghnav-flask/githubnavigator/containers.py @@ -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, + ) diff --git a/examples/miniapps/ghnav-flask/githubnavigator/entrypoint.py b/examples/miniapps/ghnav-flask/githubnavigator/entrypoint.py deleted file mode 100644 index a64be771..00000000 --- a/examples/miniapps/ghnav-flask/githubnavigator/entrypoint.py +++ /dev/null @@ -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() diff --git a/examples/miniapps/ghnav-flask/githubnavigator/services.py b/examples/miniapps/ghnav-flask/githubnavigator/services.py index d167404e..413e9b04 100644 --- a/examples/miniapps/ghnav-flask/githubnavigator/services.py +++ b/examples/miniapps/ghnav-flask/githubnavigator/services.py @@ -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): diff --git a/examples/miniapps/ghnav-flask/githubnavigator/tests.py b/examples/miniapps/ghnav-flask/githubnavigator/tests.py index 0fe96dbd..0d6267ed 100644 --- a/examples/miniapps/ghnav-flask/githubnavigator/tests.py +++ b/examples/miniapps/ghnav-flask/githubnavigator/tests.py @@ -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 diff --git a/src/dependency_injector/ext/flask.py b/src/dependency_injector/ext/flask.py index 8dcafc70..bcc696c8 100644 --- a/src/dependency_injector/ext/flask.py +++ b/src/dependency_injector/ext/flask.py @@ -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) diff --git a/tests/unit/ext/test_flask_py2_py3.py b/tests/unit/ext/test_flask_py2_py3.py index dac02e4b..7b9695a4 100644 --- a/tests/unit/ext/test_flask_py2_py3.py +++ b/tests/unit/ext/test_flask_py2_py3.py @@ -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__()