Flask Signal踩坑總結(jié)

0.前言:

最近使用flask搭建博客實(shí)現(xiàn)登錄時(shí),碰到登錄用戶無法分配Permissions的問題,整整花了一個(gè)星期的時(shí)間,所以想記錄一下。

1.Signal基本了解:

根據(jù)官方文檔給出的解釋:

What are signals? Signals help you decouple applications by sending notifications when actions occur elsewhere in the core framework or another Flask extensions. In short, signals allow certain senders to notify subscribers that something happened.

Signal實(shí)際上是用于解耦系統(tǒng)中實(shí)現(xiàn)某種業(yè)務(wù)邏輯和行為。所謂的解耦,就是某些行為被觸發(fā)時(shí),自動(dòng)發(fā)送定義好的一種信號(hào),與這個(gè)信號(hào)綁定的一些業(yè)務(wù)邏輯或行為,接收到這個(gè)信號(hào)后,會(huì)自動(dòng)執(zhí)行各自相應(yīng)的業(yè)務(wù)邏輯。信號(hào)和一些業(yè)務(wù)邏輯或行為綁定好了之后,只需要發(fā)送一個(gè)信號(hào)即可,所有與該行為有關(guān)的業(yè)務(wù)邏輯或行為都會(huì)自動(dòng)觸發(fā),從而實(shí)現(xiàn)了解耦。信號(hào)發(fā)送無需了解接收信號(hào)的訂閱者是誰,這就是觀察者模式的一種實(shí)現(xiàn)方式。

2.Signal實(shí)現(xiàn)原理:

from flask.signals import Namespace
from flask import current_app

signals = Namespace()
mysignal = signals.signal('save-models') #signal的創(chuàng)建

def save_models:
...
    mysignal.send(current_app._get_current_object_) #信號(hào)發(fā)送

Signal的訂閱者:
一些訂閱了指定signal也就是上文中的mysignal的函數(shù),當(dāng)調(diào)用send()函數(shù)發(fā)送signal時(shí),會(huì)自動(dòng)觸發(fā)這些訂閱者的調(diào)用,以完成某些功能。將一個(gè)普通函數(shù)變?yōu)閟ignal的訂閱函數(shù)非常簡(jiǎn)單,只要加一個(gè)decorator即可,仍以上面signal為例,定義一個(gè)訂閱者方法如下:

@mysignal.connect_via(app)
# mysignal.connect也可以
def on_model_saved():
    # do something ...

3.踩坑:

上面代碼看起來是不是很簡(jiǎn)單?接下來就開始講之前碰到的坑:
其實(shí)在Flask-Principal應(yīng)用場(chǎng)景解析中有講過,flask在用戶登錄時(shí)實(shí)現(xiàn)身份改變信號(hào)邏輯中,通過identity_changed和identity_loaded這兩個(gè)不同的signal來處理的,當(dāng)用戶登錄之前,我們都會(huì)通過發(fā)送一個(gè)信號(hào)說明用戶身份已經(jīng)改變,當(dāng)相應(yīng)的邏輯處理接收到信號(hào)之后,能后改變其身份信息以及權(quán)限信息,login_view.py:

from flask import Flask, current_app, request, session
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from flask_principal import identity_changed

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = datastore.find_user(email=form.email.data)
        if form.password.data == user.password:

            # Flask-Login的login_user方法將登錄用戶信息保存于session中
            login_user(user)

            # 發(fā)送信號(hào)
            identity_changed.send(current_app._get_current_object(),
                                  identity=Identity(user.id))

            return redirect(request.args.get('next') or '/')

    return render_template('login.html', form=form)

Signal訂閱者on_identity_change:

from flask_principal import identity_loaded, RoleNeed, UserNeed, Permission
from flask_login import current_user

admin_need = RoleNeed('admin')
admin_Permission = Permission(admin_need)


@identity_loaded.connect
def on_identity_change(sender, identity):
    identity.user = current_user

    if hasattr(current_user, 'username'):
        identity.provides.add(UserNeed(current_user.username))

    if hasattr(current_user, 'role'):
        identity.provides.add(RoleNeed(current_user.role))

    if hasattr(current_user, 'admin') 
        identity.provides.add(admin_need)

運(yùn)行后發(fā)現(xiàn){'identity': <Identity id="admin" auth_type="None" provides=set()>}provides為空
然后在debug跟蹤發(fā)現(xiàn),receivers為空的dict,不是說過只要是有定義的signal發(fā)送信號(hào),再綁定對(duì)應(yīng)的訂閱者就會(huì)自動(dòng)觸發(fā)執(zhí)行該函數(shù)嗎?


debug.png

接下來,就開始看源代碼,看看執(zhí)行流程:
1.在工廠函數(shù)初始化創(chuàng)建app時(shí),就有init_app函數(shù)進(jìn)行初始化,identity_changed.connect(self._on_identity_changed, app)來進(jìn)行連接:

from flask import Flask
from flask_login import LoginManager
from flask_principal import Principal


login_manager = LoginManager()
login_manager.session_protection = 'basic'
login_manager.login_view = 'xxx.login'
Principals = Principal()


def create_app(config_name):
    app = Flask(__name__)
    ...

    login_manager.init_app(app)
    Principals.init_app(app)//初始化app
    return app

app = create_app()

