1. 程式人生 > >用盡洪荒之力學習Flask源碼

用盡洪荒之力學習Flask源碼

sci onf comm 多語 tde 構造 包含 live bin

WSGI
app.run()
werkzeug
@app.route(‘/‘)
Context
Local
LocalStack
LocalProxy
Context Create
Stack push
Stack pop
Request
Response
Config


一直想做源碼閱讀這件事,總感覺難度太高時間太少,可望不可見。最近正好時間充裕,決定試試做一下,並記錄一下學習心得。
首先說明一下,本文研究的Flask版本是0.12。
首先做個小示例,在pycharm新建flask項目"flask_source"後,默認創建項目入口"flask_source.py"文件。
運行該文件,在瀏覽器上訪問 http://127.0.0.1:5000/上可以看到“hello,world"內容。這是flask_source.py源碼:

#源碼樣例-1
from flask import Flask

app = Flask(__name__)


@app.route(‘/‘)
def hello_world():
    return ‘Hello World!‘


if __name__ == ‘__main__‘:
    app.run()

本篇博文的目標:閱讀flask源碼了解flask服務器啟動後,用戶訪問http://127.0.0.1:5000/後瀏覽“Hello World"這個過程Flask的工作原理及代碼框架。

WSGI

WSGI,全稱 Web Server Gateway Interface,或者 Python Web Server Gateway Interface ,是基於 Python 定義的 Web 服務器和 Web 應用程序或框架之間的一種簡單而通用的接口。WSGI接口的作用是確保HTTP請求能夠轉化成python應用的一個功能調用,這也就是Gateway的意義所在,網關的作用就是在協議之前進行轉換


WSGI接口中有一個非常明確的標準,每個Python Web應用必須是可調用callable的對象且返回一個iterator,並實現了app(environ, start_response) 的接口,server 會調用 application,並傳給它兩個參數:environ 包含了請求的所有信息,start_response 是 application 處理完之後需要調用的函數,參數是狀態碼、響應頭部還有錯誤信息。引用代碼示例:

#源碼樣例-2
# 1. 可調用對象是一個函數
def application(environ, start_response):

  response_body = ‘The request method was %s‘ % environ[‘REQUEST_METHOD‘]

  # HTTP response code and message
  status = ‘200 OK‘

  # 應答的頭部是一個列表,每對鍵值都必須是一個 tuple。
  response_headers = [(‘Content-Type‘, ‘text/plain‘),
                      (‘Content-Length‘, str(len(response_body)))]

  # 調用服務器程序提供的 start_response,填入兩個參數
  start_response(status, response_headers)

  # 返回必須是 iterable
  return [response_body]

#2. 可調用對象是一個類實例
class AppClass:
    """這裏的可調用對象就是 AppClass 的實例,使用方法類似於: 
        app = AppClass()
        for result in app(environ, start_response):
            do_somthing(result)
    """

    def __init__(self):
        pass

    def __call__(self, environ, start_response):
        status = ‘200 OK‘
        response_headers = [(‘Content-type‘, ‘text/plain‘)]
        self.start(status, response_headers)
        yield "Hello world!\n"

技術分享
如上圖所示,Flask框架包含了與WSGI Server通信部分和Application本身。Flask Server本身也包含了一個簡單的WSGI Server(這也是為什麽運行flask_source.py可以在瀏覽器訪問的原因)用以開發測試使用。在實際的生產部署中,我們將使用apache、nginx+Gunicorn等方式進行部署,以適應性能要求。

app.run()

下圖是服務器啟動和處理請求的流程圖,本節從分析這個圖開始:
技術分享
flask的核心組件有兩個Jinjia2和werkzeug。
Jinjia2是一個基於python實現的模板引擎,提供對於HTML的頁面解釋,當然它的功能非常豐富,可以結合過濾器、集成、變量、流程邏輯支持等作出非常簡單又很酷炫的的web出來。Flask類實例運行會創造一個Jinjia的環境。
在本文使用的樣例中,我們是直接返回"Hello, world"字符串生成響應,因此本文將不詳細介紹Jinjia2引擎,但不否認Jinjia2對於Flask非常重要也非常有用,值得重點學習。不過在源碼學習中重點看的是werkzeug。

werkzeug

werkzeug是基於python實現的WSGI的工具組件庫,提供對HTTP請求和響應的支持,包括HTTP對象封裝、緩存、cookie以及文件上傳等等,並且werkzeug提供了強大的URL路由功能。具體應用到Flask中:

  1. Flask使用werkzeug庫中的Request類和Response類來處理HTTP請求和響應
  2. Flask應用使用werkzeug庫中的Map類和Rule類來處理URL的模式匹配,每一個URL模式對應一個Rule實例,這些Rule實例最終會作為參數傳遞給Map類構造包含所有URL模式的一個“地圖”。
  3. Flask使用SharedDataMiddleware來對靜態內容的訪問支持,也即是static目錄下的資源可以被外部,

Flask的示例運行時將與werkzeug進行大量交互:

#源碼樣例-3
def run(self, host=None, port=None, debug=None, **options):
        from werkzeug.serving import run_simple
        if host is None:
            host = ‘127.0.0.1‘
        if port is None:
            server_name = self.config[‘SERVER_NAME‘]
            if server_name and ‘:‘ in server_name:
                port = int(server_name.rsplit(‘:‘, 1)[1])
            else:
                port = 5000
        if debug is not None:
            self.debug = bool(debug)
        options.setdefault(‘use_reloader‘, self.debug)
        options.setdefault(‘use_debugger‘, self.debug)
        try:
            run_simple(host, port, self, **options)
        finally:
            # reset the first request information if the development server
            # reset normally.  This makes it possible to restart the server
            # without reloader and that stuff from an interactive shell.
            self._got_first_request = False

