Flask部分源碼閱讀 原

Flask主要依賴於WerkzeugJinja這兩個庫,是很簡潔的Python Web框架。

  • Werkzeug 是一個WSGI的工具包,是Flask的核心庫。
  • Jinja 則是一個模板渲染的庫,主要負責渲染返回給客戶端的html文件。

各個文件的功能簡介

文件名功能簡介
__init__.py主要是添加了一些常用的模塊或者對象,方便導入
_compat.py主要是做 Python3Python2 之間的兼容處理,還有pypy的兼容處理
app.py實現與定義WSGI對象的主要模塊(WSGI規範可以參考PEP333PEP3333)
blueprints.py是Blueprints的實現與定義模塊。Blueprints的作用是項目的模塊劃分,類似於Django的APP
cli.py主要是Flask的命令行功能實現和命令行解析
config.py實現與配置相關等功能
ctx.py實現需要上下文管理相關的類與方法
debughelpers.py定義一些錯誤提示或者檢查的方法或類,增強開發者的體驗
globals.py定義所有全局對象或方法的文件
helpers.py各種幫助類工具類和方法的文件
logging.py日誌模塊的實現文件
sessions.py定義和實現cookiesession的文件
signals.py定義和處理信號的文件,主要以blinker庫實現
templating.py實現了與Jinja交互的接口文件
testing.py實現測試需要的幫助類或者函數。一般不用於生產環境
views.py定義和實現了視圖的類
wrappers.py實現WSGI規範中的Wrapper,Request和Response

主要類的簡單解析

主要類的關係

  1. 主要類的關係

    Flask_主要類的關係.png

  2. Session類的關係

    Flask_Session關係.png

  3. Wrapper類的關係

    Flask_Wrapper關係.png

Context 機制

Werkzeug.local模塊

參考之http://python.jobbole.com/87738/

Pyhton標準庫中thread.local,實現了線程局部變量,使得每個線程可以有私有變量,具有線程隔離性,可以通過線程安全的方式獲取或者改變線程中的變量。

Werkzeug.local的功能與thread.local類似,不過Werkzeug.local在支持線程的基礎上,加上了greenlet的協程支持。

在Flask的上下文管理中,主要依賴了Werkzeug.local來實現的。

Werkzeug.local主要有4個類:

  • Local
  • LocalStack
  • LocalProxy
  • LocalManager

Local

# werkzeug.local

...
try:
    from greenlet import getcurrent as get_ident
except ImportError:
    try:
        from thread import get_ident
    except ImportError:
        from _thread import get_ident
...

class Local(object):
    __slots__ = ('__storage__', '__ident_func__')

    def __init__(self):
        object.__setattr__(self, '__storage__', {})
        object.__setattr__(self, '__ident_func__', get_ident)

    def __iter__(self):
        return iter(self.__storage__.items())

    def __call__(self, proxy):
        """Create a proxy for a name."""
        return LocalProxy(self, proxy)

    def __release_local__(self):
        self.__storage__.pop(self.__ident_func__(), None)

    def __getattr__(self, name):
        try:
            return self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        ident = self.__ident_func__()
        storage = self.__storage__
        try:
            storage[ident][name] = value
        except KeyError:
            storage[ident] = {name: value}

    def __delattr__(self, name):
        try:
            del self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

  • __storage__: 字典,用來保存不同線程/協程中的線程變量。字典中的Key爲線程/協程的ID。
  • __ident_func__: 這個是保存獲取當前線程/協程的ID函數。如果是協程,則是greenlet.getcurrent。如果是線程,則是thread. get_ident

在Local中調用local.xxx時,會調用__getattr__函數,在__storage__中用當前線程/協程ID作爲Key然後再查找變量,代碼: self.__storage__[self.__ident_func__()][name]

LocalStack

LocalStack則是使用Local實現一個棧來存儲線程/協程的變量。

LocalStack還實現了push、pop、top等方法或屬性。調用這些屬性或者方法時,該類會根據當前線程/協程的ID,在Local實例中對相應的數值進行操作。

AppContext應用上下文

AppContext在請求、Flask的CLI命令及其他活動期間,保存整個應用程序級別的數據。類似於RequestContext是保存請求期間的數據一樣。

# ctx.py

class AppContext(object):
    """The application context binds an application object implicitly
    to the current thread or greenlet, similar to how the
    :class:`RequestContext` binds request information.  The application
    context is also implicitly created if a request context is created
    but the application is not on top of the individual application
    context.
    """

    def __init__(self, app):
        self.app = app
        self.url_adapter = app.create_url_adapter(None)
        self.g = app.app_ctx_globals_class()

        # Like request context, app contexts can be pushed multiple times
        # but there a basic "refcount" is enough to track them.
        self._refcnt = 0

    def push(self):
        """Binds the app context to the current context."""
        self._refcnt += 1
        if hasattr(sys, 'exc_clear'):
            sys.exc_clear()
        _app_ctx_stack.push(self)
        appcontext_pushed.send(self.app)

    def pop(self, exc=_sentinel):
        """Pops the app context."""
        try:
            self._refcnt -= 1
            if self._refcnt <= 0:
                if exc is _sentinel:
                    exc = sys.exc_info()[1]
                self.app.do_teardown_appcontext(exc)
        finally:
            rv = _app_ctx_stack.pop()
        assert rv is self, 'Popped wrong app context.  (%r instead of %r)' \
            % (rv, self)
        appcontext_popped.send(self.app)

    def __enter__(self):
        self.push()
        return self

    def __exit__(self, exc_type, exc_value, tb):
        self.pop(exc_value)

        if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
            reraise(exc_type, exc_value, tb)
