The Flask Mega-Tutorial 之 Chapter 15: A Better Application Structure (Blueprint)_Kungreye的博客-程序员秘密

技术标签: Blueprint  Flask  

Current Limitations

1、当前 application 含有多个 subsystems,但是相关 code 交错分布,无明确界限,难以复用。

  • User Authentication:

    • app/routes.py ,部分 view funcs,
    • app/forms.py ,部分 forms,
    • app/templates ,部分 templates,
    • app/email.py , email 支持。
  • Error Handling:

    • app/errors.py , error handlers
    • app/templates , 404.html & 500.html
  • Core Functionality:

    • posts display & write
    • user profiles
    • follow & unfollow
    • live translations of posts

organization logic 是,不同 module 对应实现不同的 functionality,如 module for view func, module for web_forms, module for errors, module for emails, dir for HTML templates, etc..
当 project 规模增大时,这些 modules 的数量会越来越多,某些 module 会越来越大,难以维护。(譬如,复用 authentication 组块时,必须到不同的 module 中复制/粘贴,很不变;最理想的状况时,能够模块分离。)

解决方案Blueprint (from Flask)


2、app/__init__.py 中的 application instance 是 global variable,不利于 testing

  • 无法实例化出 两个 使用不同配置的 application instance,用于测试

    Imagine you want to test this application under different configurations. Because the application is defined as a global variable, there is really no way to instantiate two applications that use different configuration variables.

  • 由于所有 tests 共用一个 application,前面的 test 有可能更改了 application,干扰后续的 test

    Another situation that is not ideal is that all the tests use the same application, so a test could be making changes to the application that affect another test that runs later. Ideally you want all tests to run on a pristine application instance.


解决方案: 创建 application factory function,接收 configuration 为 参数,返回不同配置的 application instance


Blueprints

  • Flask blueprint,是一种逻辑结构(logical structure),代表 application 的一个子单元(subset)。
  • Blueprint 可以包含多种元素,如 routes、view funcs、forms、templates 和 static files。
  • 如果将 blueprint 写入 单独的 python package,则此包封装 application 某个功能特性相关的所有元素。

    In Flask, a blueprint is a logical structure that represents a subset of the application. A blueprint can include elements such as routes, view functions, forms, templates and static files. If you write your blueprint in a separate Python package, then you have a component that encapsulates the elements related to specific feature of the application.

  • Blueprint 内含的元素内容最初处于休眠状态,为关联它们,必须将 blueprint 注册到 application。(注册过程中,blueprint 中已添加的内容将全部传到 application)。

    So you can think of a blueprint as a temporary storage for application functionality that helps in organizing your code.


1、Error Handling Blueprint

app/
    errors/                             <-- blueprint package
        __init__.py                     <-- blueprint creation
        handlers.py                     <-- error handlers
    templates/
        errors/                         <-- error templates
            404.html
            500.html
    __init__.py                         <-- blueprint registration
  • 创建 app/errors/ ,移进原有的 app/errors.py ,并改名。
  • 创建 templates/errors/,移进 app/templates error handling 相关的 templates。
  • 创建生成蓝图的 app/errors/__init__.py
from flask import Blueprint

bp = Blueprint('errors', __name__)

from app.errors import handlers

注:若加参数 template_folder='templates',则 app/errors/templates/ 亦可使用。
生成 bp 后,引入 handlers 模块,将模块注册到 bp 中。

so that the error handlers in it are registered with the blueprint. This import is at the bottom to avoid circular dependencies.

  • handlers.py 中引入 bp,将 @app.errorhandler 替换为 @bp.app_errorhandler,并更改 template 路径。
from app.errors import bp

@bp.app_errorhandler(404)
def not_found_error(error):
    return render_template('errors/404.html'), 404

@bp 更加加 independent of application,。

  • 注册 bpapp/__init__.py
app = Flask(__name__)

# ...

from app.errors import bp as errors_bp
app.register_blueprint(errors_bp)

# ...

from app import routes, models  # <-- remove errors from this import!



Authentication Blueprint

app/
    auth/                               <-- blueprint package
        __init__.py                     <-- blueprint creation
        email.py                        <-- authentication emails
        forms.py                        <-- authentication forms
        routes.py                       <-- authentication routes
    templates/
        auth/                           <-- blueprint templates
            login.html
            register.html
            reset_password_request.html
            reset_password.html
    __init__.py                         <-- blueprint registration


1、 每个 module,只保留 auth 相关部分

  • email.py 中发密码重置邮件的函数
#..
from app.email import send_email

def send_password_reset_email(user):
    #...


  • forms.py 中:
LoginForm()
RegistrationForm()
ResetPasswordRequestForm()
ResetPasswordForm()


  • routes.py 中:
login()
logout()
register()
reset_password_request()
reset_password(token)