排除設置host、port、debug模式這些參數操作以外,我們重點關註第一句函數from werkzeug.serving import run_simple
基於wekzeug,可以迅速啟動一個WSGI應用,官方文檔 上有詳細的說明,感興趣的同學可以自行研究。我們繼續分析Flask如何與wekzeug調用。

Flask調用run_simple共傳入5個參數,分別是host=127.0.0.1, port=5001,self=app,use_reloader=False,use_debugger=False。按照上述代碼默認啟動的話,在run_simple函數中,我們執行了以下的代碼:

#源碼樣例-4
def inner():
        try:
            fd = int(os.environ[‘WERKZEUG_SERVER_FD‘])
        except (LookupError, ValueError):
            fd = None
        srv = make_server(hostname, port, application, threaded,
                          processes, request_handler,
                          passthrough_errors, ssl_context,
                          fd=fd)
        if fd is None:
            log_startup(srv.socket)
        srv.serve_forever()

上述的代碼主要的工作是啟動WSGI server並監聽指定的端口。
WSGI server啟動之後,如果收到新的請求,它的監聽在serving.py的run_wsgi中,執行的代碼如下:

#源碼樣例-5
def execute(app):
    application_iter = app(environ, start_response)
    try:
        for data in application_iter:
            write(data)
        if not headers_sent:
            write(b‘‘)
    finally:
        if hasattr(application_iter, ‘close‘):
            application_iter.close()
        application_iter = None

還記得上面介紹WSGI的內容時候強調的python web實現時需要實現的一個WSGI標準接口,特別是源碼樣例-2中的第二個參考樣例實現,Flask的實現與之類似,當服務器(gunicorn/uwsgi…)接收到HTTP請求時,它通過werkzeug再execute函數中通過application_iter = app(environ, start_response)調用了Flask應用實例app(在run_simple中傳進去的),實際上調用的是Flask類的__call__方法,因此Flask處理HTTP請求的流程將從__call__開始,代碼如下:

#源碼樣例-6
def __call__(self, environ, start_response):
        """Shortcut for :attr:`wsgi_app`."""
        return self.wsgi_app(environ, start_response)

我們來看一下wsgi_app這個函數做了什麽工作:

#源碼樣例-7
def wsgi_app(self, environ, start_response):
    ctx = self.request_context(environ)
    ctx.push()
    error = None
    try:
        try:
            response = self.full_dispatch_request()
        except Exception as e:
            error = e
            response = self.handle_exception(e)
        return response(environ, start_response)
    finally:
        if self.should_ignore_error(error):
            error = None
        ctx.auto_pop(error)

在Flask的源碼註釋中,開發者顯著地標明"The actual WSGI application.",這個函數的工作流程包括:

  1. ctx = self.request_context(environ)創建請求上下文,並把它推送到棧中,在“上下文”章節我們會介紹其數據結構。
  2. response = self.full_dispatch_request()處理請求,通過flask的路由尋找對應的視圖函數進行處理,會在下一章介紹這個函數
  3. 通過try…except封裝處理步驟2的處理函數,如果有問題,拋出500錯誤。
  4. ctx.auto_pop(error)當前請求退棧。

@app.route(‘/‘)

Flask路由的作用是用戶的HTTP請求對應的URL能找到相應的函數進行處理。
@app.route(‘/‘)通過裝飾器的方式為對應的視圖函數指定URL,可以一對多,即一個函數對應多個URL。
Flask路由的實現時基於werkzeug的URL Routing功能,因此在分析Flask的源碼之前,首先學習一下werkzeug是如何處理路由的。
werkzeug有兩類數據結構:Map和Rule:

  • Map,主要作用是提供ImmutableDict來存儲URL的Rule實體。
  • Rule,代表著URL與endpoint一對一匹配的模式規則。
    舉例說明如下,假設在werkzeug中設置了如下的路由,當用戶訪問http://myblog.com/,werkzeug會啟用別名為blog/index的函數來處理用戶請求。
#源碼樣例-8
from werkzeug.routing import Map, Rule, NotFound, RequestRedirect

url_map = Map([
    Rule(‘/‘, endpoint=‘blog/index‘),
    Rule(‘/<int:year>/‘, endpoint=‘blog/archive‘),
    Rule(‘/<int:year>/<int:month>/‘, endpoint=‘blog/archive‘),
    Rule(‘/<int:year>/<int:month>/<int:day>/‘, endpoint=‘blog/archive‘),
    Rule(‘/<int:year>/<int:month>/<int:day>/<slug>‘,
        endpoint=‘blog/show_post‘),
    Rule(‘/about‘, endpoint=‘blog/about_me‘),
    Rule(‘/feeds/‘, endpoint=‘blog/feeds‘),
    Rule(‘/feeds/<feed_name>.rss‘, endpoint=‘blog/show_feed‘)
])

def application(environ, start_response):
    urls = url_map.bind_to_environ(environ)
    try:
        endpoint, args = urls.match()
    except HTTPException, e:
        return e(environ, start_response)
    start_response(‘200 OK‘, [(‘Content-Type‘, ‘text/plain‘)])
    return [‘Rule points to %r with arguments %r‘ % (endpoint, args)]

