mirror of
https://github.com/ets-labs/python-dependency-injector.git
synced 2025-02-07 07:00:49 +03:00
Merge branch 'release/3.21.0' into master
This commit is contained in:
commit
0c77e73d51
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.*
|
*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>`_
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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%
|
||||||
|
|
|
@ -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),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
34
examples/miniapps/ghnav-flask/githubnavigator/containers.py
Normal file
34
examples/miniapps/ghnav-flask/githubnavigator/containers.py
Normal 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,
|
||||||
|
)
|
|
@ -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):
|
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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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__()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user