(1) @app.route() 更新为 bp.route()
(2) render_template() 中的 path 更新
(3) url_for() 添加相应 bp 前缀 ,如 url_for(‘login’) → url_for(‘auth.login’)
注: url_for() 的更改,包括 templates 中设置重定向的 *.html

2、注册 authentication blueprintapp/__init__.py

# ...
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
# ...

Flask gives you the option to attach a blueprint under a URL prefix, so any routes defined in the blueprint get this prefix in their URLs.
In many cases this is useful as a sort of “namespacing” that keeps all the routes in the blueprint separated from other routes in the application or other blueprints.



Main Application Blueprint

app/
    main/                               <-- blueprint package
        __init__.py                     <-- blueprint creation
        forms.py                        <-- main forms
        routes.py                       <-- main routes

    templates/
                                        <-- blueprint templates (位置不变)
            base.html
            index.html
            user.html
            _post.html
            edit_profile.html

    __init__.py                         <-- blueprint registration
  • Blueprint 名字为bp,则所有调用 mainview funcurl_for() 都须加 main. 前缀。
  • main 相关的 templates 位置不变。

    Given that this is the core functionality of the application, we leave the templates in the same locations.
    This is not a problem because templates from the other two blueprints have been moved into sub-directories.

    -


Application Factory Function

  • 引入 Blueprints 前,因为所有的 view funcserror handlers 需要的 @app 装饰器都来自于 application,所以 application 必须是 global variable
  • 引入 Blueprints 后,所有的 view funcserror handlers 已移至 blueprints 中,所以 application 无需为 global variable


# ...
db = SQLAlchemy()
migrate = Migrate()
login = LoginManager()
login.login_view = 'auth.login'
login.login_message = _l('Please log in to access this page.')
mail = Mail()
bootstrap = Bootstrap()
moment = Moment()
babel = Babel()

def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(config_class)

    db.init_app(app)
    migrate.init_app(app, db)
    login.init_app(app)
    mail.init_app(app)
    bootstrap.init_app(app)
    moment.init_app(app)
    babel.init_app(app)

    # ... no changes to blueprint registration

    if not app.debug and not app.testing:
        # ... no changes to logging setup

    return app

(1) 先创建各种 extension 实例(global)。
(2) 定义 create_app(),先创建 Flask app,后依次 extension.init_app(app),将 extension 绑定到 application 上。
(3) not app.testing:如果 configTESTINGTrue,则略过后续的 email & logging


那么, app/__init__.py 中的 create_app() 的调用者是 ?

Microblog / microblog.py
Microblog / tests.py

只有这两处内创建的 application,才存在于 global scope


current_app

1、替换 code 中 的 appcurrent_app

app/models.py:      token相关 app.config['SECRET_KEY']
app/translate.py:   MS Translator API相关 app.config['MS_TRANSLATOR_KEY']
app/main/routes.py: pagination相关 app.config['POSTS_PER_PAGE'] 
app/__init__.py:    get_locale 相关 app.config['LANGUAGES']

全部替换为 current_app (from Flask)


2、app/email.py 中涉及的 pass application instance to another thread.

from flask import current_app

def send_async_email(app, msg):
    with app.app_context():
        mail.send(msg)

def send_email(subject, sender, recipients, text_body, html_body):
    msg = Message(subject, sender=sender, recipients=recipients)
    msg.body = text_body
    msg.html = html_body
    Thread(target=send_async_email,
           args=(current_app._get_current_object(), msg)).start()
  • 首先,由于 current_app 是 context-aware 的变量(tied to the thread that is handling the client request),所以将其传到 another thread无效。
  • 其次,current_app 实质是个 proxy object,动态地映射到 application instance 上。真正需要的是 application instance
  • current_app._get_current_object() 语句,提取 proxy object 内部真实的 application instance


3、app / cli.py 中注册的 custom commands

  • 首先,创建 register() 函数,然后将 custom commands 移入,将 app instance 作为参数传入。
  • 然后,app / microblog.py 中,在创建 app 后,调用 register(app)

为何不延续 current_app 的思路呢?

答: current_app 变量只局限于处理 request 时发挥作用,但 custom commands 是在 start up 时注册,而不是请求中。

import os
import click

def register(app):
    @app.cli.group()
    def translate():
        """Translation and localization commands."""
        pass

    @translate.command()
    @click.argument('lang')
    def init(lang):
        """Initialize a new language."""
        # ...

    @translate.command()
    def update():
        """Update all languages."""
        # ...

    @translate.command()
    def compile():
        """Compile all languages."""
        # ...


4、重构 Microblog / microblog.py

from app import create_app, db, cli
from app.models import User, Post

app = create_app()
cli.register(app)

@app.shell_context_processor
def make_shell_context():
    return {
   'db': db, 'User': User, 'Post' :Post}