更多關於werkzeug路由的細節可以看官方文檔。
在上面的示例中,werkzeug完成了url與endpoint的匹配,endpoint與視圖函數的匹配將由Flask來完成,Flask通過裝飾器的方式來包裝app.route,實際工作函數是add_url_rule,其工作流程如下:

  1. 處理endpoint和構建methods,methods默認是GET和OPTIONS,即默認處理的HTTP請求是GET/OPTIONS方式;
  2. self.url_map.add(rule) 更新url_map,本質是更新werkzeug的url_map
  3. self.view_functions[endpoint] = view_func 更新view_functions,更新endpoint和視圖函數的匹配,兩者必須一一匹配,否則報錯AssertionError。
#源碼樣例-9
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
    if endpoint is None:
        endpoint = _endpoint_from_view_func(view_func)
    options[‘endpoint‘] = endpoint
    methods = options.pop(‘methods‘, None)
    if methods is None:
        methods = getattr(view_func, ‘methods‘, None) or (‘GET‘,)
    if isinstance(methods, string_types):
        raise TypeError(‘Allowed methods have to be iterables of strings, ‘
                        ‘for example: @app.route(…, methods=["POST"])‘)
    methods = set(item.upper() for item in methods)
    required_methods = set(getattr(view_func, ‘required_methods‘, ()))

    provide_automatic_options = getattr(view_func,
                                        ‘provide_automatic_options‘, None)

    if provide_automatic_options is None:
        if ‘OPTIONS‘ not in methods:
            provide_automatic_options = True
            required_methods.add(‘OPTIONS‘)
        else:
            provide_automatic_options = False

    methods |= required_methods

    rule = self.url_rule_class(rule, methods=methods, **options)
    rule.provide_automatic_options = provide_automatic_options

    self.url_map.add(rule)
    if view_func is not None:
        old_func = self.view_functions.get(endpoint)
        if old_func is not None and old_func != view_func:
            raise AssertionError(‘View function mapping is overwriting an ‘
                                ‘existing endpoint function: %s‘ % endpoint)
        self.view_functions[endpoint] = view_func

設置好了Flask的路由之後,接下來再看看在上一章節中當用戶請求進來後是如何匹配請求和視圖函數的。
用戶請求進來後,Flask類的wsgi_app函數進行處理,其調用了full_dispatch_request函數進行處理:

#源碼樣例-10
def full_dispatch_request(self):
    self.try_trigger_before_first_request_functions()
    try:
        request_started.send(self)
        rv = self.preprocess_request()
        if rv is None:
            rv = self.dispatch_request()
    except Exception as e:
        rv = self.handle_user_exception(e)
    return self.finalize_request(rv)

講一下這個處理的邏輯:

  1. self.try_trigger_before_first_request_functions()觸發第一次請求之前需要處理的函數,只會執行一次。
  2. self.preprocess_request()觸發用戶設置的在請求處理之前需要執行的函數,這個可以通過@app.before_request來設置,使用的樣例可以看我之前寫的博文中的示例-11
  3. rv = self.dispatch_request() 核心的處理函數,包括了路由的匹配,下面會展開來講
  4. rv = self.handle_user_exception(e) 處理異常
  5. return self.finalize_request(rv),將返回的結果轉換成Response對象並返回。
    接下來我們看dispatch_request函數:
#源碼樣例-11
def dispatch_request(self):
    req = _request_ctx_stack.top.request
    if req.routing_exception is not None:
        self.raise_routing_exception(req)
    rule = req.url_rule
    if getattr(rule, ‘provide_automatic_options‘, False)             and req.method == ‘OPTIONS‘:
        return self.make_default_options_response()
    return self.view_functions[rule.endpoint](**req.view_args)

處理的邏輯如下:

  1. req = _request_ctx_stack.top.request獲得請求對象,並檢查有效性。
  2. 對於請求的方法進行判斷,如果HTTP請求時OPTIONS類型且用戶未設置provide_automatic_options=False,則進入默認的OPTIONS請求回應,否則請求endpoint匹配的函數執行,並返回內容。
    在上述的處理邏輯中,Flask從請求上下文中獲得匹配的rule,這是如何實現的呢,請看下一節“上下文”。

Context

純粹的上下文Context理解可以參見知乎的這篇文章,可以認為上下文就是程序的工作環境。
Flask的上下文較多,用途也不一致,具體包括:

對象上下文類型說明
current_app AppContext 當前的應用對象
g AppContext 處理請求時用作臨時存儲的對象,當前請求結束時被銷毀
request RequestContext 請求對象,封裝了HTTP請求的額內容
session RequestContext 用於存儲請求之間需要共享的數據

引用博文Flask 的 Context 機制:

App Context 代表了“應用級別的上下文”,比如配置文件中的數據庫連接信息;Request Context 代表了“請求級別的上下文”,比如當前訪問的 URL。這兩種上下文對象的類定義在 flask.ctx 中,它們的用法是推入 flask.globals 中創建的 _app_ctx_stack 和 _request_ctx_stack 這兩個單例 Local Stack 中。因為 Local Stack 的狀態是線程隔離的,而 Web 應用中每個線程(或 Greenlet)同時只處理一個請求,所以 App Context 對象和 Request Context 對象也是請求間隔離的。

在深入分析上下文源碼之前,需要特別介紹一下Local、LocalProxy和LocalStack。這是由werkzeug的locals模塊提供的數據結構:

