你好!我创建这个仓库是为了跟踪关于 Flask 的一系列博客文章,但我从未完成过,也永远无法完成。实际上,我已经停止使用 Flask 转而使用 FastAPI,并建议你也这样做,以获得真正美妙的 Python REST API 创建体验。
Flask REST API:Flask 基础知识
这篇文章如何运作
- 这篇文章是系列文章的一部分,该系列文章详细分析了我使用 Python 和 Flask 创建 REST API 的方法。
- 每篇文章都是在假设读者已经阅读并理解该系列之前的所有文章的情况下撰写的。
- 我会尽力解释我所做的每一个选择。如果你觉得我遗漏了什么,请在下方评论区留言。
- 每个部分都解释了为什么以及如何使用特定的技巧或技术。您可以跳过已经熟悉的部分。
- 本系列生成的源代码可以在 GitHub 上找到。
在这篇文章中
这篇文章将指导您如何使用工厂模式创建一个非常简单的 Flask 应用。然后,它将使用 pytest 的测试驱动开发,在蓝图中创建一个简单的健康检查控制器。读完这篇文章后,您将拥有一个极其简单且测试覆盖率完整的 Web 服务器!
主题:
- Flask 工厂模式
- 文档字符串
- 类型注解
- 测试驱动开发
- pytest
- Flask 蓝图
Flask 工厂模式
Flask 的核心是Flask
app 对象,要开始使用 Flask,首先需要在项目中的某个位置创建一个这样的对象。教程中会介绍两种常用的创建方法:
-
在文件(通常名为“app.py”)中创建对象,该文件稍后将被任何为其提供服务的服务器导入。这种方法的主要优点是样板代码较少,可以快速启动并运行,因此是教程的热门选择。
-
声明一个“工厂方法”,用于返回应用实例。这种方法需要输入一些代码,但有很多好处:
- 通过将导入放入工厂方法来延迟导入以避免循环依赖(当您开始使用扩展时非常重要)
- 延迟扩展的初始化(使单元测试的修补更容易)
- 允许您针对不同的用例动态配置应用程序(例如测试、开发和生产)
- 更多内容,请查看此处的文档
我一直推荐使用工厂方法,它很容易上手。首先,在__init__.py
模块的基类中创建方法本身。
# python_rest/__init__.py
from flask import Flask
def create_app():
return Flask(__name__)
这就是声明应用程序的全部代码!现在我们只需要使用 Flask 开发服务器运行它。
在命令行上运行
我们可以轻松地在命令行上使用 Poetry 运行该应用,但首先我们必须告诉 Flask 要运行什么。幸运的是,它内置了对环境文件的支持,让我们可以声明各种内容。首先,将其添加python-dotenv
为开发依赖项。
$ poetry add -D python-dotenv
现在在项目根目录中创建一个名为“ .flaskenv”的文件,将环境变量“FLASK_APP”设置为模块的名称。
注意:Flask 建议将包含所有非敏感默认值的“.flaskenv”文件检入项目。之后,你可以使用更标准的“.env”文件覆盖这些默认值,但该文件永远不应检入源代码管理。
# .flaskenv
FLASK_APP=python_rest
现在您可以运行开发服务器了。请确保在 Poetry 下运行它!
$ poetry run flask run
在 PyCharm 中运行
- 在屏幕顶部,点击“添加配置”
- 点击“+”
- 选择“Flask 服务器”
- 将“目标类型”设置为“模块名称”
- 将“目标”设置为您的模块名称(例如“python_rest”)
- 点击“确定”
- 选择新配置后,单击绿色箭头开始运行
🎉 太棒了!现在你的本地机器默认 5000 端口上已经有一个 Web 服务器在运行了!它现在还什么都没做,但我保证它正在运行。
文档字符串
既然你已经编写了一个 Python 函数,那么你绝对应该为该函数编写一个文档字符串。编写代码文档的理由有很多,我发现最佳实践是为每个函数编写一个文档字符串,并至少包含几个单词的摘要。文档字符串在以后为你的 API 创建良好的文档时非常重要,所以最好立即养成这个习惯。
文档字符串有很多种格式,PyCharm 支持其中大多数格式,并且可以在 PyCharm 中渲染。本页对各种格式进行了很好的概述。我使用 ReStructuredText 字符串,因为 Sphinx 原生支持它们,而我在其他类型的项目中也使用过 Sphinx。它们看起来像这样:
def create_app():
"""
Creates an application instance to run
:return: A Flask object
"""
return Flask(__name__)
类型注解
类型注解的重要性毋庸置疑。Python 的动态类型系统非常棒,可以快速编写代码。然而,随着项目变得越来越复杂,它很容易导致你犯一些简单的错误。我给所有代码都添加了类型提示,这样 PyCharm 就能在我犯一些愚蠢的错误时(比如忘记在某个地方输入 return 语句)提醒我,这样静态分析工具(比如 mypy)就能捕捉到一些常见的错误。我们create_app
函数的类型注解如下所示:
def create_app() -> Flask:
测试驱动开发
测试驱动开发(TDD)简单来说就是在编写代码之前先为代码编写测试。有很多书都解释了为什么这是一个好主意,但我这样做的原因很简单:
- 你总是知道什么时候完成
- 您可以更加自信地进行重构
- 帮助你在开始之前思考边缘情况和不愉快的路径
- 鼓励模块化和解耦设计
Pytest
为了编写测试,你需要使用某种框架。Python 标准库中有一个叫做“Unittest”的框架,但我更喜欢pytest。使用它有很多理由,但基本原则如下:
- 很少的样板
- 大量社区插件
前面提到的插件中,我经常在测试 Flask 应用时使用pytest-flask,它提供了一系列实用的 Fixture。要开始测试,请同时安装 pytest 和 pytest-flask 作为开发依赖项。
$ poetry add -D pytest pytest-flask
现在,如果您正在使用 PyCharm,则需要将其配置为使用 pytest
- 打开偏好设置
- 单击“工具”
- 转到 Python 集成工具
- 将“默认测试运行器”更改为“pytest”
注意:您也可以在这里更改文档字符串格式
接下来,在 tests 目录中创建 pytest 的配置文件(名为 conftest.py)。此文件始终在测试运行前导入,您需要在其中定义任何要使用的 Fixture 并执行任何其他测试前设置。要使用 pytest-flask,我们必须定义一个返回对象实例的“app”Fixture Flask
。
# tests/conftest.py
import pytest
from flask import Flask
@pytest.fixture
def app() -> Flask:
""" Provides an instance of our Flask app """
from python_rest import create_app
return create_app()
蓝图
现在是时候创建一个我们可以访问的端点了。一种非常常见的做法是使用某种健康检查端点来验证应用程序是否正在运行。为了创建这个端点,我们需要创建一个控制器(处理请求的函数),它将被注册到一个蓝图(路径下的控制器集合),而蓝图又会被注册到我们的应用对象中。
首先,让我们弄清楚这段新代码应该放在哪里。我喜欢把我创建的所有蓝图都放在主模块下一个名为“blueprints”的模块中。这个蓝图将用于管理根路径(“/”),所以我会把它放在“blueprints”模块下一个名为“root.py”的文件中。
接下来,我们应该在 tests 目录中创建“tests/test_blueprints/test_root.py”,以此来镜像此目录结构。加上相应的__init__.py
文件,整个项目结构现在应该如下所示:
python-rest
|-- README.md
|-- poetry.lock
|-- pyproject.toml
|-- python_rest
| |-- __init__.py
| {% raw %}`-- blueprints
| |-- __init__.py
| `-- root.py
`-- tests
|-- conftest.py
`-- test_blueprints
|-- __init__.py
`-- test_root.py
```
Let's write the test for the endpoint we're about to create. We can use the client fixture provided by pytest-flask to execute the request appropriately. We'll want the health check to return a status of 200 (OK) with some JSON body. Here's the test:
```python
# tests/test_blueprints/test_root.py
from http import HTTPStatus
def test_health(client):
response = client.get('/health/')
assert response.status_code == HTTPStatus.OK, 'Health check failed'
assert response.json == {'message': 'Healthy'}, 'Improper response'
```
Note that those assert statements are the things that will cause the test to pass or fail when run. The string after the comma after the assert is the message that will be displayed if the check fails. Run the test either by clicking the little green arrow next to the function definition in PyCharm, or by running `poetry run pytest` on the command line. Either way, you should see it fail.
Now let's create the root blueprint and the actual endpoint.
```python
# python_rest/blueprints/root.py
from flask import Blueprint
# Declare the blueprint with whatever name you want to give it
root_blueprint = Blueprint('root', __name__)
# This is how you register a controller, it accepts OPTIONS and GET methods by default
@root_blueprint.route('/health/')
def health():
return {'message': 'Healthy'} # This will return as JSON by default with a 200 status code
```
Now we need a way to register this blueprint to the app so we can actually reach the controller. They way to do this is with a function called at app creation which registers the blueprint to the app. The convention is to name this function "init_app", and I like to have one "init_app" function which registers all blueprints at once. The logical thing is to put this function in the `__init__.py` of the blueprints module.
```python
# python_rest/blueprints/__init__.py
from flask import Flask
def init_app(app: Flask):
from .root import root_blueprint
app.register_blueprint(root_blueprint)
```
Now you just need to call this function from your create_app factory function.
```python
# python_rest/__init__.py
from flask import Flask
def create_app() -> Flask:
"""
Creates an application instance to run
:return: A Flask object
"""
app = Flask(__name__)
from . import blueprints
blueprints.init_app(app)
return app
```
That's it! If you run the test again it should now pass! You can also run the flask app, then navigate to `localhost:5000/health/` in your browser to see your JSON response.