Flask +依赖注入器-依赖注入指南

嗨,



我是Dependency Injector的创建者这是Python的依赖项注入框架。



在本教程中,我想展示如何使用Dependency Injector开发Flask应用程序。



该手册包括以下部分:



  1. 我们要建造什么?
  2. 准备环境
  3. 项目结构
  4. 你好,世界!
  5. 包括样式
  6. 连接Github
  7. 搜索服务
  8. 连接搜索
  9. 一点重构
  10. 添加测试
  11. 结论


可以在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-flaskrequirements.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 -主页模板


templatesbase.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令牌。



为此,您需要:



  • 在Github上遵循本教程
  • 将令牌设置为环境变量:



    export GITHUB_TOKEN=<your token>


可以暂时跳过此项目。



该应用程序将在没有令牌的情况下运行,但带宽有限。未认证客户端的限制:每小时60个请求。需要令牌以将该配额增加到每小时5000。


做完了



客户端Github API安装完成。



搜索服务



现在该添加搜索服务了SearchService他会的:



  • 在Github上搜索
  • 获取有关提交的其他数据
  • 转换格式结果


SearchService将使用Github API客户端。在包中



创建一个空文件services.pygithubnavigator



./
├── 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)


连接搜索



现在我们可以开始搜索了。让我们SearchServiceindex视图中使用



编辑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使代码更干净。



添加测试



添加一些测试会很好。我们开始做吧。



我们将使用pytestcoverage



编辑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,
    )




一个容器,作为您的应用程序的映射。您总是知道什么取决于什么。



下一步是什么?






All Articles