用盡洪荒之力學習Flask源碼


一直想作源碼閱讀這件事,總感受難度過高時間太少,可望不可見。最近正好時間充裕,決定試試作一下,並記錄一下學習心得。
首先說明一下,本文研究的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的工做原理及代碼框架。html

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 處理完以後須要調用的函數,參數是狀態碼、響應頭部還有錯誤信息。引用代碼示例:python

#源碼樣例-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"

The Python WSGI server-application interface.
如上圖所示,Flask框架包含了與WSGI Server通訊部分和Application自己。Flask Server自己也包含了一個簡單的WSGI Server(這也是爲何運行flask_source.py能夠在瀏覽器訪問的緣由)用以開發測試使用。在實際的生產部署中,咱們將使用apache、nginx+Gunicorn等方式進行部署,以適應性能要求。mysql

app.run()

下圖是服務器啓動和處理請求的流程圖,本節從分析這個圖開始:

flask的核心組件有兩個Jinjia2和werkzeug。
Jinjia2是一個基於python實現的模板引擎,提供對於HTML的頁面解釋,固然它的功能很是豐富,能夠結合過濾器、集成、變量、流程邏輯支持等做出很是簡單又很酷炫的的web出來。Flask類實例運行會創造一個Jinjia的環境。
在本文使用的樣例中,咱們是直接返回"Hello, world"字符串生成響應,所以本文將不詳細介紹Jinjia2引擎,但不否定Jinjia2對於Flask很是重要也很是有用,值得重點學習。不過在源碼學習中重點看的是werkzeug。linux

werkzeug

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

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

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

#源碼樣例-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調用。sql

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

#源碼樣例-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__開始,代碼以下:apache

#源碼樣例-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有兩類數據結構:MapRule:

  • 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類對於傳入的參數進行包裝,其源碼如樣例22:

#源碼樣例-22
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經常使用的四種方式進行項目參數的配置,如樣例-22所示:

#源碼樣例-23
#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://user@localhost/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前臺,舉例說明:

#源碼樣例-24
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應該是一個字典類型的類屬性變量,這一點在源碼中驗證了:

#源碼樣例-25
#: 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函數的定義:

#源碼樣例-26
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類的定義:

#源碼樣例-27
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中定義爲:

#源碼樣例-28
#: 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是局部命名空間,用來指定執行語句時能夠訪問的局部做用域的命名空間。按照這個解釋,上述的語句實際上是轉化成了這個語法:

#源碼樣例-29
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配置。

具體的源碼細節以下:

#源碼樣例-30
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中。

相關文章
相關標籤/搜索