chapter7 - 大型程序的結構
在程序體積變得越來越大的時候,如果還是在單一腳本中編寫,就會顯得很不方便。於是需要對程序進行結構上的拆分。Flask並不強制要求使用特定的組織方式,開發者可以自行決定。
7.1 項目結構
|-flasky
|-app/|-__init__.py|-email.py|-models.py
|-templates/|-static/|-main/|-__init__.py|-errors.py|-forms.py|-views.py|-migrations/|-tests/|-__init__.py
|-test*.py|-venv|-requirements.txt|-config.py|-manage.py我們要關心的是以下3個文件夾:- Flask程序保存在名為app的包中
- migrations文件夾包含資料庫遷移腳本
- tests包裡面是單元測試腳本
還有一些新文件:
- requirements.txt 該文件列出了所有信賴包,方便在其它電腦中生成同樣的需求環境
- config.py 配置文件
- manage.py 用於啟動程序以及其它的程序任務
7.2 配置選項
本例中設置了開發、測試、部署這三種不同環境下的配置。並設置了一個默認的配置:開發環境
config.py 配置文件
import osbasedir = os.path.abspath(os.path.dirname(__file__))class Config: SECRET_KEY = os.environ.get(SECRET_KEY) or your string SQLALCHEMY_COMMIT_ON_TEARDOWN = True FLASKY_MAIL_SUBJECT_PREFIX = [Flasky] FLASKY_MAIL_SENDER = youremail@example.com FLASKY_ADMIN = os.environ.get(FLASKY_ADMIN) @staticmethod def init_app(app): passclass DevelopmentConfig(Config): DEBUG = True MAIL_SERVER = smtp.sina.com AMIL_PORT = 25 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}
程序為不同的環境選擇了不同的資料庫。
7.3 程序包
程序包即app文件夾,保存了程序的所有代碼、模板、靜態文件。
7.3.1 使用程序工廠函數
程序的工廠函數為修改程序實例的配置提供了可能。在單一文件開發中,程序配置是寫好的,在運行時程序實例已經創建,無法再修改配置。而使用工廠函數,則使得可以為開發、測試、部署等不同需要使用不同的配置以運行程序。
app/___init___.py 程序包的構造文件
from flask import Flask, render_templatefrom 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) return app
7.3.2 使用藍本(Blueprint)
對於新手而言,藍本這東西顯得有些難以理解。藍本主要用於定義路由。 在單腳本程序中,程序實例存在於全局作用域中,路由可以直接使用app.route修飾器定義,但現在程序在運行時才創建,就只有在調用了工廠函數中的create_app()後才能使用app.route修飾器,但這時再定義路由就太是了。同樣的,由於錯誤頁面使用app.errorhandler修飾器定義,因此錯誤頁面的定義也會遇到問題。藍圖就可以解決這些問題。
藍本中的路由需要在藍本註冊到程序上後,路由才能真正成為程序的一部分以供執行。 本書中,在程序中創建了一個包main用於保存藍本,可以理解為,這個藍本的名字就是main
app/main/___init___.py 創建藍本
from flask import Blueprintmain = Blueprint(main, __name__)from . import views, errors
實例化一個Blueprint類對象可以創建藍本,有兩個必須指定的參數:藍本的名字和藍本所在的包或模塊。通常第二個參數使用__name__變數即可。 程序的路由保存在app/main/views.py模塊中,錯誤處理程序保存在app/main/errors.py中。需要導入這兩個模塊以與藍本關聯。但些處在末尾導入,是為了避免循環導入,因為在views.py和errors.py中還要導入藍本main
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_templatefrom . import main@main.app_errorhandler(404)def page_not_found(e): return render_template(404.htm), 404@main.app_errorhandler(500)def internal_server_error(e): return render_template(500.html), 500
app/main/views.py 藍本中定義的程序路由
from datetime import datetimefrom flask import render_template, session, redirect, url_forfrom . import mainfrom .forms import NameFormfrom .. import dbfrom ..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會為藍本中的全部端點加上一個命名空間,以方便在不同的藍本中使用相同的端點名定義視圖函數而不會產生衝突。命名空間就是藍本的名字。因此在藍本中,視圖函數index()註冊的端點名是main.index,其URL用url_for(main.index)獲取。
此外,在藍本中使用url_for()函數可以省略藍本名,即如以上代碼中,直接用url_for(.index)代替。但這種寫法僅限於命名空間是當前請求所在的藍本。如果是跨藍本的重定向,則必須使用帶有命名空間的端點名
7.3.3 其它程序文件
書中有簡單提到但沒有給出代碼的幾個文件,也是需要讀者自行修改添加的:
app/models.py 資料庫模型文件
from . import db# 定義Role和User模型# 也就是定義這兩個表class Role(db.Model): __tablename__ = roles id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), unique=True) users = db.relationship(User, backref=role, lazy=dynamic) def __repr__(self): return <Role %r> % self.nameclass User(db.Model): __tablename__ = users id = db.Column(db.Integer, primary_key = True) username = db.Column(db.String(64), unique=True, index=True) role_id = db.Column(db.Integer, db.ForeignKey(roles.id)) def __repr__(self): return <User %r> % self.username
app/email.py 電子郵件支持函數文件
from threading import Threadfrom flask import current_app, render_templatefrom flask_mail import Messagefrom . import mail# 非同步發送電子郵件的函數def send_async_email(app, msg): with app.app_context(): mail.send(msg)def send_email(to, subject, template, **kwargs): msg = Message(app.config[FLASKY_MAIL_SUBJECT_PREFIX] + subject, sender=app.config[FLASKY_MAIL_SENDER], recipients=[to]) msg.body = render_template(template + .txt, **kwargs) msg.html = render_template(template + .html, **kwargs) thr = Thread(target=send_async_email, args=[app, msg]) thr.start() return thr
app/main/forms.py 表單對象文件
from flask_wtf import Formfrom wtforms import StringField, SubmitFieldfrom wtforms.validators import Required class NameForm(Form): name = StringField(What is your name?, validators=[Required()]) submit = SubmitField(Submit)
其它的模板文件,也要放進template文件夾中:
template/
├── 404.html├── 500.html
├── base.html├── index.html├── mail│ ├── new_user.html│ └── new_user.txt└── user.html更關於藍本的使用信息,可以參考
—— 使用藍圖的模塊化應用-Flask文檔
—— flask 藍圖-網名還沒想好-博客園
7.4 啟動腳本
manage.py 啟動腳本
#!/usr/bin/env pythonimport osfrom app import create_app, dbfrom app.models import User, Rolefrom flask_script import Manager, Shellfrom flask_migrate improt 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)if __name__ == __main__: manager.run()
應該知道的是,整個程序,到這裡的app = create_app()一行才創建程序,傳入一個環境變數作為配置名或使用默認配置。
7.5 需求文件
程序中應當包含一個requirements.txt文件,用於記錄所有依賴包及其精確的版本號,以使程序可以更好地平移到其它系統中。這個需求文件可以使用pip命令生成:
(venv) $ pip freeze > requirements.txt
在安裝(使用)了新的包或升級包之後,最好要更新這個文件。這個文件在我運行這個程序時內容如下:
alembic==0.8.8blinker==1.4click==6.6decorator==4.0.10dominate==2.2.1Flask==0.11.1Flask-Bootstrap==3.3.7.0Flask-Mail==0.9.1Flask-Migrate==2.0.0Flask-Moment==0.5.1Flask-Script==2.0.5Flask-SQLAlchemy==2.1Flask-WTF==0.12ipython==5.1.0ipython-genutils==0.1.0itsdangerous==0.24Jinja2==2.8Mako==1.0.4MarkupSafe==0.23pexpect==4.2.1pickleshare==0.7.4prompt-toolkit==1.0.7ptyprocess==0.5.1Pygments==2.1.3python-editor==1.0.1simplegeneric==0.8.1six==1.10.0SQLAlchemy==1.0.15traitlets==4.3.0visitor==0.1.3wcwidth_==0.1.7Werkzeug==0.11.11WTForms==2.1
當要在新的環境下安裝這些包以產生完全相同的虛擬環境時,可以使用以下命令:
(venv) $ pip install -r requirements.txt
7.6 單元測試
tests/test_basics.py
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])
這個測試使用了標準庫中的unittest。setUp()和tearDown()方法分別在各測試前後運行,名字以test_開頭的函數都作為測試進行。
setUp()方法創建一個測試環境,類似運行中的程序。然後創建程序、激活上下文、創建資料庫,這些上下文和資料庫都會在tearDown()方法中刪除。
第一個測試確保程序實例存在,第二個測試確保程序在測試配置中運行
為了運行單元測試,還需要在manage.py腳本中添加一一個自定義命令:
@manager.commanddef test(): """Run the unit tests.""" import unittest tests = unittest.TestLoader().discover(tests) unitttest.TextTestRunner(verbosity=2).run(tests)
運行單元測試:
(venv) $ python manage.py test
更多關於單元測試模塊的使用,可參考Python3 – unittest官方文檔
7.7 創建資料庫
這裡使用Flask-Migrate跟蹤資料庫遷移:
(venv) $ python manage.py db upgrade
推薦閱讀:
※Flask實踐:猜數字
※如圖是什麼錯誤。。。?
※Werkzeug(Flask)之Local、LocalStack和LocalProxy
※flask並發是如何區分用戶的?