Unit Testing Improvements

1、定制 TestConfigMicroblog/tests.py

from config import Config

class TestConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite://'


2、为 each test 创建各自的 app instanceMicroblog/tests.py

from app import create_app, db
#...

class UserModelCase(unittest.TestCase):
    def setUp(self):
        self.app = create_app(TestConfig)
        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()

在调用 db.create_all() 之前,db 必须先从 app.config 获得 db URI,但 app factory func 有可能已生成多个 app

那如何使 db 使用 self.app=create_app() 刚刚生成的 app 呢?
答:利用 application context

  • current_app 变量,作为 app proxy,会在 current thread 中寻找 活跃的 application context,一旦找到,就可从中获取到 app
  • current_app 未找到 application context,则无法知晓活跃的 app,最终抛出异常。

    Remember the current_app variable, which somehow acts as a proxy for the application when there is no global application to import? This variable looks for an active application context in the current thread, and if it finds one, it gets the application from it. If there is no context, then there is no way to know what application is active, so current_app raises an exception.

  • 拿 python 测试(勿用 flaskshell,因为会自动激活 application context
    application context
    可以发现,直接运行 current_app 时,抛出 RuntimeError: Working outside of application context.


3、app_context().push()app_context().pop()

  • Flask 在调用 view func 处理一个 request 之前,会先 push 一个application context ,从而激活 current_appg 变量
  • request 处理结束,context 会被 pop 移掉 (连同 context 激活的种种变量)。
  • -

Before invoking your view functions, Flask pushes an application context, which brings current_app and g to life.
When the request is complete, the context is removed, along with these variables.

For the db.create_all() call to work in the unit testing setUp() method, we push an application context for the application instance just created, and in that way, db.create_all() can use current_app.config to know where is the database.
Then in the tearDown() method I pop the context to reset everything to a clean state.


联想到 app/emai.py 中,send_email() 开启一个新 thread,然后将 app instance 作为参数传入。
传入的 app instance,在 send_async_email() 中首先激活 app_context(),然后在此内执行任务。

def send_async_email(app, msg):
    with app.app_context():
        mail.send(msg)

def send_email(subject, sender, recipients, text_body, html_body):
    msg = Message(subject, sender=sender, recipients=recipients)
    msg.body = text_body
    msg.html = html_body
    Thread(target=send_async_email,
           args=(current_app._get_current_object(), msg)).start()


4、Flask 有两类 context,


  • application contextcurrent_appg

  • request contextrequestsessioncurrent_user(flask-login’s)

request context is more specific and applies to a request.
When a request context is activated right before a request is handled, Flask’s request and session variables become available, as well as Flask-Login’s current_user.

5、tests.py 测试结果
这里写图片描述



Environment Variables (使用 .env)

  • 每次打开(重开)一个 terminal session时,都须将 microblog / config.py 中的环境变量设置一番…..

    • 优化方案:在 root application dir中,创建 。env 文件,写入环境变量。


  • 支持.env 的第三方库:python-dotenv

    (venv) $ pip install python-dotenv

  • microblog/config.py 调用 .env

import os
from dotenv import load_dotenv

basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))

class Config(object):
    # ...
  • root application dir 下创建 .env

    注:勿加入 source control

注意,.env 无法用于环境变量 FLASK_APPFLASK_DEBUG ,因为这两者在 application 引导过程时即需要使用,当时 app instanceconfig obj 尚不存在。

The .env file can be used for all the configuration-time variables, but it cannot be used for Flask’s FLASK_APP and FLASK_DEBUG environment variables, because these are needed very early in the application bootstrap process, before the application instance and its configuration object exist.

-


Requirements File

  • 生成
(venv) $ pip freeze > requirements.txt

requirments.txt format

alembic==0.9.9
async-timeout==3.0.0
attrs==18.1.0
Babel==2.6.0


  • 导入安装
    To create a same virtual environment on another machine, instead of installing packages one by one, just:
(venv) $ pip install -r requirements.txt


若使用 pipenv

pipenv install -r path/to/requirements.tx

If your requirements file has version numbers pinned, you’ll likely want to edit the new Pipfile to remove those, and let pipenv keep track of pinning.
If you want to keep the pinned versions in your Pipfile.lock for now, run pipenv lock --keep-outdated. Make sure to upgrade soon!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Kungreye/article/details/80838686

智能推荐

在win10中配置java环境变量_数据结构和算法的博客-程序员秘密

想了解更多数据结构以及算法题,可以关注微信公众号“数据结构和算法”,每天一题为你精彩解答。也可以扫描下面的二维码关注1,JDK下载和安装目前使用java8的比较多,这里就以java8为例,来看下在win10中环境是怎么配置的。1,首先要下载jdk,官网地址java8 sdk下载根据你的系统进行选择,这里就以win10,64位为例下载点击之后他会弹出一个框,然后勾选图中红色部分,接着下载Oracle会让你注册一个账号,注册一个就行了,下载之后安装,安装完之后我们再来开配置环境。没配置之前我