對(duì)應(yīng)源碼:

class Principal(object):
   ...
   ...

    def init_app(self, app):
        if hasattr(app, 'static_url_path'):
            self._static_path = app.static_url_path
        else:
            self._static_path = app.static_path

        app.before_request(self._on_before_request)
        identity_changed.connect(self._on_identity_changed, app)

        if self.use_sessions:
            self.identity_loader(session_identity_loader)
            self.identity_saver(session_identity_saver)

2.接著通過_on_identity_changed來進(jìn)行調(diào)用,接著調(diào)用set_identity,源碼:

class Principal(object):
   ...
   ...

    def _on_identity_changed(self, app, identity):
        if self._is_static_route():
            return

        self.set_identity(identity)

3.接著調(diào)用_set_thread_identity來執(zhí)行identity_loaded.send()發(fā)送identity_loaded信號(hào),源碼:

class Principal(object):
   ...
   ...

    def set_identity(self, identity):
        """Set the current identity.

        :param identity: The identity to set
        """

        self._set_thread_identity(identity)
        for saver in self.identity_savers:
            saver(identity)

    def _set_thread_identity(self, identity):
        g.identity = identity
        identity_loaded.send(current_app._get_current_object(),
                             identity=identity)

4.接下來,我們看send函數(shù)的源碼:

    def send(self, *sender, **kwargs):
        """Emit this signal on behalf of *sender*, passing on \*\*kwargs.

        Returns a list of 2-tuples, pairing receivers with their return
        value. The ordering of receiver notification is undefined.

        :param \*sender: Any object or ``None``.  If omitted, synonymous
          with ``None``.  Only accepts one positional argument.

        :param \*\*kwargs: Data to be sent to receivers.

        """
        # Using '*sender' rather than 'sender=None' allows 'sender' to be
        # used as a keyword argument- i.e. it's an invisible name in the
        # function signature.
        if len(sender) == 0:
            sender = None
        elif len(sender) > 1:
            raise TypeError('send() accepts only one positional argument, '
                            '%s given' % len(sender))
        else:
            sender = sender[0]
        if not self.receivers:
            return []
        else:
            return [(receiver, receiver(sender, **kwargs))

就在程序執(zhí)行到這一步中,發(fā)現(xiàn)self.receivers為dict{ },為什么找不到接收信號(hào)的訂閱者函數(shù)?其實(shí)我們回過頭來看signal基本實(shí)現(xiàn)機(jī)制就知道,當(dāng)定義一個(gè)signal之后發(fā)送信號(hào),所以與這個(gè)信號(hào)有關(guān)的業(yè)務(wù)邏輯或者行為都會(huì)自動(dòng)觸發(fā),自動(dòng)觸發(fā)的條件就是與這個(gè)信號(hào)有關(guān),也就是說,我們要用引用到這個(gè)條件才行。所以在login_view.py中,引用訂閱者函數(shù)對(duì)應(yīng)的模塊:

from flask import Flask, current_app, request, session
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from flask_principal import identity_changed
from .permissions import admin_Permission # 引用Permissions模塊

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = datastore.find_user(email=form.email.data)
        if form.password.data == user.password:

            # Flask-Login的login_user方法將登錄用戶信息保存于session中
            login_user(user)

            # 發(fā)送信號(hào)
            identity_changed.send(current_app._get_current_object(),
                                  identity=Identity(user.id))

            return redirect(request.args.get('next') or '/')

    return render_template('login.html', form=form)

總結(jié):
1.要熟悉實(shí)現(xiàn)某種功能使用到的某個(gè)模塊的實(shí)現(xiàn)原理和流程
2.善于研究源碼,熟悉flask一些機(jī)制
3.記得利用stack overflow等一些平臺(tái)解決問題。

參考:
Gevin大神的文章:https://blog.igevin.info/posts/flask-signal-get-started/
stackoverflow:https://stackoverflow.com/questions/7050137/flask-principal-tutorial-auth-authr/9781669#9781669
官方文檔:http://flask.pocoo.org/docs/1.0/signals/
blinker文檔:https://pythonhosted.org/blinker/
http://www.bjhee.com/flask-ad2.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文首發(fā)于Gevin的博客 原文鏈接:Flask Signals 入門 未經(jīng) Gevin 授權(quán),禁止轉(zhuǎn)載 1. 如...
    Gevin閱讀 2,230評(píng)論 0 12
  • 1.ReactiveCocoa簡(jiǎn)介 ReactiveCocoa(簡(jiǎn)稱為RAC),是由Github開源的一個(gè)應(yīng)用于i...
    F麥子閱讀 678評(píng)論 0 0
  • RAC使用測(cè)試Demo下載:github.com/FuWees/WPRACTestDemo 1.ReactiveC...
    FuWees閱讀 6,651評(píng)論 3 10
  • 前言 很多blog都說ReactiveCocoa好用,然后各種秀自己如何靈活運(yùn)用ReactiveCocoa,但是感...
    RainyGY閱讀 1,598評(píng)論 0 1
  • 1.ReactiveCocoa簡(jiǎn)介 ReactiveCocoa(簡(jiǎn)稱為RAC),是由Github開源的一個(gè)應(yīng)用于i...
    愛睡覺的魚閱讀 1,223評(píng)論 0 1

友情鏈接更多精彩內(nèi)容