Local

#源碼樣例-12
class Local(object):
    __slots__ = (‘__storage__‘, ‘__ident_func__‘)

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

   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}

Local維護了兩個對象:1.stroage,字典;2.idente_func, 調用的是thread的get_indent方法,從_thread內置模塊導入,得到的線程號。
註意,這裏的__stroage__的數據組織形式是:__storage__ ={ident1:{name1:value1},ident2:{name2:value2},ident3:{name3:value3}}所以取值時候__getattr__通過self.__storage__[self.__ident_func__()][name]獲得。
這種設計確保了Local類實現了類似 threading.local 的效果——多線程或者多協程情況下全局變量的相互隔離。

LocalStack

一種基於棧的數據結構,其本質是維護了一個Locals對象的代碼示例如下:

#源碼樣例-13
class LocalStack(object):
    def __init__(self):
        self._local = Local()

   def push(self, obj):
        rv = getattr(self._local, ‘stack‘, None)
        if rv is None:
            self._local.stack = rv = []
        rv.append(obj)
        return rv

    def pop(self):
        stack = getattr(self._local, ‘stack‘, None)
        if stack is None:
            return None
        elif len(stack) == 1:
            release_local(self._local)
            return stack[-1]
        else:
            return stack.pop()

    @property
    def top(self):
        """The topmost item on the stack.  If the stack is empty,
        `None` is returned.
        """
        try:
            return self._local.stack[-1]
        except (AttributeError, IndexError):
            return None
>>> ls = LocalStack()
>>> ls.push(42)
>>> ls.top
42
>>> ls.push(23)
>>> ls.top
23
>>> ls.pop()
23
>>> ls.top
42

LocalProxy

典型的代理模式實現,在構造時接受一個callable參數,這個參數被調用後返回對象是一個Thread Local的對象,對一個LocalProxy對象的所有操作,包括屬性訪問、方法調用都會轉發到Callable參數返回的對象上。LocalProxy 的一個使用場景是 LocalStack 的 call 方法。比如 my_local_stack 是一個 LocalStack 實例,那麽 my_local_stack() 能返回一個 LocalProxy 對象,這個對象始終指向 my_local_stack 的棧頂元素。如果棧頂元素不存在,訪問這個 LocalProxy 的時候會拋出 RuntimeError。
LocalProxy的初始函數:

#源碼樣例-14
def __init__(self, local, name=None):
    object.__setattr__(self, ‘_LocalProxy__local‘, local)
    object.__setattr__(self, ‘__name__‘, name)

LocalProxy與LocalStack可以完美地結合起來,首先我們註意LocalStack的__call__方法:

#源碼樣例-15
def __call__(self):
    def _lookup():
        rv = self.top
        if rv is None:
            raise RuntimeError(‘object unbound‘)
        return rv
    return LocalProxy(_lookup)

假設創建一個LocalStack實例:

#源碼樣例-16
_response_local = LocalStack()
response = _response_local()

然後,response就成了一個LocalProxy對象,能操作LocalStack的棧頂元素,該對象有兩個元素:_LocalProxy__local(等於_lookup函數)和__name__(等於None)。

這種設計簡直碉堡了!!!!

回到Flask的上下文處理流程,這裏引用Flask的核心機制!關於請求處理流程和上下文的一張圖進行說明:
技術分享

Context Create

#源碼樣例-17
def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)

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
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, ‘request‘))
session = LocalProxy(partial(_lookup_req_object, ‘session‘))
g = LocalProxy(partial(_lookup_app_object, ‘g‘))

從源碼可以了解到以下內容:
*. Flask維護的request全局變量_request_ctx_stack 和app全局變量_app_ctx_stack 均為LocalStack結構,這兩個全局變量均是Thread local的棧結構
*. request、session每次都是調用_request_ctx_stack棧頭部的數據來獲取和保存裏面的請求上下文信息。

為什麽需要LocalProxy對象,而不是直接引用LocalStack的值?引用flask 源碼解析:上下文的介紹:

這是因為 flask 希望在測試或者開發的時候,允許多 app 、多 request 的情況。而 LocalProxy 也是因為這個才引入進來的!我們拿 current_app = LocalProxy(_find_app) 來舉例子。每次使用 current_app 的時候,他都會調用 _find_app 函數,然後對得到的變量進行操作。如果直接使用 current_app = _find_app() 有什麽區別呢?區別就在於,我們導入進來之後,current_app 就不會再變化了。如果有多 app 的情況,就會出現錯誤。

原文示例代碼:

#源碼樣例-18
from flask import current_app

app = create_app()
admin_app = create_admin_app()

def do_something():
    with app.app_context():
        work_on(current_app)
        with admin_app.app_context():
            work_on(current_app)

我的理解是:Flask考慮了一些極端的情況出現,例如兩個Flask APP通過WSGI的中間件組成一個應用,兩個APP同時運行的情況,因此需要動態的更新當前的應用上下文,而_app_ctx_stack每次都指向棧的頭元素,並且更新頭元素(如果存在刪除再創建)來確保當前運行的上下文(包括請求上下文和應用上下文)的準確

Stack push

在本文第二章節介紹Flask運行流程的內容時,我們介紹了wsig_app函數,這個函數是處理用戶的HTTP請求的,其中有兩句ctx = self.request_context(environ)ctx.push()兩句。
本質上實例了一個RequestContext,通過WSGI server傳過來的environ來構建一個請求的上下文。源碼:

#源碼樣例-19
class RequestContext(object):
    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      
        self.preserved = False
        self._preserved_exc = None
        self._after_request_functions = []
        self.match_request()

    def push(self):
        top = _request_ctx_stack.top
        if top is not None and top.preserved:
            top.pop(top._preserved_exc)
        app_ctx = _app_ctx_stack.top
        if app_ctx is None or app_ctx.app != self.app:
            app_ctx = self.app.app_context()
            app_ctx.push()
            self._implicit_app_ctx_stack.append(app_ctx)
        else:
            self._implicit_app_ctx_stack.append(None)

        if hasattr(sys, ‘exc_clear‘):
            sys.exc_clear()

        _request_ctx_stack.push(self)

        self.session = self.app.open_session(self.request)
        if self.session is None:
            self.session = self.app.make_null_session()

Flask的上下文入棧的操作在RequestContext類的push函數:

  1. 清空_request_ctx_stack棧;
  2. 確保當前的Flask實例推入_app_ctx_stack棧;
  3. 根據WSGI服務器傳入的environ構建了request(在__init__函數完成),將該request推入_request_ctx_stack棧;
  4. 創建session對象。

Stack pop

wsig_app函數在完成上一小節上下文入棧之後進行請求分發,進行路由匹配尋找視圖函數處理請求,並生成響應,此時用戶可以在應用程序中import上下文對象作為全局變量進行訪問:

from flask import request,session,request,g

請求完成後,同樣在源碼樣例-7wsgi_app函數中可以看到上下文出棧的操作ctx.auto_pop(error),auto_pop函數只彈出請求上下文,應用上下文仍然存在以應對下次的HTTP請求。至此,上下文的管理和操作機制介紹完畢。

Request