DataFrame的applymap和apply_格林黄的博客-程序员秘密

DataFrame的applymap将DataFrame的每个元素经过函数运算之后转化成新的元素import pandas as pddf = pd.DataFrame({ 'a':[1,2,3], 'b':[4,5,6]},index=['L1','L2','L3'])print(df)df1 = df.applymap(lambda x:'even' if...

QListWidget的一些常用函数_qt中listwidget设置元素个数_alan00000的博客-程序员秘密

listWidget = QListWidget() #实例化一个(item base)的列表listWidget.addItem('dd') #添加一个项listWidget.addItems([]) # 从序列中添加子项listWidget.setDragEnabled(True) #设置拖拉listWidget.sortItems() #排序listWidget.sele

二叉树中序遍历_中序遍历二叉树_爱谁谁未成年的博客-程序员秘密

题目描述给出一棵二叉树,返回其中序遍历算法思路中序遍历,左根右,使用递归方式实现代码实现(JAVA)public List&amp;lt;Integer&amp;gt; inorderTraversal(TreeNode root) { List&amp;lt;Integer&amp;gt; result = new ArrayList&amp;lt;Integer&amp;gt;(); t...

重磅整理!推荐系统之深度召回模型综述(PART III)_文文学霸的博客-程序员秘密

NewBeeNLP原创出品作者@一块小蛋糕知乎|推荐系统小筑前段时间读完了李航、何向南的《Deep learning for matching in search and Recomm...

CSS响应式图片运用中的srcset属性_aliyunc的博客-程序员秘密

在整个网站的开发中,在管理图片上较为困难。注意,图片要在各种设备上平滑过渡显示,它们将会碰到的问题有:适当的优化和减少图片的体积注意不要浪费带宽(网站的成败与否加载速度是其中主要因素之一)设备使用相应的解决方案对于第一个问题,使用TinyPng和JPEGmini工具可以帮助减少图片的体积和优化图片。对于第二个问题,在一些场合下我们可能要使用到强大的媒体查询。多亏了有他们,我们可以很简单的...

随便推点

学习使用简单的php_weixin_30402343的博客-程序员秘密

配置文件在:/etc/php5/$中,不同的模式含有自己的php.ini配置文件。php可以运行于多种模式:cgi、fastcgi、cli、web模块模式等4种;我现在使用的模式是cli模式,这里进行一次测试。在ubuntu下需要安装sudo apt-get install php5-devphp应该是php5的链接。修改config.m4文件:...

OpenWrt添加软件包之一概述_hzlarm的博客-程序员秘密

参考 openwrt 官方文档 下面用&lt;BUILDROOT&gt;表示 Openwrt 源码树顶层目录。Openwrt 所有的软件包都都保存在&lt;BUILDROOT&gt;/package目录下,一个软件包占一个子目录。软件包目录结构例如: helloworld,如下所示&lt;BUILDROOT&gt;/package/helloworld/Makefile重点,它定义了如何构...

Java编写的教室管理系统 带详细设计报告 功能非常齐全 完整源码_java教室管理系统_bangxiecode的博客-程序员秘密

本次分享的是设计一个教室管理系统,分两种用户类型:普通老师和系统管理员。其中普老师包括查询,和修改自己的信息,查看教师排课安排等;系统管理员的操作包括老师的增删改、教室排课的增删该查等功能。系统具有完整源码,下载后直接运行,可以完美运用到课程设计中。

oracle导入blob字段,向oracle的blob字段导入文件_叫布鲁诺的中医的博客-程序员秘密

在数据库主机上创建测试目录及文件$mkdir /test$cd /test$echo "Test Subject" &gt;&gt; subject.html$echo "test ok !" &gt;&gt; mail.html定义文件路径(都是数据库主机上的),并授权$sqlplus user/[email protected]&gt;create or replace directory ...

Linux-stty命令终端显示控制_stty回显_张艳霞zhangyx的博客-程序员秘密

stty终端显示控制:将回显功能关闭:stty -echo将回显功能恢复:stty echo可参考下列操作创建一个测试脚本:[[email protected] ~]# vim user.sh //创建一个测试脚本#!/bin/bashread -p “请输入用户名:” username //读取用户名stty -echo //关闭回显read -p “请输入密码:” passwd

idea:Command line is too long. Shorten command line for xxxor also for xxx 报错_是我来晚了@的博客-程序员秘密

idea:Command line is too long. Shorten command line for xxxor also for xxx 报错

推荐文章

热门文章

相关标签