工作上開始使用flask,很輕很好用,可以讓使用者高度自定義,然而,這種情形下的缺點就是,很容易寫出義大利麵的程式阿!
所以,以下一步步的分析,如何從義大利麵程式轉換到井然有序的架構!
首先,先看看義大利麵的亂中有序。
首先,我們先一起欣賞一下義大利麵形式的flask主程式hello.py:
邏輯不是這裡的重點,試著將類似的程式碼歸類。
import os
from threading import Thread
from flask import Flask, render_template, session, redirect, url_for
from flask_bootstrap import Bootstrap
from flask_moment import Moment
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_mail import Mail, Message
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
app.config['SQLALCHEMY_DATABASE_URI'] =\
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <flasky@example.com>'
app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')
bootstrap = Bootstrap(app)
moment = Moment(app)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
mail = Mail(app)
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.name
class 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
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
class NameForm(FlaskForm):
name = StringField('What is your name?', validators=[DataRequired()])
submit = SubmitField('Submit')
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
@app.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)
db.session.commit()
session['known'] = False
if app.config['FLASKY_ADMIN']:
send_email(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))
是不是感覺有點亂?還是亂中有序?對,義大利麵也有其亂中有序的層次,想要井然有序的架構,首先要先了解義大利麵的層次。
讓我們由上往下看。
如果先不看import的話,其中這段程式碼是不是很像某種設定值(config)呢?
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
app.config['SQLALCHEMY_DATABASE_URI'] =\
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <flasky@example.com>'
app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')
bootstrap = Bootstrap(app)
moment = Moment(app)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
mail = Mail(app)
這段很明顯是資料庫物件。
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.name
class 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
這段是為了實現每一個註冊都會傳送給admin一封郵件(不重要,知道是某功能就足夠)。
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
同3. 也是某功能
class NameForm(FlaskForm):
name = StringField('What is your name?', validators=[DataRequired()])
submit = SubmitField('Submit')
與flask實例啟動有關,因此應該會在主程式中。
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
也是很明顯。
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
複習一下:
AP收到客戶端發來的請求時,需要找到處理該請求的視圖函數(index)。 為了完成這個任務,Flask 會在應用的 URL 映射中查找請求的URL。 URL映射是URL和視圖函數之間的對應關係,Flask 使用 app.route 裝飾器或者作用相同的app.add_url_rule() 方法構建映射
@app.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)
db.session.commit()
session['known'] = False
if app.config['FLASKY_ADMIN']:
send_email(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))
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
整理一下,義大利麵層次,共有以下幾種:
以下就是我們最後要得到的井然有序的檔案結構,先有個印象。
+---flasky
| | .gitignore
| | config.py
| | flasky.py
| | LICENSE
| | README.md
| | requirements.txt
| |
| +---app
| | | email.py
| | | models.py
| | | __init__.py
| | |
| | +---main
| | | errors.py
| | | forms.py
| | | views.py
| | | __init__.py
| | |
| | +---static
| | | favicon.ico
| | |
| | \---templates
| | | 404.html
| | | 500.html
| | | base.html
| | | index.html
| | |
| | \---mail
| | new_user.html
| | new_user.txt
| |
| +---migrations
| | | alembic.ini
| | | env.py
| | | README
| | | script.py.mako
| | |
| | \---versions
| | 38c4e85512a9_initial_migration.py
| |
| +---tests
| | test_basics.py
| | __init__.py
| |
| +---venv
| |
| \---__pycache__
讓我們從第一層開始看起。
1.gitignore:沒什麼好說,Ignore it.
2.設定值(config)=>config.py:對應之前總結的"設定值(config)",更改如下:
import os
basedir = os.path.abspath(os.path.dirname(__file__))#<2>
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'
MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.googlemail.com')
MAIL_PORT = int(os.environ.get('MAIL_PORT', '587'))
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in \
['true', 'on', '1']
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')
SQLALCHEMY_TRACK_MODIFICATIONS = False
@staticmethod #<4>
def init_app(app): #<3>
pass
class 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://'
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
}
說明:
Class Config:將原本利用app.config()設定的,轉換為Config類的屬性。 Class Development/Testing/ProductionConfig:繼承自Config類,並進一步區分了dev/test/prod環境,不同環境需要不同的配置,最後再用一個config dict一一對應。
1.flask的config類(app.config()),其實就是一個dict,所以可以應用dict的方法,賦值(設定),取值。
2.os.environ.get,能從os環境中取值,如此一來就能將敏感資料放在server os的環境中,避免敏感資料外洩,用法如下:
D:\Flask_Web_Development>set FLASK_APP=flasky.py
D:\Flask_Web_Development>python
Python 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 22:45:29) [MSC v.1916 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.getenv('FLASK_APP')
'flasky.py '
3.init_app:為了再給應用提供一種定製配置的方式,Config類及其子類可以定義 init_app() 類方法,其參數為應用實例(在創建實例後才能使用的另一種配置,不過此為空)。
4.@staticmethod:為什麼要使用靜態方法?雖然與類無關但是屬於類的 ,這代表他可以客製化地為類服務,所以能使我們達成客製化Config以及其子類(Class Development/Testing/ProductionConfig)的目的。
3.flasky.py:此為主檔案,長的最像下面這樣,先大概看一下,最後再講(flasky.py就是hello.py的前身)。
import os
import click
from flask_migrate import Migrate
from app import create_app, db
from app.models import User, Role
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
migrate = Migrate(app, db)
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
@app.cli.command()
@click.argument('test_names', nargs=-1)
def test(test_names):
"""Run the unit tests."""
import unittest
if test_names:
tests = unittest.TestLoader().loadTestsFromNames(test_names)
else:
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
4.LICENSE/README.md/requirements.txt:LICENSE/說明書/環境依賴包,同樣放在第一層。
第一層所有的檔案都說明完畢,不過還有許多子包,接下來逐一說明。
app資料夾定義為存放應用的所有代碼、模板和靜態文件,因此將templates 和 static資料夾放入,且根據定義,其餘均為應用代碼的一部分,因此其他所有層次一一擺入:
2.資料庫物件(models)=>models.py:一模一樣,只是將依賴的包import進去。
from . import db
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.name
class 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
什麼包的db方法?好像怪怪的?不要緊,其實就是用到python的__init__.py構造函數,晚點就會說明。
3.email功能=>email.py:一模一樣,只是將依賴的包import進去。
from threading import Thread
from flask import current_app, render_template
from flask_mail import Message
from . import mail
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_email(to, subject, template, **kwargs):
app = current_app._get_current_object()
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
4.Form表單功能=>forms.py:一模一樣,只是將依賴的包import進去。
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class NameForm(FlaskForm):
name = StringField('What is your name?', validators=[DataRequired()])
submit = SubmitField('Submit')
5.導入db與data model(與啟動相關) => 母資料夾的flasky.py :由於與啟動相關,所以應該要擺入我們的主程式內,也就是flasky.py中。
import os
import click
from flask_migrate import Migrate
from app import create_app, db
from app.models import User, Role
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
migrate = Migrate(app, db)
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
@app.cli.command()
@click.argument('test_names', nargs=-1)
def test(test_names):
"""Run the unit tests."""
import unittest
if test_names:
tests = unittest.TestLoader().loadTestsFromNames(test_names)
else:
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
6.錯誤處理(errors)=>errors.py:一模一樣,只是將依賴的包import進去。
anotation從@app.errorhandler改變為@main.app_errorhandler,but why? 稍作解釋。
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
7.URL route與對應處理的視圖函數index (views)=>views.py:一模一樣,只是將依賴的包import進去。
flask實例app被換成flask的module "current_app,but why? 稍作解釋。"**
from flask import render_template, session, redirect, url_for, current_app
from .. import db
from ..models import User
from ..email import send_email
from . import main
from .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)
db.session.commit()
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))
撇除剛剛尚未解釋的部分,我們好像把程式碼都分配完了阿?
那又要\app__init__.py與\app\main又要做什麼呢?工廠函數與藍圖
migration與venv資料夾是與原本相同,無需做任何動作;test資料夾稍後說明,顧名思義,就是存放測試程式碼的地方。
關於config.py,如果你夠細心的話,其實關於flask與其外部擴展的實例外,這一部分程式碼通通不見了!
#flask實例化
app = Flask(__name__)
#外部擴展實例化
bootstrap = Bootstrap(app)
moment = Moment(app)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
mail = Mail(app)
為了能夠延遲實例的創建,我們有必要將其移動至另一個module:\app__init__.py。
在單個文件中開發應用是很方便,但卻有個很大的缺點:應用在全局作用域中創建,無法動態修改配置。
運行腳本時,應用實例已經創建,再修改配置為時已晚,這一點對單元測試尤其重要,因為有時為了提高測試覆蓋度,必須在不同的配置下運行應用。
這個問題的解決方法是延遲創建應用實例:
延遲創建應用實例把創建過程移到可顯式調用的工廠函數中,不僅可以給腳本留出配置應用的時間,還能夠創建多個應用實例,為測試提供便利。
因此這個構建app實例的的工廠函數,顯然應在app資料夾的構造文件(int.py)中定義:
為什麼放在app資料夾中?因為在hello.py中,會應用到這些實例的都是在其之後的程式碼,而他們正是被我們移入app資料夾的那些程式碼
from flask import Flask
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_moment import Moment
from flask_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)
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
在__init__.py中引入依賴的包,如此一來在相同的資料夾層級中(app),就不必每次用到都import一次,只需要引入app目錄,因為只要此目錄存在__init__.py,Python就會把它當作一個Module!
由於尚未初始化所需的應用實例,所以創建擴展類時沒有向構造函數傳入參數,因此擴展並未真正初始化。
create_app() 函數是應用的工廠函數,接受一個參數,是應用使用的配置名。
前面理應都可以理解,但create_app()中的倒數2,3行用途是什麼?藍圖
1.為什麼需要藍圖?
轉換成應用工廠函數的操作讓定義路由變複雜了。
在義大利麵式應用中,應用實例存在於全局作用域中,路由可以直接使用 app.route 裝飾器定義。
然而,現在應用在運行時創建,只有調用 create_app() 之後才能使用 app.route 裝飾器,但這時在create_app()中定義路由就太晚了,因為原先需要的app.route,其中的app才正要從flasky.py中實體化。
自定義的錯誤頁面處理程序也面臨相同的問題,因為錯誤頁面處理程序使用app.errorhandler裝飾器定義。
import os
import click
from flask_migrate import Migrate
from app import create_app, db
from app.models import User, Role
app = create_app(os.getenv('FLASK_CONFIG') or 'default') #這裡才開始實體化
migrate = Migrate(app, db)
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
@app.cli.command()
@click.argument('test_names', nargs=-1)
def test(test_names):
"""Run the unit tests."""
import unittest
if test_names:
tests = unittest.TestLoader().loadTestsFromNames(test_names)
else:
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
因此,Flask 使用藍本(blueprint)提供了更好的解決方法。
2.如何使用藍本
藍本和應用類似,也可以定義路由和錯誤處理程序。
不同的是,在藍本中定義的路由和錯誤處理程序處於休眠狀態,直到藍本"註冊"到應用上之後,它們才真正成為應用的一部分。使用位於全局作用域中的藍本時,定義路由和錯誤處理程序的方法幾乎與單腳本應用一樣。
因此構建app實例的的工廠函數,才需要將藍本註冊上去!
與應用一樣,藍本可以在單個文件中定義,也可使用更結構化的方式在包中的多個模塊中 創建。為了獲得最大的靈活性,我們將在應用包中創建一個子資料夾(main),用於保存應用的第一個藍本。
(1)創建主藍本=>\app\main__init__.py
from flask import Blueprint
main = Blueprint('main', __name__)
from . import views, errors
1.應用的路由保存在包裡的 app/main/views.py 模塊中,而錯誤處理程序保存在 app/main/ errors.py 模塊中,導入這兩個模塊就能把路由和錯誤處理程序與藍本關聯起來。
2.這些模塊在 app/main/init.py 腳本的末尾導入,是為了避免循環導入依賴,因為在 app/main/views.py 和 app/main/errors.py 中還要導入 main 藍本,所以除非循環引用出現在定義 main之後,否則會致使導入出錯。
(2)錯誤處理程序和定義路由 錯誤處理程序=>\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 flask import render_template, session, redirect, url_for, current_app
from .. import db
from ..models import User
from ..email import send_email
from . import main
from .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)
db.session.commit()
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))
在藍本中編寫視圖函數有兩點不同:
與前面的錯誤處理程序一樣,路由裝飾器由藍本提供,因此使用的是main.route,而非app.route。
url_for() 函數的用法不同:url_for() 函數的第一個參數是路由的端點名,在應用的路由中, 默認為視圖函數的名稱。例如,在單腳本應用中,index() 視圖函數的 URL 可使用 url_ for('index') 獲取,但在藍本中就不一樣了。
Flask 會為藍本中的全部端點加上一個命名空間,這樣就可以在不同的藍本中使用相同的端點名定義視圖函數,而不產生衝突。
命名空間是藍本的名稱(Blueprint 構造函數的第一個參數) ,而且它與端點名之間以一個點號分隔。因此,視圖函數 index() 註冊的端點名是 main.index,其 URL 使用 url_for('main.index') 獲取。
url_for() 函數還支持一種簡寫的端點形式,在藍本中可以省略藍本名,例如url_for ('.index')。
在這種寫法中,使用當前請求的藍本名補足端點名。這意味著,同一藍本中的重定向可以使用簡寫形式,但跨藍本的重定向必須使用帶有藍本名的完全限定端點名。
註:至於forms.py是否需要移入main中端看個人喜好,因為其並無使用到app.route,放在app/forms.py也可以,不過這裡為了不再改動原本視圖函數的程式碼,因此放至app/main/forms.py。
構建app實例的的工廠函數,需註冊藍本註冊。
from flask import Flask
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_moment import Moment
from flask_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)
# 註冊藍本
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
setUp :因為可能同時存在多個前置操作相同的測試,我們可以把測試的前置操作從測試代碼中拆解出來,並實現測試前置方法 setUp() 。在運行測試時,測試框架會自動地為每個單獨測試調用前置方法。
tearDown:tearDown() 方法在測試方法運行後進行清理工作,若 setUp() 成功運行,無論測試方法是否成功,都會運行 tearDown() 。
test_xxx:名稱以 test_ 開頭的方法都作為測試運行。
向下述這樣的一個測試代碼運行的環境被稱為 test fixture 。
一個新的 TestCase 實例作為一個測試腳手架,用於運行各個獨立的測試方法,在運行每個測試時,setUp() 、tearDown() 和 init() 會被調用一次。
/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'])
1.若想把 tests目錄作為包來使用,要添加 tests/init.py 模塊。 2.app.app_context().push() vs with app.app_context(): app.app_context().push() make the application context globally available, but using a with block does not
更詳細請參閱:unittest --- 單元測試框架
還記得flasky.py的def test()嗎?這就是為了運行單元測試所添加一個自定義命令。
註:@app.cli.command():有了這個裝飾器,flask就能夠自定義命令,被裝飾的函數名就是命令名。
關於Python命令行製作,參見: 利用Click快速製作可運行的命令行並說明@click.argument其nargs=-1的用途-
/flasky.py:
import os
import click
from flask_migrate import Migrate
from app import create_app, db
from app.models import User, Role
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
migrate = Migrate(app, db)
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
@app.cli.command()
@click.argument('test_names', nargs=-1)
def test(test_names):
"""Run the unit tests."""
import unittest
if test_names:
tests = unittest.TestLoader().loadTestsFromNames(test_names)
else:
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
run test on cmd:
flask test
test_app_exists (test_basics.BasicsTestCase) ... ok test_app_is_testing (test_basics.BasicsTestCase) ... ok
hello.py範例出自“Flask Web Development, 2nd Edition, by Miguel Grinberg (O’Reilly). Copyright 2018 Miguel Grinberg, 978-1-491-99173-2"
Reference:
https://github.com/dokelung/Python-QA/blob/master/questions/object/Python%E7%9A%84staticmethod%E5%9C%A8%E4%BB%80%E9%BA%BC%E6%83%85%E6%B3%81%E4%B8%8B%E7%94%A8.md https://stackoverflow.com/questions/58042067/what-is-purpose-of-init-app-function-in-flask https://matthung0807.blogspot.com/2019/11/windows-cmd-tree.html https://stackoverflow.com/questions/57734132/flask-application-context-app-app-context-push-works-but-cant-get-with-ap https://dormousehole.readthedocs.io/en/latest/appcontext.html https://dormousehole.readthedocs.io/en/latest/blueprints.html https://zhuanlan.zhihu.com/p/115350758
Share on Twitter Share on FacebookSQL Server Analytics Service 1
SEO(1) Github(2) Title Tag(2) ML(1) 李宏毅(1) SQL Server(18) Tempdb(1) SSMS(1) Windows(1) 自我成長(2) Excel(1) python Flask(1) python(5) Flask(2)
Max Chen (159)