把一個小應用程序的代碼都放在一起會很方便,但是不利于擴展,尤其當項目開始變大時在一個文件中工作就會帶來一些問題。不像其他框架,Flask應用程序沒有特定的組織方式,選擇權完全交給了使用者。本章會介紹一種按照包和模塊來組織大型應用程序的方法,并會在本書剩余的章節都采用這種結構。
項目結構
Example 7-1展示了一個Flask應用程序的布局:
Example 7-1. Basic multiple-file Flask application structure
頂級有四個文件夾,分別是:
- Flask應用程序所在的包通常被命名為app
- 數據庫遷移相關的腳本被放置在migration
- 單元測試寫在在tests
- venv包含了Python的虛擬環境
同樣,增加了一些新的文件:
- requirements.txt 列舉了依賴的包方便在新的電腦中對虛擬環境快速進行配置
- config.py 存儲了應用程序的配置參數
- manage.py 用于啟動應用程序以及做一些其他任務
為了更好地理解這樣的布局方式,后面的部分會介紹如何從一個只有hello.py的程序擴展到上圖所示的結構。
配置選項
應用程序需要一些配置,比如對于開發、測試、產品會需要不同的數據庫那樣才不會相互影響。和單文件版本中在hello.py中寫所有的配置不同,我們能夠用類層級的方式來組織配置:
Example 7-2. config.py: Application configuration
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被賦予了不同的值,這樣運行在三套不同配置下的應用程序都使用了不同的數據庫。
配置類定義了類方法init_app(),它接受一個應用程序實例作為參數。這樣特殊的配置就能夠執行了(注
:原文是 Here configuration-specific initialization can performed 沒明白init_app()這個方法跟特殊配置起不起作用有什么關系,至少在本章中的例子中沒有體現出來)。當前,僅Config類實現了一個空的init_app()方法。
在配置文件的底部不同的配置被添加到了字典中,并且開發環境的配置被設置成了默認的。
應用程序包App
應用程序包app是所有應用程序代碼、模板、靜態資源文件存放的地方,當然你也可以根據項目需求取別的名字。模板和資源文件的文件夾都被放入了app中,數據庫對應的models和郵件支持功能模塊則分別對應 app/models.py 和 app/email.py。
使用工廠方法來構建應用示例
在單文件版本中創建應用程序實例很方便,但是通常會有缺陷。因為應用程序實例在全局作用于下被創建,而實例被創建后是沒辦法動態修改配置的。 尤其在做單元測試時,因為要跑不同的數據庫,所以我們要應用不同的配置。
解決辦法就是通過使用工廠方法延遲應用程序實例的創建,這樣不僅僅是延遲了創建時間還讓腳本有創建多個應用程序實例的能力,這對于測試尤其有用。Example 7-3中在app包中定義了了這樣一個工廠方法。
app包導入了Flask目前會用到的擴展,但因為應用程序實例還沒有被構建出來,它們都還沒有被正確初始化。create_app()這個工廠方法接受一個配置名稱作為參數,通過使用Flask提供的app.config的from_object()方法,我們就能從config.py中導入所需要的配置。一旦應用程序實例被創建出來,擴展就能夠通過調用init_app()來完成初始化。
Example 7-3. app/__init__.py: Application package constructor
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
工廠方法返回的應用程序實例還不完整,因它們沒有包含路由和錯誤處理功能,下一節會介紹如何解決這個問題。
使用Blueprint來實現應用程實例的功能
用工廠方法構建應用程序實例會給路由設置帶來一些麻煩。單腳本應用中,應用程序實例是全局的,路由能簡單地用app.route decorator來定義。但是現在應用程序實例是運行時創建的,app.route decorator只在在create_app()以后才存在,除此之外app.errorhandler decorator也有同樣的問題。
Flask提供的解決方案是使用blueprints來解決這個問題。blueprints跟application類似,也能定義路由。不同之處是它的路由都處于休眠狀態,直到它被注冊到應用程序實例后路由才是它的一部分。
blueprint在全局作用域下使用,因此我們完全可以像在單文件中那樣使用路由。當然你既能通過單文件也能通過更加組織良好的方式。為了達到最大程度的便利性,一個子包結構被創建用于管理blueprint。Example 7-4展示了在這個main包中如何創建blueprint:
Example 7-4. app/main/init.py: Blueprint creation
from flask import Blueprint
main = Blueprint('main', __name__)
from . import views, errors
blueprints被創建為Blueprint的實例對象,構造函數有兩個參數:blueprint的名字和它所在的模塊或者包,在這個應用程序中,Python的 __name__ 變量就是第二個參數所需要的值。
應用程序的路由被存儲在app/main/views.py模塊中, 錯誤處理則在app/main/errors.py。導入這些模塊以后,路由和錯誤處理就和blueprint關聯起來了。
有一點要注意路由和錯誤處理模塊是在app/__init__.py的底部被導入的,因為views.py 和 errors.py要導入main blueprint,所以為了避免循環依賴我們要等到main被創建出來才能夠導入路由和錯誤處理。
如Example 7-5所示,blueprint在create_app()方法內被注冊到應用程序實例中:
Example 7-5. app/__init__.py: Blueprint registration
def create_app(config_name):
# ...
from main
import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
Example 7-6展現了錯誤處理:
Example 7-6. app/main/errors.py: Blueprint with error handlers
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
在blueprint使用錯誤處理,如果使用@app.errorhandler,只有由blueprint定義的路由中導致的錯誤才會觸發對應的handler,如果想要錯誤處理對整個應用程序可用,我們需要使用@main.app_errorhandler。
Example 7-7展示了使用blueprint方式的路由:
Example 7-7. app/main/views.py: Blueprint with application routes
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())
在blueprint中使用視圖方法跟之前有兩個不同的地方。第一個是route是來自blueprint,即-使用@main.route,第二個是url_for()方法的使用。在前面介紹過url_for()的參數默認是視圖方法的名稱,比如在單腳本應用中index()這個視圖方法的URL能夠通過url_for('index')獲取到。
在blueprints中區別在于所有的作用域都來自于blueprint(作用域就是blueprint的名稱,即Blueprint構造函數的第一個參數),因此index()視圖方法需要通過main.index來獲取到URL,即url_for('main.index')。url_for()方法同樣支持參數的更短形式,通過將blueprint名字省略,我們可以簡寫為url_for('.index')。當然如果跨越不同的blueprints,blueprint的名字還是要加上的。
為了完成應用程序,我們還需要在app/main/forms.py模塊導入form相關的一些對象。
啟動腳本
在頂層文件夾下的manage.py是用來啟動application的:
Example 7-8. manage.py: Launch script
#!/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變量,如果該變量沒有定義則使用默認值。然后Flask-Script, Flask-Migrate等擴展的實例都被初始化。為了方便在Unix-based系統下運行我們增加了第一行。
Requirements文件
Applications應該包含一個requirements.txt,它記錄了有著準確版本號的所有包依賴,這對以在其他電腦上初始化項目環境很重要。通過如下命令能夠自動生成一個項目用到的包的requirement.txt文件:
(venv) $ pip freeze >requirements.txt
在一個新的環境中,你如果要復制虛擬環境中的安裝包,只需要執行如下命令即可:
(venv) $ pip install -r requirements.txt
該書示例中的requirement.txt中的包可能有一些已經過時了,你可以選擇更加新版的包。如果因此遇到了什么問題,只要回退到老版本即可,因為老版本的都是通過了測試和應用程序兼容的。
單元測試
到目前應用程序還很小,幾乎還沒有什么要測試的,但如Example 7-9所示我們先來寫一個小的測試例子:
Example 7-9. tests/test_basics.py: Unit tests
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包中的典型的單元測試的寫法來構建的,setUp() 和 tearDown() 方法在每個測試方法執行前后都會運行,任何以test_ 開頭的方法都會被當做測試方法來執行。關于使用Python包來做單元測試的更多信息可以查看official documentation。
setUp()方法創建了測試所需的環境, 他首先創建了應用程序實例用作測試的山下文環境,這樣就能確保測試拿到current_app, 然后新建了一個全新的數據庫。數據庫和應用程序實例最后都會在tearDown() 方法被銷毀。
第一個測試確保了應用程序實例是存在的,第二個測試應用程序實例在測試配置下運行。為了確保測試文件夾有正確的包結構,我們需要添加一個tests/__init__.py文件(注
:涉及Python包相關知識),這樣單元測試包就能掃描所有在測試文件夾中的模塊了。
你可以把代碼checkout到7a的歷史節點,并且執行 pip install -r requirements.txt
來確保你安裝了所需要的包。為了運行測試用例,還需要添加命令到manage.py中:
Example 7-10. manage.py: Unit test launcher command
@manager.command
def test():
"""Run the unit tests."""
import unittest
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
manager.command decorator所對應的方法名字就是命令的名字,并且方法的文檔信息會被顯示在help中,test() 的實現調用了unittest package包的test runner。如下是運行過程:
(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
數據庫設置
重構后的應用程序使用了跟單文件本版本中完全不同的數據庫。數據庫URL會首先從環境變量中獲取,然后把默認的SQLite數據庫作為備選,在三個配置環境下數據庫的名字是不同的。
不論數據庫的URL是什么,只要是轉換到一個新的數據庫數,據庫表一定要被重新創建(注
:原文Regardless of the source of the database URL, the database tables must be created for the new database 不完全理解)。使用Flask-Migrate進行遷移管理的過程中,數據庫表能夠通過如下命令被新建或者upgrade:
(venv) $ python manage.py db upgrade
第一部分的內容到此算是結束了,我們已經基本介紹了使用Flask來創建應用程序的所有知識,但是你也許仍舊不確定如何將他們捏合在一起。第二部分的目標就是幫助你完成一個應用程序的開發。