Flask大型應用程式架構 | 從義大利麵到井然有序的架構 - Flask

Posted by: Max Chen | in Flask | 1 year ago |

工作上開始使用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))

是不是感覺有點亂?還是亂中有序?對,義大利麵也有其亂中有序的層次,想要井然有序的架構,首先要先了解義大利麵的層次。

義大利麵的層次

讓我們由上往下看。

1. 設定值(config)

如果先不看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)

2. 資料庫物件(models)

這段很明顯是資料庫物件。

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

3. email功能

這段是為了實現每一個註冊都會傳送給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

4. Form表單功能

同3. 也是某功能

class NameForm(FlaskForm):
    name = StringField('What is your name?', validators=[DataRequired()])
    submit = SubmitField('Submit')

5. 導入db與data model (與啟動相關)

與flask實例啟動有關,因此應該會在主程式中。

@app.shell_context_processor
def make_shell_context():
    return dict(db=db, User=User, Role=Role)

6. 錯誤處理(errors)

也是很明顯。

@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

7. URL route與對應處理的視圖函數index (views)

複習一下:

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

整理一下,義大利麵層次,共有以下幾種:

  1. 設定值(config)
  2. 資料庫物件(models)
  3. email功能
  4. Form表單功能
  5. 導入db與data model(與啟動相關)
  6. 錯誤處理(errors)
  7. URL route與對應處理的視圖函數index (views)

二,開始架構

以下就是我們最後要得到的井然有序的檔案結構,先有個印象。

+---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__

讓我們從第一層開始看起。

一,flasky母資料夾 (也就是包含所有物件的專案資料夾)

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子資料夾

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資料夾稍後說明,顧名思義,就是存放測試程式碼的地方。

三,進一步更好的重構 - 工廠函數與藍圖

一, 工廠函數(Factory method)

關於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
  1. 在__init__.py中引入依賴的包,如此一來在相同的資料夾層級中(app),就不必每次用到都import一次,只需要引入app目錄,因為只要此目錄存在__init__.py,Python就會把它當作一個Module!

  2. 由於尚未初始化所需的應用實例,所以創建擴展類時沒有向構造函數傳入參數,因此擴展並未真正初始化。

  3. create_app() 函數是應用的工廠函數,接受一個參數,是應用使用的配置名。

前面理應都可以理解,但create_app()中的倒數2,3行用途是什麼?藍圖

二, 藍圖(blueprint)

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))

在藍本中編寫視圖函數有兩點不同:

  1. 與前面的錯誤處理程序一樣,路由裝飾器由藍本提供,因此使用的是main.route,而非app.route。

  2. 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。

(3)註冊藍本

構建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

三,<補充>單元測試

1. 使用unittest定義單元測試類

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 --- 單元測試框架

2. 使用命令行運行單元測試

還記得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

Currently unrated
 or 

Subscribe

* indicates required

Recent Posts

Archive

2023
2022
2021

Categories

Apache 1

Data Science 2

Dbfit 1

Design Pattern 1

Devops 4

DigitalOcean 1

Django 1

English 3

Excel 5

FUN 4

Flask 3

Git 1

HackMD 1

Heroku 1

Html/Css 1

Linux 4

MDX 1

Machine Learning 2

Manufacture 1

Master Data Service 1

Mezzanine 18

Oracle 1

Postgresql 7

PowerBI 4

Powershell 4

Python 22

SEO 2

SQL Server 53

SQL Server Analytics Service 1

SQLite 1

Windows 1

database 8

work-experience 1

其他 1

投資入門 1

投資心得 2

時間管理 1

總體經濟 2

自我成長 3

資料工程 1

Tags

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)

Authors

Max Chen (159)

Feeds

RSS / Atom

Flask大型應用程式架構 | 從義大利麵到井然有序的架構 - Flask

© COPYRIGHT 2011-2022. Max的文藝復興. ALL RIGHT RESERVED.