#源碼樣例-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,全稱 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"
如上圖所示,Flask框架包含了與WSGI Server通訊部分和Application自己。Flask Server自己也包含了一個簡單的WSGI Server(這也是爲何運行flask_source.py能夠在瀏覽器訪問的緣由)用以開發測試使用。在實際的生產部署中,咱們將使用apache、nginx+Gunicorn等方式進行部署,以適應性能要求。mysql
下圖是服務器啓動和處理請求的流程圖,本節從分析這個圖開始:
flask的核心組件有兩個Jinjia2和werkzeug。
Jinjia2是一個基於python實現的模板引擎,提供對於HTML的頁面解釋,固然它的功能很是豐富,能夠結合過濾器、集成、變量、流程邏輯支持等做出很是簡單又很酷炫的的web出來。Flask類實例運行會創造一個Jinjia的環境。
在本文使用的樣例中,咱們是直接返回"Hello, world"字符串生成響應,所以本文將不詳細介紹Jinjia2引擎,但不否定Jinjia2對於Flask很是重要也很是有用,值得重點學習。不過在源碼學習中重點看的是werkzeug。linux
werkzeug是基於python實現的WSGI的工具組件庫,提供對HTTP請求和響應的支持,包括HTTP對象封裝、緩存、cookie以及文件上傳等等,而且werkzeug提供了強大的URL路由功能。具體應用到Flask中:nginx
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.",這個函數的工做流程包括:
ctx = self.request_context(environ)
建立請求上下文,並把它推送到棧中,在「上下文」章節咱們會介紹其數據結構。response = self.full_dispatch_request()
處理請求,經過flask的路由尋找對應的視圖函數進行處理,會在下一章介紹這個函數ctx.auto_pop(error)
當前請求退棧。Flask路由的做用是用戶的HTTP請求對應的URL能找到相應的函數進行處理。
@app.route('/')
經過裝飾器的方式爲對應的視圖函數指定URL,能夠一對多,即一個函數對應多個URL。
Flask路由的實現時基於werkzeug的URL Routing功能,所以在分析Flask的源碼以前,首先學習一下werkzeug是如何處理路由的。
werkzeug有兩類數據結構:Map和Rule:
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
,其工做流程以下:
self.url_map.add(rule)
更新url_map,本質是更新werkzeug的url_mapself.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)
講一下這個處理的邏輯:
self.try_trigger_before_first_request_functions()
觸發第一次請求以前須要處理的函數,只會執行一次。self.preprocess_request()
觸發用戶設置的在請求處理以前須要執行的函數,這個能夠經過@app.before_request
來設置,使用的樣例能夠看我以前寫的博文中的示例-11rv = self.dispatch_request()
核心的處理函數,包括了路由的匹配,下面會展開來說rv = self.handle_user_exception(e)
處理異常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)
處理的邏輯以下:
req = _request_ctx_stack.top.request
得到請求對象,並檢查有效性。純粹的上下文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模塊提供的數據結構:
#源碼樣例-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 的效果——多線程或者多協程狀況下全局變量的相互隔離。
一種基於棧的數據結構,其本質是維護了一個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
典型的代理模式實現,在構造時接受一個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的核心機制!關於請求處理流程和上下文的一張圖進行說明:
#源碼樣例-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每次都指向棧的頭元素,而且更新頭元素(若是存在刪除再建立)來確保當前運行的上下文(包括請求上下文和應用上下文)的準確。
在本文第二章節介紹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函數:
wsig_app函數在完成上一小節上下文入棧以後進行請求分發,進行路由匹配尋找視圖函數處理請求,並生成響應,此時用戶能夠在應用程序中import上下文對象做爲全局變量進行訪問:
from flask import request,session,request,g
請求完成後,一樣在源碼樣例-7
wsgi_app函數中能夠看到上下文出棧的操做ctx.auto_pop(error)
,auto_pop函數只彈出請求上下文,應用上下文仍然存在以應對下次的HTTP請求。至此,上下文的管理和操做機制介紹完畢。
接下來繼續學習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來實現,這邊再也不一一贅述。
在本文的源碼樣例-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
返回信息的封裝順序以下:
response = self.make_response(rv)
:根據視圖函數返回值生成response對象。response = self.process_response(response)
:在response發送給WSGI服務器錢對於repsonse進行後續處理,並執行當前請求的後續hooks函數。request_finished.send(self, response=response)
向特定的訂閱者發送響應信息。關於Flask的信號機制能夠學習一下這篇博文,這裏再也不展開詳細說明。make_response
該函數能夠根據不一樣的輸入獲得不一樣的輸出,即參數rv的類型是多樣化的,包括:
源碼樣例-1
所示直接返回str後,將其設置爲body主題後,調用response_class
生成其餘響應信息,例如狀態碼、headers信息。源碼樣例-1
能夠修改成返回return make_response(('hello,world!', 202, None))
,獲得返回碼也就是202,便可以在視圖函數中定義返回狀態碼和返回頭信息。process_response
處理了兩個邏輯:
after_this_request
方法進行執行,同時檢查了是否在blueprint中定義了after_request
和after_app_request
,若是存在,將其放在執行序列;上述的源碼是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的代碼參見教程 這裏就再也不展開分析了。
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源碼中關於配置的部分。能夠發現config
是app
的一個屬性,而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_object
。from_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_envvar
和from_pyfile
兩個函數的輸入配置文件必須是能夠執行的py文件,py文件中變量名必須是大寫,只有這樣配置變量參數才能順利的導入到Flask中。