接下來繼續學習Flask的請求對象。Flask是基於WSGI服務器werkzeug傳來的environ參數來構建請求對象的,檢查發現environ傳入的是一個字典,在本文的經典訪問樣例(返回“Hello, World")中,傳入的environ包含的信息包括

"wsgi.multiprocess":"False"
"SERVER_SOFTWARE":"Werkzeug/0.11.15"
"SCRIPT_NAME":""
"REQUEST_METHOD":"GET"
"PATH_INFO":"/favicon.ico"
"SERVER_PROTOCOL":"HTTP/1.1"
"QUERY_STRING":""
"werkzeug.server.shutdown":"<function shutdown_server at 0x0000000003F4FAC8>"
"CONTENT_LENGTH":""
"HTTP_USER_AGENT":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0"
"HTTP_CONNECTION":"keep-alive"
"SERVER_NAME":"127.0.0.1"
"REMOTE_PORT":"12788"
"wsgi.url_scheme":"http"
"SERVER_PORT":"5000"
"wsgi.input":"<socket._fileobject object at 0x0000000003E18408>"
"HTTP_HOST":"127.0.0.1:5000"
"wsgi.multithread":"False"
"HTTP_ACCEPT":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
"wsgi.version":"(1, 0)"
"wsgi.run_once":"False"
"wsgi.errors":"<open file ‘<stderr>‘, mode ‘w‘ at 0x0000000001DD2150>"
"REMOTE_ADDR":"127.0.0.1"
"HTTP_ACCEPT_LANGUAGE":"zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3"
"CONTENT_TYPE":""
"HTTP_ACCEPT_ENCODING":"gzip, deflate"

Flask需要將WSGI server傳進來的上述的字典改造成request對象,它是通過調用werkzeug.wrappers.Request類來進行構建。Request沒有構造方法,且Request繼承了多個類,在

#源碼樣例-20
class Request(BaseRequest, AcceptMixin, ETagRequestMixin,
              UserAgentMixin, AuthorizationMixin,
              CommonRequestDescriptorsMixin):

    """Full featured request object implementing the following mixins:

    - :class:`AcceptMixin` for accept header parsing
    - :class:`ETagRequestMixin` for etag and cache control handling
    - :class:`UserAgentMixin` for user agent introspection
    - :class:`AuthorizationMixin` for http auth handling
    - :class:`CommonRequestDescriptorsMixin` for common headers
    """

這裏有多重繼承,有多個類負責處理request的不同內容,python的多重繼承按照從下往上,從左往右的入棧出棧順序進行繼承,且看構造方法的參數匹配。在Request的匹配中只有BaseRequest具有構造函數,其他類只有功能函數,這種設計模式很特別,但是跟傳統的設計模式不太一樣,傳統的設計模式要求是多用組合少用繼承多用拓展少用修改,這種利用多重繼承來達到類功能組合的設計模式稱為Python的mixin模式,感覺的同學可以看看Python mixin模式,接下來重點關註BaseRequest。
底層的Request功能均由werkzeug來實現,這邊不再一一贅述。

Response

在本文的源碼樣例-1中,訪問URL地址“http://127.0.0.1” 後,查看返回的response,除了正文文本"Hello, world"外,我們還可以得到一些額外的信息,通過Chrome調試工具可以看到:
技術分享
以上的信息都是通過flask服務器返回,因此,在視圖函數返回“Hello,World”的響應後,Flask對響應做了進一步的包裝。本章節分析一下Flask如何封裝響應信息。
在本文的源碼樣例-10中用戶的請求由full_dispatch_request函數進行處理,其調用了視圖函數index()返回得到rv=‘Hello, World‘,接下來調用了finalize_request函數進行封裝,得到其源碼如下:

#源碼樣例-21
def finalize_request(self, rv, from_error_handler=False):
    response = self.make_response(rv)
    try:
        response = self.process_response(response)
        request_finished.send(self, response=response)
    except Exception:
        if not from_error_handler:
            raise
        self.logger.exception(‘Request finalizing failed with an ‘
                              ‘error while handling an error‘)
    return response

def make_response(self, rv):
    status_or_headers = headers = None
    if isinstance(rv, tuple):
        rv, status_or_headers, headers = rv + (None,) * (3 - len(rv))

    if rv is None:
        raise ValueError(‘View function did not return a response‘)

    if isinstance(status_or_headers, (dict, list)):
        headers, status_or_headers = status_or_headers, None

    if not isinstance(rv, self.response_class):
        if isinstance(rv, (text_type, bytes, bytearray)):
            rv = self.response_class(rv, headers=headers,
                                    status=status_or_headers)
            headers = status_or_headers = None
        else:
            rv = self.response_class.force_type(rv, request.environ)

    if status_or_headers is not None:
        if isinstance(status_or_headers, string_types):
            rv.status = status_or_headers
        else:
            rv.status_code = status_or_headers
    if headers:
        rv.headers.extend(headers)

    return rv

def process_response(self, response):
    ctx = _request_ctx_stack.top
    bp = ctx.request.blueprint
    funcs = ctx._after_request_functions
    if bp is not None and bp in self.after_request_funcs:
        funcs = chain(funcs, reversed(self.after_request_funcs[bp]))
    if None in self.after_request_funcs:
        funcs = chain(funcs, reversed(self.after_request_funcs[None]))
    for handler in funcs:
        response = handler(response)
    if not self.session_interface.is_null_session(ctx.session):
        self.save_session(ctx.session, response)
    return response

返回信息的封裝順序如下:

  1. response = self.make_response(rv):根據視圖函數返回值生成response對象。
  2. response = self.process_response(response):在response發送給WSGI服務器錢對於repsonse進行後續處理,並執行當前請求的後續hooks函數。
  3. request_finished.send(self, response=response) 向特定的訂閱者發送響應信息。關於Flask的信號機制可以學習一下這篇博文,這裏不再展開詳細說明。

make_response該函數可以根據不同的輸入得到不同的輸出,即參數rv的類型是多樣化的,包括:

  • str/unicode,如源碼樣例-1所示直接返回str後,將其設置為body主題後,調用response_class生成其他響應信息,例如狀態碼、headers信息。
  • tuple 通過構建status_or_headers和headers來進行解析。在源碼樣例-1可以修改為返回return make_response((‘hello,world!‘, 202, None)),得到返回碼也就是202,即可以在視圖函數中定義返回狀態碼和返回頭信息。
  • WSGI方法:這個用法沒有找到示例,不常見。
  • response類實例。視圖函數可以直接通過調用make_response接口,該接口可以提供給用戶在視圖函數中設計自定制的響應,可以參見我之前寫的博文lask自帶的常用組件介紹, 相較於tuple類型,response能更加豐富和方便地訂制響應。

process_response處理了兩個邏輯:

  1. 將用戶定義的after_this_request方法進行執行,同時檢查了是否在blueprint中定義了after_requestafter_app_request,如果存在,將其放在執行序列;
  2. 保存sesseion。

上述的源碼是flask對於response包裝的第一層外殼,去除這個殼子可以看到,flask實際上調用了Response類對於傳入的參數進行包裝,其源碼如下:

#源碼樣例-21
class Response(ResponseBase):
    """The response object that is used by default in Flask.  Works like the
    response object from Werkzeug but is set to have an HTML mimetype by
    default.  Quite often you don‘t have to create this object yourself because
    :meth:`~flask.Flask.make_response` will take care of that for you.

    If you want to replace the response object used you can subclass this and
    set :attr:`~flask.Flask.response_class` to your subclass.
    """
    default_mimetype = ‘text/html‘

嗯,基本上 沒啥內容,就是繼承了werkzeug.wrappers:Response,註意上面的類註釋,作者明確建議使用flask自帶的make_response接口來定義response對象,而不是重新實現它。werkzeug實現Response的代碼參見教程 這裏就不再展開分析了。

Config

Flask配置導入對於其他項目的配置導入有很好的借鑒意義,所以我這裏還是作為一個單獨的章節進行源碼學習。Flask常用的四種方式進行項目參數的配置,分別是:

#Type1: 直接配置參數
app.config[‘SECRET_KEY‘] = ‘YOUCANNOTGUESSME‘

#Type2: 從環境變量中獲得配置文件名並導入配置參數
export MyAppConfig=/path/to/settings.cfg #linux
set MyAppConfig=d:\settings.cfg#不能立即生效,不建議windows下通過這種方式獲得環境變量。
app.config.from_envvar(‘MyAppConfig‘)

#Type3: 從對象中獲得配置
class Config(object):
    DEBUG = False
    TESTING = False
    DATABASE_URI = ‘sqlite://:memory:‘
class ProductionConfig(Config):
    DATABASE_URI = ‘mysql://[email protected]/foo‘
app.config.from_object(ProductionConfig)
print app.config.get(‘DATABASE_URI‘)

#Type4: 從文件中獲得配置參數
# default_config.py
HOST = ‘localhost‘
PORT = 5000
DEBUG = True
# flask中使用
app.config.from_pyfile(‘default_config.py‘)

Flask已經默認自帶的配置包括:
[‘JSON_AS_ASCII‘, ‘USE_X_SENDFILE‘, ‘SESSION_COOKIE_PATH‘, ‘SESSION_COOKIE_DOMAIN‘, ‘SESSION_COOKIE_NAME‘, ‘SESSION_REFRESH_EACH_REQUEST‘, ‘LOGGER_HANDLER_POLICY‘, ‘LOGGER_NAME‘, ‘DEBUG‘, ‘SECRET_KEY‘, ‘EXPLAIN_TEMPLATE_LOADING‘, ‘MAX_CONTENT_LENGTH‘, ‘APPLICATION_ROOT‘, ‘SERVER_NAME‘, ‘PREFERRED_URL_SCHEME‘, ‘JSONIFY_PRETTYPRINT_REGULAR‘, ‘TESTING‘, ‘PERMANENT_SESSION_LIFETIME‘, ‘PROPAGATE_EXCEPTIONS‘, ‘TEMPLATES_AUTO_RELOAD‘, ‘TRAP_BAD_REQUEST_ERRORS‘, ‘JSON_SORT_KEYS‘, ‘JSONIFY_MIMETYPE‘, ‘SESSION_COOKIE_HTTPONLY‘, ‘SEND_FILE_MAX_AGE_DEFAULT‘, ‘PRESERVE_CONTEXT_ON_EXCEPTION‘, ‘SESSION_COOKIE_SECURE‘, ‘TRAP_HTTP_EXCEPTIONS‘]
其中關於debug這個參數要特別的進行說明,當我們設置為app.config["DEBUG"]=True時候,flask服務啟動後進入調試模式,在調試模式下服務器的內部錯誤會展示到web前臺,舉例說明:

app.config["DEBUG"]=True

@app.route(‘/‘)
def hello_world():
    a=3/0
    return ‘Hello World!‘

打開頁面我們會看到
技術分享
除了顯示錯誤信息以外,Flask還支持從web中提供console進行調試(需要輸入pin碼),破解pin碼很簡單,這意味著用戶可以對部署服務器執行任意的代碼,所以如果Flask發布到生產環境,必須確保DEBUG=False
嗯,有空再寫一篇關於Flask的安全篇。另外,關於如何配置Flask參數讓網站更加安全,可以參考這篇博文,寫的很好。
接下來繼續研究Flask源碼中關於配置的部分。可以發現configapp的一個屬性,而app是Flask類的一個示例,並且可以通過app.config["DEBUG"]=True來設置屬性,可以大膽猜測config應該是一個字典類型的類屬性變量,這一點在源碼中驗證了:

#: The configuration dictionary as :class:`Config`.  This behaves
#: exactly like a regular dictionary but supports additional methods
#: to load a config from files.
self.config = self.make_config(instance_relative_config)

我們進一步看看make_config函數的定義:

def make_config(self, instance_relative=False):
    """Used to create the config attribute by the Flask constructor.
    The `instance_relative` parameter is passed in from the constructor
    of Flask (there named `instance_relative_config`) and indicates if
    the config should be relative to the instance path or the root path
    of the application.

    .. versionadded:: 0.8
    """
    root_path = self.root_path
    if instance_relative:
        root_path = self.instance_path
    return self.config_class(root_path, self.default_config)

config_class = Config

其中有兩個路徑要選擇其中一個作為配置導入的默認路徑,這個用法在上面推薦的博文中用到過,感興趣的看看,make_config真正功能是返回config_class的函數,而這個函數直接指向Config類,也就是說make_config返回的是Config類的實例。似乎這裏面有一些設計模式在裏面,後續再研究一下。記下來是Config類的定義:

class Config(dict):
      def __init__(self, root_path, defaults=None):
        dict.__init__(self, defaults or {})
        self.root_path = root_path

root_path代表的是項目配置文件所在的目錄。defaults是Flask默認的參數,用的是immutabledict數據結構,是dict的子類,其中default中定義為:

#: Default configuration parameters.
    default_config = ImmutableDict({
        ‘DEBUG‘:                                get_debug_flag(default=False),
        ‘TESTING‘:                              False,
        ‘PROPAGATE_EXCEPTIONS‘:                None,
        ‘PRESERVE_CONTEXT_ON_EXCEPTION‘:        None,
        ‘SECRET_KEY‘:                          None,
        ‘PERMANENT_SESSION_LIFETIME‘:          timedelta(days=31),
        ‘USE_X_SENDFILE‘:                      False,
        ‘LOGGER_NAME‘:                          None,
        ‘LOGGER_HANDLER_POLICY‘:              ‘always‘,
        ‘SERVER_NAME‘:                          None,
        ‘APPLICATION_ROOT‘:                    None,
        ‘SESSION_COOKIE_NAME‘:                  ‘session‘,
        ‘SESSION_COOKIE_DOMAIN‘:                None,
        ‘SESSION_COOKIE_PATH‘:                  None,
        ‘SESSION_COOKIE_HTTPONLY‘:              True,
        ‘SESSION_COOKIE_SECURE‘:                False,
        ‘SESSION_REFRESH_EACH_REQUEST‘:        True,
        ‘MAX_CONTENT_LENGTH‘:                  None,
        ‘SEND_FILE_MAX_AGE_DEFAULT‘:            timedelta(hours=12),
        ‘TRAP_BAD_REQUEST_ERRORS‘:              False,
        ‘TRAP_HTTP_EXCEPTIONS‘:                False,
        ‘EXPLAIN_TEMPLATE_LOADING‘:            False,
        ‘PREFERRED_URL_SCHEME‘:                ‘http‘,
        ‘JSON_AS_ASCII‘:                        True,
        ‘JSON_SORT_KEYS‘:                      True,
        ‘JSONIFY_PRETTYPRINT_REGULAR‘:          True,
        ‘JSONIFY_MIMETYPE‘:                    ‘application/json‘,
        ‘TEMPLATES_AUTO_RELOAD‘:                None,
    })

我們再看看Config的三個導入函數from_envvar,from_pyfile, from_objectfrom_envvar相當於在from_pyfile外面包了一層殼子,從環境變量中獲得,其函數註釋中也提到了這一點。而from_pyfile最終也是調用from_object。所以我們的重點是看from_object這個函數的細節。
from_pyfile源碼中有一句特別難懂,如下。config_file是讀取的文件頭,file_name是文件名稱。

exec (compile(config_file.read(), filename, ‘exec‘), d.__dict__)

dict__是python的內置屬性,包含了該對象(python萬事萬物都是對象)的屬性變量。類的實例對象的__dict__只包括類實例後的變量,而類對象本身的__dict__還包括包括一些類內置屬性和類變量clsvar以及構造方法__init
再理解exec函數,exec語句用來執行存儲在代碼對象、字符串、文件中的Python語句,eval語句用來計算存儲在代碼對象或字符串中的有效的Python表達式,而compile語句則提供了字節編碼的預編譯。

exec(object[, globals[, locals]]) #內置函數

其中參數obejctobj對象可以是字符串(如單一語句、語句塊),文件對象,也可以是已經由compile預編譯過的代碼對象,本文就是最後一種。參數globals是全局命名空間,用來指定執行語句時可以訪問的全局命名空間;參數locals是局部命名空間,用來指定執行語句時可以訪問的局部作用域的命名空間。按照這個解釋,上述的語句其實是轉化成了這個語法:

import types
var2=types.ModuleType("test")
exec("A=‘bb‘",var2.__dict__)

把配置文件中定義的參數寫入到了定義為config Module類型的變量d的內置屬性__dict__中。
再看看complie函數compile( str, file, type )
compile語句是從type類型(包括’eval’: 配合eval使用,’single’: 配合單一語句的exec使用,’exec’: 配合多語句的exec使用)中將str裏面的語句創建成代碼對象。file是代碼存放的地方,通常為”。compile語句的目的是提供一次性的字節碼編譯,就不用在以後的每次調用中重新進行編譯了。

from_object源碼中將輸入的參數進行類型判斷,如果是object類型的,則說明是通過from_pyfile中傳過來的,只要遍歷from_pyfile傳輸過來的d比變量的內置屬性__dict__即可。如果輸入的string類型,意味著這個是要從默認的config.py文件中導入,用戶需要輸入app.config.from_object("config")進行明確,這時候根據config直接導入config.py配置。

具體的源碼細節如下:

def from_envvar(self, variable_name, silent=False):
    rv = os.environ.get(variable_name)
    if not rv:
        if silent:
            return False
        raise RuntimeError(‘The environment variable %r is not set ‘
                          ‘and as such configuration could not be ‘
                          ‘loaded.  Set this variable and make it ‘
                          ‘point to a configuration file‘ %
                          variable_name)
    return self.from_pyfile(rv, silent=silent)


def from_pyfile(self, filename, silent=False):
    filename = os.path.join(self.root_path, filename)
    d = types.ModuleType(‘config‘)
    d.__file__ = filename
    try:
        with open(filename) as config_file:
            exec (compile(config_file.read(), filename, ‘exec‘), d.__dict__)
    except IOError as e:
        if silent and e.errno in (errno.ENOENT, errno.EISDIR):
            return False
        e.strerror = ‘Unable to load configuration file (%s)‘ % e.strerror
        raise
    self.from_object(d)
    return True


def from_object(self, obj):
    """Updates the values from the given object.  An object can be of one
    of the following two types:

    -  a string: in this case the object with that name will be imported
    -  an actual object reference: that object is used directly

    Objects are usually either modules or classes. :meth:`from_object`
    loads only the uppercase attributes of the module/class. A ``dict``
    object will not work with :meth:`from_object` because the keys of a
    ``dict`` are not attributes of the ``dict`` class.

    Example of module-based configuration::

        app.config.from_object(‘yourapplication.default_config‘)
        from yourapplication import default_config
        app.config.from_object(default_config)

    You should not use this function to load the actual configuration but
    rather configuration defaults.  The actual config should be loaded
    with :meth:`from_pyfile` and ideally from a location not within the
    package because the package might be installed system wide.

    See :ref:`config-dev-prod` for an example of class-based configuration
    using :meth:`from_object`.

    :param obj: an import name or object
    """
    if isinstance(obj, string_types):
        obj = import_string(obj)
    for key in dir(obj):
        if key.isupper():
            self[key] = getattr(obj, key)

根據源碼分析,from_envvarfrom_pyfile兩個函數的輸入配置文件必須是可以執行的py文件,py文件中變量名必須是大寫,只有這樣配置變量參數才能順利的導入到Flask中。

<wiz_tmp_tag id="wiz-table-range-border" contenteditable="false" style="display: none;">

用盡洪荒之力學習Flask源碼