flask成长记(六)
来源:互联网 发布:毛昆仑 常凯申 知乎 编辑:程序博客网 时间:2024/05/14 17:22
大型程序的结构
尽管在单一脚本中编写小型 Web 程序很方便,但这种方法并不能广泛使用。程序变复杂
后,使用单个大型源码文件会导致很多问题。
不同于大多数其他的 Web 框架, Flask 并不强制要求大型项目使用特定的组织方式,程序
结构的组织方式完全由开发者决定。 在本章,我们将介绍一种使用包和模块组织大型程序
的方式。后续示例都将采用这种结构。
项目结构
Flask 程序的基本结构如下:
多文件 Flask 程序的基本结构|-flasky |-app/ |-templates/ |-static/ |-main/ |-__init__.py |-errors.py |-forms.py |-views.py |-__init__.py |-email.py |-models.py |-migrations/ |-tests/ |-__init__.py |-test*.py|-venv/|-requirements.txt|-config.py|-manage.py
这种结构有 4 个顶级文件夹:
- Flask 程序一般都保存在名为 app 的包中;
- 和之前一样, migrations 文件夹包含数据库迁移脚本;
- 单元测试编写在 tests 包中;
- 和之前一样, venv 文件夹包含 Python 虚拟环境。
同时还创建了一些新文件:
- requirements.txt 列出了所有依赖包,便于在其他电脑中重新生成相同的虚拟环境;
- config.py 存储配置;
- manage.py 用于启动程序以及其他的程序任务。
我们不再使用 hello.py 中简单的字典状结构配置,而使用层次结构的配置类。 config.py 文
件的内容如下:
config.py是根目录下的文件,是程序的配置:
import osbasedir = os.path.abspath(os.path.dirname(__file__))class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string' SQLALCHEMY_COMMIT_ON_TEARDOWN = True MAIL_SERVER = 'smtp.googlemail.com' MAIL_PORT = 587 MAIL_USE_TLS = True MAIL_USERNAME = os.environ.get('MAIL_USERNAME') MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]' FLASKY_MAIL_SENDER = 'Flasky Admin <flasky@example.com>' FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN') @staticmethod def init_app(app): passclass DevelopmentConfig(Config): DEBUG = True SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \ 'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')class TestingConfig(Config): TESTING = True SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \ 'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')class ProductionConfig(Config): SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ 'sqlite:///' + os.path.join(basedir, 'data.sqlite')config = { 'development': DevelopmentConfig, 'testing': TestingConfig, 'production': ProductionConfig, 'default': DevelopmentConfig}基类 Config 中包含通用配置,子类分别定义专用的配置。如果需要,你还可添加其他配置类。
为了让配置方式更灵活且更安全,某些配置可以从环境变量中导入。例如, SECRET_KEY 的值,
这是个敏感信息,可以在环境中设定,但系统也提供了一个默认值,以防环境中没有定义。
在 3 个子类中, SQLALCHEMY_DATABASE_URI 变量都被指定了不同的值。这样程序就可在不同
的配置环境中运行,每个环境都使用不同的数据库。
配置类可以定义 init_app() 类方法,其参数是程序实例。在这个方法中,可以执行对当前
环境的配置初始化。现在,基类 Config 中的 init_app() 方法为空。
在这个配置脚本末尾, config 字典中注册了不同的配置环境,而且还注册了一个默认配置
(本例的开发环境)。
程序包
程序包用来保存程序的所有代码、模板和静态文件。我们可以把这个包直接称为 app(应
用),如果有需求, 也可使用一个程序专用名字。 templates 和 static 文件夹是程序包的一部
分,因此这两个文件夹被移到了 app 中。 数据库模型和电子邮件支持函数也被移到了这个
包中,分别保存为 app/models.py 和 app/email.py。
使用程序工厂函数
在单个文件中开发程序很方便,但却有个很大的缺点,因为程序在全局作用域中创建,所
以无法动态修改配置。 运行脚本时,程序实例已经创建,再修改配置为时已晚。这一点对
单元测试尤其重要,因为有时为了提高测试覆盖度,必须在不同的配置环境中运行程序。
这个问题的解决方法是延迟创建程序实例, 把创建过程移到可显式调用的工厂函数中。这
种方法不仅可以给脚本留出配置程序的时间, 还能够创建多个程序实例,这些实例有时在
测试中非常有用。程序的工厂函数在 app 包的构造文件中定义。
构造文件导入了大多数正在使用的 Flask 扩展。由于尚未初始化所需的程序实例,所以没
有初始化扩展, 创建扩展类时没有向构造函数传入参数。 create_app() 函数就是程序的工
厂函数,接受一个参数,是程序使用的配置名。配置类在 config.py 文件中定义,其中保存
的配置可以使用 Flask app.config 配置对象提供的 from_object() 方法直接导入程序。至
于配置对象, 则可以通过名字从 config 字典中选择。程序创建并配置好后,就能初始化
扩展了。在之前创建的扩展对象上调用 init_app() 可以完成初始化过程。
from flask import Flaskfrom flask_bootstrap import Bootstrapfrom flask_mail import Mailfrom flask_moment import Momentfrom flask_sqlalchemy import SQLAlchemyfrom config import configbootstrap = Bootstrap()mail = Mail()moment = Moment()db = SQLAlchemy()def create_app(config_name): app = Flask(__name__) app.config.from_object(config[config_name]) config[config_name].init_app(app) bootstrap.init_app(app) mail.init_app(app) moment.init_app(app) db.init_app(app) from .main import main as main_blueprint app.register_blueprint(main_blueprint) return app
在蓝本中实现程序功能
转换成程序工厂函数的操作让定义路由变复杂了。在单脚本程序中,程序实例存在于全
局作用域中, 路由可以直接使用 app.route 修饰器定义。但现在程序在运行时创建,只
有调用 create_app() 之后才能使用 app.route 修饰器,这时定义路由就太晚了。和路由
一样,自定义的错误页面处理程序也面临相同的困难,因为错误页面处理程序使用 app.
errorhandler 修饰器定义。
幸好 Flask 使用蓝本提供了更好的解决方法。 蓝本和程序类似,也可以定义路由。不同的
是,在蓝本中定义的路由处于休眠状态, 直到蓝本注册到程序上后,路由才真正成为程序
的一部分。使用位于全局作用域中的蓝本时,定义路由的方法几乎和单脚本程序一样。
和程序一样, 蓝本可以在单个文件中定义,也可使用更结构化的方式在包中的多个模块中
创建。为了获得最大的灵活性,程序包中创建了一个子包,用于保存蓝本。下面是这
个子包的构造文件,蓝本就创建于此。
注意下面这个文件是在main文件夹下面:
app/main/__init__.py: 创建蓝本from flask import Blueprintmain = Blueprint('main', __name__)from . import views, errors.大概就是本目录下的文件吧。连路径都没给,那就是本文件夹下喽。通过实例化一个 Blueprint 类对象可以创建蓝本。这个构造函数有两个必须指定的参数:蓝本的名字和蓝本所在的包或模块。 和程序一样,大多数情况下第二个参数使用 Python 的__name__ 变量即可。程序的路由保存在包里的 app/main/views.py 模块中,而错误处理程序保存在 app/main/errors.py 模块中。 导入这两个模块就能把路由和错误处理程序与蓝本关联起来。注意,这些模块在 app/main/__init__.py 脚本的末尾导入,这是为了避免循环导入依赖,因为在views.py 和 errors.py 中还要导入蓝本 main。蓝本在工厂函数 create_app() 中注册到程序上。app/_init_.py: 注册蓝本def create_app(config_name): # ... from .main import main as main_blueprint app.register_blueprint(main_blueprint) return app
下面显示了错误处理程序:
from flask import render_templatefrom . import main@main.app_errorhandler(404)def page_not_found(e): return render_template('404.html'), 404@main.app_errorhandler(500)def internal_server_error(e): return render_template('500.html'), 500在蓝本中编写错误处理程序稍有不同,如果使用 errorhandler 修饰器,那么只有蓝本中的错误才能触发处理程序。要想注册程序全局的错误处理程序,必须使用 app_errorhandler。
在蓝本中定义的程序路由如下:
from flask import render_template, session, redirect, url_for, current_appfrom .. import dbfrom ..models import Userfrom ..email import send_emailfrom . import mainfrom .forms import NameForm@main.route('/', methods=['GET', 'POST'])def index(): form = NameForm() if form.validate_on_submit(): user = User.query.filter_by(username=form.name.data).first() if user is None: user = User(username=form.name.data) db.session.add(user) session['known'] = False if current_app.config['FLASKY_ADMIN']: send_email(current_app.config['FLASKY_ADMIN'], 'New User', 'mail/new_user', user=user) else: session['known'] = True session['name'] = form.name.data return redirect(url_for('.index')) return render_template('index.html', form=form, name=session.get('name'), known=session.get('known', False))
在蓝本中编写视图函数主要有两点不同:第一,和前面的错误处理程序一样,路由修饰器
由蓝本提供; 第二, url_for() 函数的用法不同。你可能还记得, url_for() 函数的第一
个参数是路由的端点名, 在程序的路由中,默认为视图函数的名字。例如,在单脚本程序
中, index() 视图函数的 URL 可使用 url_for(‘index’) 获取。
在蓝本中就不一样了, Flask 会为蓝本中的全部端点加上一个命名空间,这样就可以在不
同的蓝本中使用相同的端点名定义视图函数, 而不会产生冲突。命名空间就是蓝本的名字
( Blueprint 构造函数的第一个参数),所以视图函数 index() 注册的端点名是 main.index,
其 URL 使用 url_for(‘main.index’) 获取。
url_for() 函数还支持一种简写的端点形式,在蓝本中可以省略蓝本名,例如 url_for(‘.
index’)。在这种写法中,命名空间是当前请求所在的蓝本。这意味着同一蓝本中的重定向
可以使用简写形式,但跨蓝本的重定向必须使用带有命名空间的端点名。
为了完全修改程序的页面,表单对象也要移到蓝本中,保存于 app/main/forms.py 模块。
启动脚本
顶级文件夹中的 manage.py 文件用于启动程序。脚本内容如下:
#!/usr/bin/env pythonimport osfrom app import create_app, dbfrom app.models import User, Rolefrom flask_script import Manager, Shellfrom flask_migrate import Migrate, MigrateCommandapp = create_app(os.getenv('FLASK_CONFIG') or 'default')manager = Manager(app)migrate = Migrate(app, db)def make_shell_context(): return dict(app=app, db=db, User=User, Role=Role)manager.add_command("shell", Shell(make_context=make_shell_context))manager.add_command('db', MigrateCommand)@manager.commanddef test(): """Run the unit tests.""" import unittest tests = unittest.TestLoader().discover('tests') unittest.TextTestRunner(verbosity=2).run(tests)if __name__ == '__main__': manager.run()如果是在顶级文件夹下的话,我估计这个是程序的入口吧也算是。
需求文件
程序中必须包含一个 requirements.txt 文件,用于记录所有依赖包及其精确的版本号。如果
要在另一台电脑上重新生成虚拟环境, 这个文件的重要性就体现出来了,例如部署程序时
使用的电脑。 pip 可以使用如下命令自动生成这个文件:
$ pip freeze >requirements.txt
安装或升级包后,最好更新这个文件。
单元测试
import unittestfrom flask import current_appfrom app import create_app, dbclass BasicsTestCase(unittest.TestCase): def setUp(self): self.app = create_app('testing') self.app_context = self.app.app_context() self.app_context.push() db.create_all() def tearDown(self): db.session.remove() db.drop_all() self.app_context.pop() def test_app_exists(self): self.assertFalse(current_app is None) def test_app_is_testing(self): self.assertTrue(current_app.config['TESTING'])
这个测试使用 Python 标准库中的 unittest 包编写。 setUp() 和 tearDown() 方法分别在各
测试前后运行,并且名字以 test_ 开头的函数都作为测试执行。
如果你想进一步了解如何使用 Python 的 unittest 包编写测试,请阅读官方文
档( https://docs.python.org/2/library/unittest.html)。
setUp() 方法尝试创建一个测试环境,类似于运行中的程序。首先,使用测试配置创建程
序,然后激活上下文。 这一步的作用是确保能在测试中使用 current_app,像普通请求一
样。然后创建一个全新的数据库, 以备不时之需。数据库和程序上下文在 tearDown() 方法
中删除。
第一个测试确保程序实例存在。 第二个测试确保程序在测试配置中运行。若想把 tests 文
件夹作为包使用, 需要添加 tests/init.py 文件,不过这个文件可以为空,因为 unittest
包会扫描所有模块并查找测试。
为了运行单元测试,你可以在 manage.py 脚本中添加一个自定义命令。
manage.py:启动单元测试的命令@manager.commanddef test(): """Run the unit tests.""" import unittest tests = unittest.TestLoader().discover('tests') unittest.TextTestRunner(verbosity=2).run(tests)
manager.command 修饰器让自定义命令变得简单。修饰函数名就是命令名,函数的文档字符
串会显示在帮助消息中。 test() 函数的定义体中调用了 unittest 包提供的测试运行函数。
单元测试可使用下面的命令运行:
(venv) $ python manage.py test
test_app_exists (test_basics.BasicsTestCase) … ok
test_app_is_testing (test_basics.BasicsTestCase) … ok
.———————————————————————-
Ran 2 tests in 0.001s
- flask成长记(六)
- flask成长记(一)
- flask成长记(二)
- flask成长记(三)
- flask成长记(四)
- flask成长记(五)
- flask成长记(一)增强版
- flask成长记(二)增强版
- 安卓成长记(六)
- 嵌入式linux菜鸟成长记(六)
- 我的成长(六)
- Python Flask Web框架(六)
- 小菜编程成长记(六 关于Flex的争论)
- 小菜编程成长记(六 工厂不好用了?)
- 小菜编程成长记(六 工厂不好用了?)
- 草根年代--六爷成长记
- Flask源码阅读(六)——Flash消息
- (转帖)小菜编程成长记(六 工厂不好用了?)
- Trie树标准模版
- 1104. Sum of Number Segments (20)
- 学习方法摘录
- 深入了解View的绘制流程
- Understanding LSTM Networks
- flask成长记(六)
- UWP开发技巧:实现SMB协议操作文件服务器文件
- LeetCode_292. Nim Game
- 编写函数void change(char *a,char *b,char*c)。
- 2017.1.30 Java有感3.2
- 1035. Password (20)
- 16.进程同步与死锁——进程同步与信号量
- 并发 并行
- 1008. Elevator (20)