# globals.py

from functools import partial
from werkzeug.local import LocalStack, LocalProxy

...

def _lookup_app_object(name):
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return getattr(top, name)

...

def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return top.app

...

# context locals
_app_ctx_stack = LocalStack()

...

current_app = LocalProxy(_find_app)
g = LocalProxy(partial(_lookup_app_object, 'g'))

從代碼中可以看到,_app_ctx_stack是一個werkzeug.local.LocalStack的棧,current_appg是通過代理,每次獲取棧頂元素AppContextappg屬性。

AppContext的實現目的,主要目的是:

  • 使得一個進程中,可以多個Flask App共存,比如Werkzeug內置的Middleware可以將兩個Flask App組合成一個WSGI應用,這種情況下,每個App都有自己獨立的AppContext
  • 可以使得一些App的全局屬性,在離線的CLI命令或者單元測試等非Web環境中中進行訪問,而不需要構造出請求,只需要將AppContext push到棧頂。

AppContext中有屬性_refcnt,這個值是記錄該上下文是否已經沒被使用,默認值爲0,每次push()則增1,pop()則減1,直至_refcnt減至0,纔會觸發teardown_appcontext的函數。

RequestContext請求上下文

RequestContext在整個請求處理期間,保存整個請求級別的數據。

# ctx.py

class RequestContext(object):
    """The request context contains all request relevant information.  It is
    created at the beginning of the request and pushed to the
    `_request_ctx_stack` and removed at the end of it.  It will create the
    URL adapter and request object for the WSGI environment provided.

    Do not attempt to use this class directly, instead use
    :meth:`~flask.Flask.test_request_context` and
    :meth:`~flask.Flask.request_context` to create this object.

    When the request context is popped, it will evaluate all the
    functions registered on the application for teardown execution
    (:meth:`~flask.Flask.teardown_request`).

    The request context is automatically popped at the end of the request
    for you.  In debug mode the request context is kept around if
    exceptions happen so that interactive debuggers have a chance to
    introspect the data.  With 0.4 this can also be forced for requests
    that did not fail and outside of ``DEBUG`` mode.  By setting
    ``'flask._preserve_context'`` to ``True`` on the WSGI environment the
    context will not pop itself at the end of the request.  This is used by
    the :meth:`~flask.Flask.test_client` for example to implement the
    deferred cleanup functionality.

    You might find this helpful for unittests where you need the
    information from the context local around for a little longer.  Make
    sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in
    that situation, otherwise your unittests will leak memory.
    """

    def __init__(self, app, environ, request=None):
        self.app = app
        if request is None:
            request = app.request_class(environ)
        self.request = request
        self.url_adapter = app.create_url_adapter(self.request)
        self.flashes = None
        self.session = None

        # Request contexts can be pushed multiple times and interleaved with
        # other request contexts.  Now only if the last level is popped we
        # get rid of them.  Additionally if an application context is missing
        # one is created implicitly so for each level we add this information
        self._implicit_app_ctx_stack = []

        # indicator if the context was preserved.  Next time another context
        # is pushed the preserved context is popped.
        self.preserved = False

        # remembers the exception for pop if there is one in case the context
        # preservation kicks in.
        self._preserved_exc = None

        # Functions that should be executed after the request on the response
        # object.  These will be called before the regular "after_request"
        # functions.
        self._after_request_functions = []

        self.match_request()

# globals.py

from functools import partial
from werkzeug.local import LocalStack, LocalProxy

...

def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)

...

# context locals
_request_ctx_stack = LocalStack()

...

request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))

...

從代碼中可以看到,_request_ctx_stack是一個werkzeug.local.LocalStack的棧,requestsession是通過代理,每次獲取棧頂元素RequestContextrequestsession屬性。

RequestContext的實現目的,主要目的是:

  • 保證了每個請求之間的信息完全隔離,避免衝突。
  • 同時,可以簡單的使用from flask import request獲取當前的請求。

一個線程只執行一個請求,所以RequestContext對於每個線程是唯一的。

當Flask應用開始處理請求時,會將當前RequestContext``push到棧頂,也會同時將AppContext``push到它的棧頂。當請求結束時,會將RequestContext``pop出,然後將AppContext也從其棧頂pop出。

流程

Flask_Context出入棧流程.png

  • 在經過步驟A後,globals.py_request_ctx_stack_app_ctx_stack的top分別爲當前的req_ctxapp_ctx,經過LocalProxy的代理後,在Flask中所有的from flask import session, request, g,都是來自棧頂元素的req_ctx或者app_ctx
  • 所以在步驟B中,對請求進行處理的都是當前線程的request。
  • 在步驟C中,會將_request_ctx_stack_app_ctx_stack的棧頂元素彈出。

RequestContext中的_implicit_app_ctx_stack是爲了防止在處理請求中有如下操作,使得app_ctxreq_ctx出棧錯誤:

# 在請求處理中
from

with req_ctx:
   ...
# 如果沒有`_implicit_app_ctx_stack`做判斷,會出錯。AppContext中的`_refcnt`作用也類似

請求接收到返回響應流程

流程的圖裏,當req_ctx.push()後,就是開始調用self.full_dispatch_request()進行請求的處理了。

Flask_full_dispatch_request的過程.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章