我是Dependency Injector的创建者。这是Python的依赖项注入框架。
在本教程中,我想展示如何使用Dependency Injector开发Flask应用程序。
该手册包括以下部分:
可以在Github上找到完成的项目。
首先,您必须具备:
- Python 3.5+
- 虚拟环境
并且希望具有:
- Flask的初步开发技能
- 了解依赖注入的原理
我们要建造什么?
我们将构建一个应用程序,以帮助您在Github上搜索存储库。我们称之为Github Navigator。
Github Navigator如何工作?
- 用户打开一个网页,提示他输入搜索查询。
- 用户输入查询,然后按Enter。
- Github导航器会在Github上寻找匹配的存储库。
- 搜索完成后,Github导航器会向用户显示一个包含结果的网页。
- 结果页面显示所有找到的存储库和搜索查询。
- 对于每个存储库,用户将看到:
- 仓库名称
- 仓库所有者
- 对存储库的最后一次提交
- 用户可以单击任何元素以在Github上打开其页面。
准备环境
首先,我们需要创建一个项目文件夹和一个虚拟环境:
mkdir ghnav-flask-tutorial
cd ghnav-flask-tutorial
python3 -m venv venv
现在让我们激活虚拟环境:
. venv/bin/activate
环境已经准备就绪,现在让我们从项目结构开始。
项目结构
让我们在当前文件夹中创建以下结构。现在将所有文件留空。这还不是关键。
初始结构:
./
├── githubnavigator/
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
└── requirements.txt
现在该安装Flask和Dependency Injector。
让我们在文件中添加以下几行
requirements.txt
:
dependency-injector
flask
现在让我们安装它们:
pip install -r requirements.txt
并检查安装是否成功:
python -c "import dependency_injector; print(dependency_injector.__version__)"
python -c "import flask; print(flask.__version__)"
您将看到类似以下内容:
(venv) $ python -c "import dependency_injector; print(dependency_injector.__version__)"
3.22.0
(venv) $ python -c "import flask; print(flask.__version__)"
1.1.2
你好,世界!
让我们创建一个最小的hello world应用程序。
让我们在文件中添加以下几行
views.py
:
"""Views module."""
def index():
return 'Hello, World!'
现在,我们为依赖项添加一个容器(以下简称为容器)。该容器将包含应用程序的所有组件。让我们添加前两个组件。这是一个Flask应用程序和视图
index
。
让我们将以下内容添加到文件中
containers.py
:
"""Application containers module."""
from dependency_injector import containers
from dependency_injector.ext import flask
from flask import Flask
from . import views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = flask.Application(Flask, __name__)
index_view = flask.View(views.index)
现在我们需要创建一个Flask应用程序工厂。通常称为
create_app()
。它将创建一个容器。该容器将用于创建Flask应用程序。最后一步是设置路由-我们将分配index_view
来自容器的视图以处理对应用程序根目录“ /”的请求。
让我们编辑
application.py
:
"""Application module."""
from .containers import ApplicationContainer
def create_app():
"""Create and return Flask application."""
container = ApplicationContainer()
app = container.app()
app.container = container
app.add_url_rule('/', view_func=container.index_view.as_view())
return app
容器是应用程序中的第一个对象。它用于获取所有其他对象。
我们的应用程序现在准备说“你好,世界!”
在终端中运行:
export FLASK_APP=githubnavigator.application
export FLASK_ENV=development
flask run
输出应如下所示:
* 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)
* Restarting with fsevents reloader
* Debugger is active!
* Debugger PIN: 473-587-859
打开浏览器,然后转到http://127.0.0.1:5000/。
您将看到“你好,世界!”
优秀的。我们最小的应用程序将启动并成功运行。
让我们使其更漂亮。
包括样式
我们将使用Bootstrap 4。让我们为此使用Bootstrap-Flask扩展。只需单击几下,它将帮助我们添加所有必需的文件。
添加
bootstrap-flask
到requirements.txt
:
dependency-injector
flask
bootstrap-flask
并在终端中执行:
pip install --upgrade -r requirements.txt
现在,将扩展名添加
bootstrap-flask
到容器中。
编辑
containers.py
:
"""Application containers module."""
from dependency_injector import containers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from . import views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = flask.Application(Flask, __name__)
bootstrap = flask.Extension(Bootstrap)
index_view = flask.View(views.index)
让我们初始化extension
bootstrap-flask
。我们将需要改变create_app()
。
编辑
application.py
:
"""Application module."""
from .containers import ApplicationContainer
def create_app():
"""Create and return Flask application."""
container = ApplicationContainer()
app = container.app()
app.container = container
bootstrap = container.bootstrap()
bootstrap.init_app(app)
app.add_url_rule('/', view_func=container.index_view.as_view())
return app
现在我们需要添加模板。为此,我们需要
templates/
在package中添加一个文件夹githubnavigator
。在模板文件夹内添加两个文件:
base.html
-基本模板index.html
-主页模板
templates
在base.html
和中
创建一个文件夹和两个空文件index.html
:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
└── requirements.txt
现在让我们填写基本模板。
让我们在文件中添加以下几行
base.html
:
<!doctype html>
<html lang="en">
<head>
{% block head %}
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{% block styles %}
<!-- Bootstrap CSS -->
{{ bootstrap.load_css() }}
{% endblock %}
<title>{% block title %}{% endblock %}</title>
{% endblock %}
</head>
<body>
<!-- Your page content -->
{% block content %}{% endblock %}
{% block scripts %}
<!-- Optional JavaScript -->
{{ bootstrap.load_js() }}
{% endblock %}
</body>
</html>
现在,让我们填写母版页模板。
让我们在文件中添加以下几行
index.html
:
{% extends "base.html" %}
{% block title %}Github Navigator{% endblock %}
{% block content %}
<div class="container">
<h1 class="mb-4">Github Navigator</h1>
<form>
<div class="form-group form-row">
<div class="col-10">
<label for="search_query" class="col-form-label">
Search for:
</label>
<input class="form-control" type="text" id="search_query"
placeholder="Type something to search on the GitHub"
name="query"
value="{{ query if query }}">
</div>
<div class="col">
<label for="search_limit" class="col-form-label">
Limit:
</label>
<select class="form-control" id="search_limit" name="limit">
{% for value in [5, 10, 20] %}
<option {% if value == limit %}selected{% endif %}>
{{ value }}
</option>
{% endfor %}
</select>
</div>
</div>
</form>
<p><small>Results found: {{ repositories|length }}</small></p>
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Repository</th>
<th class="text-nowrap">Repository owner</th>
<th class="text-nowrap">Last commit</th>
</tr>
</thead>
<tbody>
{% for repository in repositories %} {{n}}
<tr>
<th>{{ loop.index }}</th>
<td><a href="{{ repository.url }}">
{{ repository.name }}</a>
</td>
<td><a href="{{ repository.owner.url }}">
<img src="{{ repository.owner.avatar_url }}"
alt="avatar" height="24" width="24"/></a>
<a href="{{ repository.owner.url }}">
{{ repository.owner.login }}</a>
</td>
<td><a href="{{ repository.latest_commit.url }}">
{{ repository.latest_commit.sha }}</a>
{{ repository.latest_commit.message }}
{{ repository.latest_commit.author_name }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
太好了,差不多完成了。最后一步是更改视图
index
以使用模板index.html
。
让我们编辑
views.py
:
"""Views module."""
from flask import request, render_template
def index():
query = request.args.get('query', 'Dependency Injector')
limit = request.args.get('limit', 10, int)
repositories = []
return render_template(
'index.html',
query=query,
limit=limit,
repositories=repositories,
)
做完了
确保应用程序正在运行或正在运行,
flask run
然后打开http://127.0.0.1:5000/。
您应该看到:
连接Github
在本节中,我们将把我们的应用程序与Github API集成在一起。
我们将使用PyGithub库。
让我们将其添加到
requirements.txt
:
dependency-injector
flask
bootstrap-flask
pygithub
并在终端中执行:
pip install --upgrade -r requirements.txt
现在我们需要将Github API客户端添加到容器中。为此,我们将需要使用模块中的两个新提供程序
dependency_injector.providers
:
- 提供者
Factory
将创建Github客户端。 - 提供者
Configuration
会将API令牌和Github超时传递给客户端。
我们开始做吧。
让我们编辑
containers.py
:
"""Application containers module."""
from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github
from . import views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = flask.Application(Flask, __name__)
bootstrap = flask.Extension(Bootstrap)
config = providers.Configuration()
github_client = providers.Factory(
Github,
login_or_token=config.github.auth_token,
timeout=config.github.request_timeout,
)
index_view = flask.View(views.index)
在设置它们的值之前,我们使用了配置参数。这是提供者工作的原则Configuration
。
首先我们使用,然后设置值。
现在,让我们添加配置文件。
我们将使用YAML。在项目的根目录
创建一个空文件
config.yml
:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ └── views.py
├── venv/
├── config.yml
└── requirements.txt
并用以下几行填写:
github:
request_timeout: 10
要使用配置文件,我们将使用PyYAML库。让我们将其添加到具有依赖性的文件中。
编辑
requirements.txt
:
dependency-injector
flask
bootstrap-flask
pygithub
pyyaml
并安装依赖项:
pip install --upgrade -r requirements.txt
我们将使用环境变量来传递API令牌
GITHUB_TOKEN
。
现在,我们需要编辑
create_app()
以在应用程序启动时执行2个操作:
- 从加载配置
config.yml
- 从环境变量加载API令牌
GITHUB_TOKEN
编辑
application.py
:
"""Application module."""
from .containers import ApplicationContainer
def create_app():
"""Create and return Flask application."""
container = ApplicationContainer()
container.config.from_yaml('config.yml')
container.config.github.auth_token.from_env('GITHUB_TOKEN')
app = container.app()
app.container = container
bootstrap = container.bootstrap()
bootstrap.init_app(app)
app.add_url_rule('/', view_func=container.index_view.as_view())
return app
现在我们需要创建一个API令牌。
为此,您需要:
可以暂时跳过此项目。
该应用程序将在没有令牌的情况下运行,但带宽有限。未认证客户端的限制:每小时60个请求。需要令牌以将该配额增加到每小时5000。
做完了
客户端Github API安装完成。
搜索服务
现在该添加搜索服务了
SearchService
。他会的:
- 在Github上搜索
- 获取有关提交的其他数据
- 转换格式结果
SearchService
将使用Github API客户端。在包中
创建一个空文件:
services.py
githubnavigator
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── services.py
│ └── views.py
├── venv/
├── config.yml
└── requirements.txt
并添加以下几行:
"""Services module."""
from github import Github
from github.Repository import Repository
from github.Commit import Commit
class SearchService:
"""Search service performs search on Github."""
def __init__(self, github_client: Github):
self._github_client = github_client
def search_repositories(self, query, limit):
"""Search for repositories and return formatted data."""
repositories = self._github_client.search_repositories(
query=query,
**{'in': 'name'},
)
return [
self._format_repo(repository)
for repository in repositories[:limit]
]
def _format_repo(self, repository: Repository):
commits = repository.get_commits()
return {
'url': repository.html_url,
'name': repository.name,
'owner': {
'login': repository.owner.login,
'url': repository.owner.html_url,
'avatar_url': repository.owner.avatar_url,
},
'latest_commit': self._format_commit(commits[0]) if commits else {},
}
def _format_commit(self, commit: Commit):
return {
'sha': commit.sha,
'url': commit.html_url,
'message': commit.commit.message,
'author_name': commit.commit.author.name,
}
现在让我们添加
SearchService
到容器中。
编辑
containers.py
:
"""Application containers module."""
from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github
from . import services, views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = flask.Application(Flask, __name__)
bootstrap = flask.Extension(Bootstrap)
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)
连接搜索
现在我们可以开始搜索了。让我们
SearchService
在index
视图中使用。
编辑
views.py
:
"""Views module."""
from flask import request, render_template
from .services import SearchService
def index(search_service: SearchService):
query = request.args.get('query', 'Dependency Injector')
limit = request.args.get('limit', 10, int)
repositories = search_service.search_repositories(query, limit)
return render_template(
'index.html',
query=query,
limit=limit,
repositories=repositories,
)
现在,让我们更改容器,
SearchService
以便index
在调用依赖项时将其传递给视图。
编辑
containers.py
:
"""Application containers module."""
from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github
from . import services, views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = flask.Application(Flask, __name__)
bootstrap = flask.Extension(Bootstrap)
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,
)
确保应用程序正在运行或正在运行,
flask run
然后打开http://127.0.0.1:5000/。
你会看见:
一点重构
我们的视图
index
包含两个硬编码值:
- 默认搜索词
- 结果数限制
让我们做一些重构。我们会将这些值传输到配置中。
编辑
views.py
:
"""Views module."""
from flask import request, render_template
from .services import SearchService
def index(
search_service: SearchService,
default_query: str,
default_limit: int,
):
query = request.args.get('query', default_query)
limit = request.args.get('limit', default_limit, int)
repositories = search_service.search_repositories(query, limit)
return render_template(
'index.html',
query=query,
limit=limit,
repositories=repositories,
)
现在我们需要这些值在调用时传递。让我们更新容器。
编辑
containers.py
:
"""Application containers module."""
from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github
from . import services, views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = flask.Application(Flask, __name__)
bootstrap = flask.Extension(Bootstrap)
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_query=config.search.default_query,
default_limit=config.search.default_limit,
)
现在,让我们更新配置文件。
编辑
config.yml
:
github:
request_timeout: 10
search:
default_query: "Dependency Injector"
default_limit: 10
做完了
重构完成。Mu使代码更干净。
添加测试
添加一些测试会很好。我们开始做吧。
我们将使用pytest和coverage。
编辑
requirements.txt
:
dependency-injector
flask
bootstrap-flask
pygithub
pyyaml
pytest-flask
pytest-cov
并安装新软件包:
pip install -r requirements.txt
tests.py
在包中
创建一个空文件githubnavigator
:
./
├── githubnavigator/
│ ├── templates/
│ │ ├── base.html
│ │ └── index.html
│ ├── __init__.py
│ ├── application.py
│ ├── containers.py
│ ├── services.py
│ ├── tests.py
│ └── views.py
├── venv/
├── config.yml
└── requirements.txt
并添加以下几行:
"""Tests module."""
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)
github_client_mock.search_repositories.return_value = [
mock.Mock(
html_url='repo1-url',
name='repo1-name',
owner=mock.Mock(
login='owner1-login',
html_url='owner1-url',
avatar_url='owner1-avatar-url',
),
get_commits=mock.Mock(return_value=[mock.Mock()]),
),
mock.Mock(
html_url='repo2-url',
name='repo2-name',
owner=mock.Mock(
login='owner2-login',
html_url='owner2-url',
avatar_url='owner2-avatar-url',
),
get_commits=mock.Mock(return_value=[mock.Mock()]),
),
]
with app.container.github_client.override(github_client_mock):
response = client.get(url_for('index'))
assert response.status_code == 200
assert b'Results found: 2' in response.data
assert b'repo1-url' in response.data
assert b'repo1-name' in response.data
assert b'owner1-login' in response.data
assert b'owner1-url' in response.data
assert b'owner1-avatar-url' in response.data
assert b'repo2-url' in response.data
assert b'repo2-name' in response.data
assert b'owner2-login' in response.data
assert b'owner2-url' in response.data
assert b'owner2-avatar-url' in response.data
def test_index_no_results(client, app):
github_client_mock = mock.Mock(spec=Github)
github_client_mock.search_repositories.return_value = []
with app.container.github_client.override(github_client_mock):
response = client.get(url_for('index'))
assert response.status_code == 200
assert b'Results found: 0' in response.data
现在让我们开始测试并检查覆盖率:
py.test githubnavigator/tests.py --cov=githubnavigator
你会看见:
platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
plugins: flask-1.0.0, cov-2.10.0
collected 2 items
githubnavigator/tests.py .. [100%]
---------- coverage: platform darwin, python 3.8.3-final-0 -----------
Name Stmts Miss Cover
----------------------------------------------------
githubnavigator/__init__.py 0 0 100%
githubnavigator/application.py 11 0 100%
githubnavigator/containers.py 13 0 100%
githubnavigator/services.py 14 0 100%
githubnavigator/tests.py 32 0 100%
githubnavigator/views.py 7 0 100%
----------------------------------------------------
TOTAL 77 0 100%
注意我们如何github_client
使用方法替换为模拟.override()
。这样,您可以覆盖任何提供程序的返回值。
结论
我们使用依赖注入构建了Flask应用程序。我们使用了依赖注入器作为依赖注入框架。
我们应用程序的主要部分是容器。它在一处包含了应用程序的所有组件及其依赖性。这提供了对应用程序结构的控制。很容易理解和更改:
"""Application containers module."""
from dependency_injector import containers, providers
from dependency_injector.ext import flask
from flask import Flask
from flask_bootstrap import Bootstrap
from github import Github
from . import services, views
class ApplicationContainer(containers.DeclarativeContainer):
"""Application container."""
app = flask.Application(Flask, __name__)
bootstrap = flask.Extension(Bootstrap)
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_query=config.search.default_query,
default_limit=config.search.default_limit,
)
一个容器,作为您的应用程序的映射。您总是知道什么取决于什么。
下一步是什么?
- 在GitHub上了解有关Dependency Injector的更多信息
- 退房在阅读文档的文档
- 有疑问或发现错误?在Github上打开一个问题