Flask は軽量なことで知られるフレームワークですが、単体テストや大規模な Web アプリケーションのデータベース移行など便利な機能も多数提供しています。 ここでは、Python の Flask フレームワークを使用して大規模な Web アプリケーションを構築する方法を見ていきます。構造例:
小規模な Web アプリケーションには単一のスクリプトが便利ですが、このアプローチは拡張性が高くありません。アプリケーションが複雑になるにつれて、単一の大きなソース ファイル内の処理に問題が生じる可能性があります。
他のほとんどの Web フレームワークとは異なり、Flask には大規模なプロジェクトを編成する特別な方法がありません。アプリケーションの構造は完全に開発者自身に任されています。この章では、大規模なアプリケーションのパッケージとモジュールを編成および管理するための可能な方法を示します。この構造は、本書の残りの例で使用されます。
1. プロジェクト構造
例 基本的なマルチファイル 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 ディレクトリには、前述したものと同じデータベース移行スクリプトが含まれています。
単体テストは test ディレクトリに配置されます
venv ディレクトリには、前述したものと同じ Python 仮想環境が含まれています。
新しいファイルもいくつかあります:
requirements.txt にはいくつかの依存関係パッケージがリストされているため、異なるコンピューターに同じ仮想環境を簡単に展開できます。
config.py には、いくつかの構成設定が保存されます。
manage.py は、アプリケーションやその他のアプリケーション タスクを起動するために使用されます。
この構造を完全に理解できるように、この構造に準拠するように hello.py アプリケーションを変更するプロセス全体を以下に説明します。
2. 構成オプション
アプリケーションには通常、いくつかの構成設定が必要です。最も良い例は、開発プロセス中に、相互に干渉しないように、異なるデータベース、テスト環境、実稼働環境を使用する必要があることです。
hello.py では、単純な辞書のような構造構成の代わりに、構成クラスの階層を使用できます。 config.py ファイルを以下に示します。
config.py: アプリケーション構成
import os basedir = 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 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): pass class DevelopmentConfig(Config): DEBUG = 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') 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 は機密性があるため、環境で設定できますが、環境で定義されていない場合はデフォルト値を指定する必要があります。
SQLALCHEMY_DATABASE_URI 変数には、3 つの構成で異なる値を割り当てることができます。こうすることで、アプリケーションを異なる構成で実行し、それぞれが異なるデータベースを使用できます。
設定クラスは、アプリケーション インスタンスをパラメータとして受け取る init_app() 静的メソッドを定義できます。ここでは構成固有の初期化が可能です。ここで、Config 基本クラスは空の init_app() メソッドを実装します。
設定スクリプトの下部で、これらのさまざまな設定が設定辞書に登録されます。いずれかの構成 (開発構成) をデフォルト構成として登録します。
3. アプリケーション パッケージ
アプリケーション パッケージには、すべてのアプリケーション コード、テンプレート、および静的ファイルが配置されます。これは単に「アプリ」と呼ばれますが、必要に応じてアプリケーション固有の名前を付けることもできます。テンプレートと静的ディレクトリはアプリケーションの一部であるため、これら 2 つのディレクトリをアプリに配置する必要があります。データベース モデルと電子メールのサポートもこのパッケージに組み込まれており、それぞれ app/models.py および app/email.py の形式の独自のモジュールに組み込まれています。
3.1. アプリケーションファクトリーを使用する
単一ファイルでアプリケーションを作成することは非常に便利ですが、大きな欠点があります。アプリケーションはグローバル スコープで作成されるため、アプリケーション構成の変更に動的に適応する方法はありません。スクリプトが実行されるまでに、アプリケーション インスタンスはすでに作成されているため、構成を変更するには手遅れです。より良いテストカバレッジを得るために、異なる構成でアプリケーションを実行する必要がある場合があるため、これは単体テストの場合に特に重要です。
この問題の解決策は、アプリケーションをファクトリ関数に入れて作成を延期し、スクリプトから明示的に呼び出せるようにすることです。
これにより、スクリプトに構成をセットアップするための十分な時間が与えられるだけでなく、アプリケーションの複数のインスタンスを作成するためにも使用できます。これは、テスト中に非常に役立ちます。アプリ・パッケージのコンストラクターで定義されたアプリケーション・ファクトリ関数を例7-3に示します。
このコンストラクターは、現在使用する必要がある拡張機能のほとんどをインポートしますが、拡張機能を初期化するアプリケーション インスタンスがないため、コンストラクターに引数を渡さないと、拡張機能を作成しても初期化できません。 create_app() はアプリケーション ファクトリ関数であり、アプリケーションに使用される構成名を渡す必要があります。構成内の設定は config.py のクラスに保存され、Flask の app.config 構成オブジェクトの from_object() メソッドを使用して直接インポートできます。構成オブジェクトは、オブジェクト名によって構成ディクショナリから選択できます。アプリケーションを作成して構成したら、拡張機能を初期化できます。拡張機能で init_app() を呼び出す前に、初期化作業を作成して完了します。
app/ _init__.py:应用程序包构造函数_
from flask import Flask, render_template from flask.ext.bootstrap import Bootstrap from flask.ext.mail import Mail from flask.ext.moment import Moment from flask.ext.sqlalchemy import SQLAlchemy from config import config bootstrap = 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) # attach routes and custom error pages here return app
工厂函数返回创建的应用程序实例,但是请注意,在当前状态下使用工厂函数创建的应用程序是不完整的,因为它们没有路由和自定义错误页面处理程序。这是下一节的主题。
3.2、在蓝图中实现应用程序的功能
应用程序工厂的转化工作引出了路由的复杂化。在单脚本应用中,应用程序实例是全局的,所以可以很容易地使用app.route装饰器定义路由。但是现在应用程序在运行时创建,app.route装饰器只有在create_app()调用后才开始存在,这就太迟了。就像路由那样,这些通过app.errorhandler装饰器定义的自定义错误页面处理程序也存在同样的问题。
幸运的是Flask使用蓝图来提供一个更好的解决方案。一个蓝图就类似于一个可以定义路由的应用程序。不同的是,和路由相关联的蓝图都在休眠状态,只有当蓝图在应用中被注册后,此时的路由才会成为它的一部分。使用定义在全局作用域下的蓝图,定义应用程序的路由就几乎可以和单脚本应用程序一样简单了。
和应用程序一样,蓝图可以定义在一个文件或一个包中与多个模块一起创建更结构化的方式。为了追求最大的灵活性,可以在应用程序包中创建子包来持有蓝图。下面展示了创建蓝图的构造函数。
app/main/ _init__.py:创建蓝图_
from flask import Blueprint main = Blueprint('main', __name__) from . import views, errors
蓝图是通过实例化Blueprint类对象来创建的。这个类的构造函数接收两个参数:蓝图名和蓝图所在的模块或包的位置。与应用程序一样,在大多数情况下,对于第二个参数值使用Python的__name__变量是正确的。
应用程序的路由都保存在app/main/views.py模块内部,而错误处理程序则保存在app/main/errors.py中。导入这些模块可以使路由、错误处理与蓝图相关联。重要的是要注意,在app/init.py脚本的底部导入模块要避免循环依赖,因为view.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
下面则展示了错误处理。
app/main/errors.py:蓝图的错误处理
from flask import render_template from . 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。
这里展示了被更新在蓝图中的应用程序路由。
app/main/views.py:带有蓝图的应用程序路由
from datetime import datetime from flask import render_template, session, redirect, url_for from . import main from .forms import NameForm from .. import db from ..models import User @main.route('/', methods=['GET', 'POST']) def index(): form = NameForm() if form.validate_on_submit(): # ... return redirect(url_for('.index')) return render_template('index.html', form=form, name=session.get('name'), known=session.get('known', False), current_time=datetime.utcnow())
在蓝图中写视图函数有两大不同点。第一,正如之前的错误处理一样,路由装饰器来自于蓝图。第二个不同是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模块中的蓝图里面。
4、启动脚本
顶层目录中的manage.py文件用于启动应用。
manage.py:启动脚本
#!/usr/bin/env python import os from app import create_app, db from app.models import User, Role from flask.ext.script import Manager, Shell from flask.ext.migrate import Migrate, MigrateCommand app = 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) if __name__ == '__main__': manager.run()
这个脚本开始于创建应用程序。使用环境变量FLASK_CONFIG,若它已经定义了则从中获取配置;如果没有,则是用默认配置。然后用于Python shell的Flask-Script、Flask-Migrate以及自定义上下文会被初始化。
为了方便,会增加一行执行环境,这样在基于Unix的操作系统上可以通过./manage.py来执行脚本来替代冗长的python manage.py。
5、需求文件
应用程序必须包含requirements.txt文件来记录所有依赖包,包括精确的版本号。这很重要,因为可以在不同的机器上重新生成虚拟环境,例如在生产环境的机器上部署应用程序。这个文件可以通过下面的pip命令自动生成:
(venv) $ pip freeze >requirements.txt
当安装或更新一个包之后最好再更新一下这个文件。以下展示了一个需求文件示例:
Flask==0.10.1 Flask-Bootstrap==3.0.3.1 Flask-Mail==0.9.0 Flask-Migrate==1.1.0 Flask-Moment==0.2.0 Flask-SQLAlchemy==1.0 Flask-Script==0.6.6 Flask-WTF==0.9.4 Jinja2==2.7.1 Mako==0.9.1 MarkupSafe==0.18 SQLAlchemy==0.8.4 WTForms==1.0.5 Werkzeug==0.9.4 alembic==0.6.2 blinker==1.3 itsdangerous==0.23
当你需要完美复制一个虚拟环境的时候,你可以运行以下命令创建一个新的虚拟环境:
(venv) $ pip install -r requirements.txt
当你读到这时,示例requirements.txt文件中的版本号可能已经过时了。如果喜欢你可以尝试用最近发布的包。如果遇到任何问题,你可以随时回退到需求文件中与应用兼容的指定版本。
6、单元测试
这个应用非常小以至于不需要太多的测试,但是作为示例会在示例中展示两个简单的测试定义。
示例:tests/test_basics.py:单元测试
import unittest from flask import current_app from app import create_app, db class 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包来写单元测试的内容,请参阅官方文档。
setUp()方法尝试创建一个测试环境,类似于运行应用程序。首先它创建应用程序配置用于测试并激活上下文。这一步确保测试可以和常规请求一样访问current_app。然后,当需要的时候,可以创建一个供测试使用的全新数据库。数据库和应用程序上下文会在tearDown()方法中被移除。
第一个测试确保应用程序实例存在。第二个测试确保应用程序在测试配置下运行。为了确保tests目录有效,需要在tests目录下增加__init__.py文件,不过该文件可以为空,这样unittest包可以扫描所有模块并定位测试。
建议:如果你有克隆在GitHub上的应用程序,你现在可以运行git checkout 7a来切换到这个版本的应用程序。为了确保你已经安装了所有依赖集,需要运行pip install -r requirements.txt。
为了运行单元测试,可以在manage.py脚本中增加一个自定义的命令。
下面展示如何添加测试命令。
示例:manage.pyt:单元测试启动脚本
@manager.command def 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 OK
7、数据库启动
与单脚本的应用相比,重构后的应用使用不同数据库。
从环境变量中获取的数据库URL作为首选,默认SQLite数据库作为可选。三个配置中的环境变量和SQLite数据库文件名是不一样的。例如,开发配置的URL是从DEV_DATABASE_URL环境变量中获取,如果没有定义则会使用名为data-dev.sqlite的SQLite数据库。
无论数据库URL源的是哪一个,都必须为新的数据库创建数据库表。如果使用了Flask-Migrate来保持迁移跟踪,数据库表可以被创建或更新到最近的版本通过下面的命令:
(venv) $ python manage.py db upgrade
相信与否,已经到了第一部分结束的地方。你现在已经学到了Flask必要的基本要素,但是你不确定如何将这些零散的知识组合在一起形成一个真正的应用程序。第二部分的目的是通过开发一个完整的应用程序来带领你继续前行。
更多Python的Flask框架构建大型Web应用程序的结构相关文章请关注PHP中文网!