Skip to content

Latest commit

 

History

History
781 lines (538 loc) · 49.9 KB

flask.md

File metadata and controls

781 lines (538 loc) · 49.9 KB

Flask

跟 Werkzeug 的關係

跟 Jinja2 的關係

Hello, World! ??

hello.py:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, World!'
$ FLASK_APP=hello.py FLASK_DEBUG=1 flask run
$ curl http://127.0.0.1:5000/
Hello, World!

若採用 package 的話 (hello/__init__.py),Flask 1.0 前要用:

$ FLASK_APP=hello/__init__.py FLASK_DEBUG=1 flask run

$ FLASK_APP=hello FLASK_DEBUG=1 python -m flask run

參考資料:

新手上路 {: #getting-started }

  • 用 virtualenv 安裝 Flask 套件。
  • 跟著 Qucikstart 做過一遍。

參考資料:

Routing ??

  • 什麼是 path converter?? 可以用在 rule 中間嗎? 又怎麼沒有 bool converter??

參考資料:

Context Local ??

  • Context Locals - Quickstart — Flask 1.0.2 documentation

    • Insider Information: If you want to understand how that works and how you can implement TESTS with context locals, read this section, otherwise just skip it. 跟測試有關??

    • Certain objects in Flask are GLOBAL objects, but not of the usual kind. These objects are actually PROXIES to objects that are LOCAL TO A SPECIFIC CONTEXT. What a mouthful. But that is actually quite easy to understand.

    • Imagine the context being the HANDLING THREAD. A request comes in and the web server decides to spawn a new thread (or something else, the underlying object is capable of dealing with concurrency systems other than threads). When Flask starts its internal request handling it figures out that the current thread is the active context and BINDS the current application and the WSGI environments to that context (thread). It does that in an intelligent way so that one application can INVOKE ANOTHER APPLICATION?? without breaking.

    • So what does this mean to you? Basically you can completely ignore that this is the case unless you are doing something like UNIT TESTING. You will notice that code which depends on a request object will suddenly break because there is no request object. The solution is creating a request object yourself and binding it to the context. The easiest solution for unit testing is to use the test_request_context() context manager. In combination with the with statement it will bind a test request so that you can interact with it. Here is an example:

      from flask import request
      
      with app.test_request_context('/hello', method='POST'):
          # now you can do something with the request until the
          # end of the with block, such as basic assertions:
          assert request.path == '/hello'
          assert request.method == 'POST'
      

      這跟 Flask.test_client() 是什麼關係?? 好像可以不用從 routing 這一層測試? 不過話說回來,離開 routing 這一層還跟 Flask 有相依,也是個問題...

    • The other possibility is passing a whole WSGI environment to the request_context() method: 直接模擬環境變數??

      from flask import request
      
      with app.request_context(environ):
          assert request.method == 'POST'
      
  • Context Locals — Werkzeug Documentation (0.14) #ril

Request ??

  • Quickstart — Flask 1.0.2 documentation

    • For web applications it’s crucial to react to the data a client sends to the server. In Flask this information is provided by the GLOBAL request object. If you have some experience with Python you might be wondering how that object can be global and how Flask manages to still be THREADSAFE. The answer is CONTEXT LOCALS:

    The Request Object

    • The request object is documented in the API section and we will not cover it here in detail (see Request). Here is a broad overview of some of the most common operations. First of all you have to import it from the flask module: from flask import request

    • The current request method is available by using the method attribute. To access form data (data transmitted in a POST or PUT request) you can use the form attribute. Here is a full example of the two attributes mentioned above:

      @app.route('/login', methods=['POST', 'GET'])
      def login():
          error = None
          if request.method == 'POST':
              if valid_login(request.form['username'],
                             request.form['password']):
                  return log_the_user_in(request.form['username']) # 裡面應該會實現 Redirect After Post
              else:
                  error = 'Invalid username/password'
          # the code below is executed if the request method
          # was GET or the credentials were invalid
          return render_template('login.html', error=error)
      
    • What happens if the key does not exist in the form attribute? In that case a special KeyError is raised. You can catch it like a standard KeyError but if you don’t do that, a HTTP 400 Bad Request error page is shown instead. So for many situations you DON’T have to deal with that problem.

    • To access parameters submitted in the URL (?key=value) you can use the args attribute:

      searchword = request.args.get('key', '')
      
    • We recommend accessing URL parameters with get or by catching the KeyError because users might change the URL and presenting them a 400 bad request page in that case is NOT USER FRIENDLY.

      還是得看情況吧,如果必要的參數沒拿到,又沒有合理的預設值,回 400 也是很合理的?

    • For a full list of methods and attributes of the request object, head over to the Request documentation.

  • The Request Context — Flask 1.0.2 documentation #ril

    • The request context keeps track of the REQUEST-LEVEL data during a request. Rather than passing the request object to each function that runs during a request, the request and session PROXIES are accessed instead.

    • This is similar to the The Application Context, which keeps track of the APPLICATION-LEVEL data independent of a request. A corresponding application context is PUSHED when a request context is pushed.

      關於 push/pop, context 要先看過下面 How the Context Works 才會懂,簡單的講 requestsession 這些 proxy object 都參照 request/application context stack 最上方的 context 來決定 current request/application,所以 push/pop 的動作很關鍵。

    Purpose of the Context

    • When the Flask application handles a request, it creates a Request object based on the environment it received from the WSGI server. Because a WORKER (thread, process, or COROUTINE depending on the server) handles only one request at a time, the request data can be considered GLOBAL TO THAT WORKER during that request. Flask uses the term CONTEXT LOCAL for this.

      原來 worker 是這麼抽象,背後可能有不同的實作方式,但 worker 同時間只會面對一個 reqeust,這一點是肯定的。

    • Flask automatically pushes a request context when handling a request. View functions, error handlers, and other functions that run during a request will have access to the request proxy, which POINTS TO the request object for the current request.

    Lifetime of the Context

    • When a Flask application begins handling a request, it pushes a request context, which also pushes an The Application Context. When the request ends it pops the request context then the application context.
    • The context is unique to each thread (or other worker type). request cannot be passed to another thread, the other thread will have a different CONTEXT STACK and will not know about the request the PARENT THREAD?? was pointing to.
    • Context locals are implemented in Werkzeug. See Context Locals for more information on how this works internally. 已經有 context,為何又要強調 local??

    ...

    How the Context Works

    • The Flask.wsgi_app() method is called to handle each request. It manages the contexts during the request. Internally, the request and application contexts work as STACKS, _request_ctx_stack and _app_ctx_stack. When contexts are PUSHED onto the stack, the proxies that depend on them are available and point at information from the TOP CONTEXT on the stack.

      難怪會有 push/pop 的說法,而且 request context 跟 application context 是兩個不同的 stack。

    • When the request starts, a RequestContext is created and pushed, which creates and pushes an AppContext FIRST if a context for that application is not already the top context. While these contexts are pushed, the current_app, g, request, and session proxies are available to the ORIGINAL THREAD handling the request.

    • After the request is dispatched and a response is generated and sent, the request context is popped, which then pops the application context. Immediately before they are popped, the teardown_request() and teardown_appcontext() functions are are executed. These execute EVEN IF an unhandled exception occurred during dispatch. 這 2 個 callback 都在 flask.Flask 上。

  • flask.request - API — Flask 1.0.2 documentation

    • To access incoming request data, you can use the GLOBAL request object. Flask parses incoming request data for you and gives you access to it through that global object. Internally Flask makes sure that you always get the correct data for the ACTIVE THREAD if you are in a multithreaded environment.

      This is a PROXY (werkzeug.local.LocalProxy). See Notes On Proxies for more information.

    • The request object is an instance of a Request subclass and provides all of the attributes Werkzeug defines. This just shows a quick overview of the most important ones.

  • class flask.Request - API — Flask 1.0.2 documentation #ril

    • The request object used by default in Flask. Remembers the MATCHED ENDPOINT and VIEW ARGUMENTS ??.

      It is what ends up as flask.request. If you want to replace the request object used you can subclass this and set Flask.request_class to your subclass.

    • The request object is a Request subclass and provides all of the attributes Werkzeug defines PLUS A FEW Flask SPECIFIC ONES.

      werkzeug.wrappers.Request 繼承自 werkzeug.wrappers.BaseRequest

    environ

    • The underlying WSGI environment.

    path, full_path, script_root, url, base_url, url_root

    • Provides different ways to look at the current IRI. Imagine your application is listening on the following application root:

      http://www.example.com/myapplication
      

      And a user requests the following URI:

      http://www.example.com/myapplication/%CF%80/page.html?x=y
      

      In this case the values of the above mentioned attributes would be the following:

      path=u'/π/page.html'
      full_path=u'/π/page.html?x=y'
      script_root=u'/myapplication'
      base_url=u'http://www.example.com/myapplication/π/page.html'
      url=u'http://www.example.com/myapplication/π/page.html?x=y'
      url_root=u'http://www.example.com/myapplication/'
      

    args

    • The parsed URL parameters (the part in the URL after the question mark).

      也就是 query string。

    • By default an ImmutableMultiDict is returned from this function. This can be changed by setting parameter_storage_class to a different type. This might be necessary if the ORDER of the form data is important.

    content_encoding

    • The Content-Encoding entity-header field is used as a modifier to the media-type. When present, its value indicates what additional content codings have been applied to the entity-body, and thus what decoding mechanisms must be applied in order to obtain the media-type referenced by the Content-Type header field. ??

    content_length

    • The Content-Length entity-header field indicates the size of the entity-body in bytes or, in the case of the HEAD method, the size of the entity-body that would have been sent had the request been a GET. ??

    content_type

    • The Content-Type entity-header field indicates the media type of the entity-body sent to the recipient or, in the case of the HEAD method, the media type that would have been sent had the request been a GET.

    cookies

    • A dict with the contents of all cookies transmitted with the request.

    data

    • Contains the incoming request data as string in case it came with a mimetype Werkzeug DOES NOT HANDLE.

      也就是說,如果 Werkzeug 認得 content type,data 就會是空的? 這聽起來很不直覺,得從實作細節看起;BeseRequest.data:

      def data(self):
          if self.disable_data_descriptor:
              raise AttributeError("data descriptor is disabled")
          return self.get_data(parse_form_data=True)
      

      對照下面 get_data() 的實作,就算 cache 預設為 True,但因為 parse_form_data=True 先讀取並解析了 input stream,所以 rv = self.stream.read() 就讀不到東西了。

      也因此 data 通常都拿不到東西,若真的想拿原始的 data,得在解析 form data 之前先呼叫 get_data() 並把資料快取起來,之後解析 form data 時才有資料。簡單做個時驗,看 request.formrequest.get_data()request.data 會造成什麼影響:

      @app.route('/', methods=['POST'])
      def post():
          logging.info('request.get_data() = %r', request.get_data())
          logging.info('request.data = %r', request.data)
          logging.info('request.form = %r', request.form)
      

      curl --data 'key=value' http://... 會得到:

      root - request.get_data() = 'key=val'
      root - request.data = 'key=val'
      root - request.form = ImmutableMultiDict([('key', u'val')])
      

      request.form 放到最前面:

      root - request.form = ImmutableMultiDict([('key', u'val')])
      root - request.get_data() = ''
      root - request.data = ''
      

      request.data 放到最前面,結果最詭異: (看過實作細節才能理解)

      root - request.data = ''
      root - request.get_data() = ''
      root - request.form = ImmutableMultiDict([('key', u'val')])
      
    • The form parameters. By default an ImmutableMultiDict is returned from this function. This can be changed by setting parameter_storage_class to a different type. This might be necessary if the order of the form data is important.

    • Please keep in mind that file uploads will not end up here, but instead in the files attribute.

    get_data(cache=True, as_text=False, parse_form_data=False)

    • This reads the BUFFERED INCOMING DATA from the client into one BYTESTRING. By default this is CACHED but that behavior can be changed by setting cache to False.

      根據 BaseRequest.get_data() 的實作,從 input stream 讀進來的值會快取在 _cached_data 裡,之後存取 BaseRequest.form 時會[觸發 _load_form_data()](https://github.com/pallets/werkzeug/blob/0.15.x/src/werkzeug/wrappers/base_request.py#L479),就會[間接從 _get_stream_for_parsing()](https://github.com/pallets/werkzeug/blob/0.15.x/src/werkzeug/wrappers/base_request.py#L318) 取得 _cached_data`。

      def get_data(self, cache=True, as_text=False, parse_form_data=False):
          rv = getattr(self, "_cached_data", None)
          if rv is None:
              if parse_form_data:
                  self._load_form_data() # 除非有 _cached_data,否則會從 self.stream 讀資料
              rv = self.stream.read()    # 如果做了 _load_form_data(),stream 裡就沒資料可讀了
              if cache:
                  self._cached_data = rv # 快取的資料也會是空的
          if as_text:
              rv = rv.decode(self.charset, self.encoding_errors)
          return rv
      
      def _get_stream_for_parsing(self):
          cached_data = getattr(self, "_cached_data", None)
          if cached_data is not None:
              return BytesIO(cached_data)
          return self.stream
      
    • Usually it’s a bad idea to call this method without checking the content length first as a client could send dozens of megabytes or more to cause memory problems on the server.

      聽起來是前端 web server 的責任 ??

    • Note that if the FORM DATA WAS ALREADY PARSED this method will not return anything as form data parsing does not cache the data like this method does.

      To implicitly invoke form data parsing function set parse_form_data to True. When this is done the return value of this method will be an EMPTY STRING if the form parser handles the data. This generally is not necessary as if the whole data is cached (which is the default) the form parser will used the cached data to parse the form data. Please be generally aware of checking the content length first in any case before calling this method to avoid exhausting server memory.

    • If as_text is set to True the return value will be a decoded UNICODE string.

    get_json(force=False, silent=False, cache=True)

    • Parse and return the data as JSON. If the mimetype does not indicate JSON (application/json, see is_json()), this returns None unless force is true. If parsing fails, on_json_loading_failed() is called and its return value is used as the return value.

    • Parameters:

      • force – Ignore the mimetype and always try to parse JSON.
      • silent – Silence parsing errors and return None instead.
      • cache – Store the parsed JSON to return for subsequent calls.

    headers

    • The headers from the WSGI environ as immutable EnvironHeaders.

    json

    • This will contain the parsed JSON data if the mimetype indicates JSON (application/json, see is_json()), otherwise it will be None.

      這很依賴 client 是否有帶正確的 content type,如果要無條件視為 JSON 則要改用 get_json(force=True)

    mimetype

    • Like content_type, but without PARAMETERS (eg, without charset, type etc.) and always LOWERCASE. For example if the content type is text/HTML; charset=utf-8 the mimetype would be 'text/html'.

    mimetype_params

    • The mimetype parameters as dict. For example if the content type is text/html; charset=utf-8 the params would be {'charset': 'utf-8'}.

    stream

    • If the incoming form data was not encoded with a known mimetype the data is stored unmodified in this stream for consumption. Most of the time it is a better idea to use data which will give you that data as a string. THE STREAM ONLY RETURNS THE DATA ONCE.

    • Unlike input_stream this stream is properly guarded that you can’t accidentally read past the length of the input. Werkzeug will internally always refer to this stream to read data which makes it possible to wrap this object with a stream that does filtering. ??

    values

    • A werkzeug.datastructures.CombinedMultiDict that combines args and form.

      用在資料從哪裡來不重要時。

Response

  • View function 直接 return,不傳回任何東西,就是單純的 200 OK 嗎? 還是用 abort(200) 比較好?
    • 單純的 return (同 return None) 會引發 ValueError: View function did not return a response 的錯誤。
    • abort(200) 會引發 LookupError: no exception for 200 的錯誤。
    • 目前似乎只能回 return '' ??
  • Flask 裡怎麼先回應一些訊息,之後再透過 callback URL 再回一些?? 就算能把工作交出去,那之後結果要怎麼更新到前端??

Redirection ??

Session ??

RESTful

Application Context ??

Proxy Object ??

Development Server ??

Debugger ??

CLI (flask) ??

Template ??

Forms

參考資料:

  • Form Validation with WTForms — Flask Documentation (0.12)

    • 為了處理 form data,很快就會讓 code 變得很難讀,WTForms 就是在解決這個問題;另外 feature 了 Flask-WTF

    • 首先要為 form 定義一個 class,不過這裡 "adding a separate module for the forms" 的建議似乎怪怪的?

    • Form 在 view function 裡用起來像是:

      @app.route('/register', methods=['GET', 'POST'])
      def register():
          form = RegistrationForm(request.form)
          if request.method == 'POST' and form.validate():
              user = User(form.username.data, form.email.data,
                          form.password.data)
              db_session.add(user)
              flash('Thanks for registering')
              return redirect(url_for('login'))
          return render_template('register.html', form=form)
      
    • 無論如何都將 request.form 傳入 form class (當 form 是 GET 送出時,要改用 request.args);當 method 是 POST 時做驗證,是 GET (或驗證失敗時) 就拿去 render。

    • 驗證用 validate() (搭配 request.method == 'POST' 的檢查,validate_on_submit()是 Flask-WTF 加的),取得個別欄位的資料用form..data

    • 驗證成功會搭配 redirect() 轉向,若沒有成功則重新 render 一次;從這個角度來看,form 的 action 應該指向自己會比較好? 否則驗證失敗時 URL 會揭露一些原本沒打算讓 end user 看到的資訊,也可能被 bookmark??

    • 在 template 裡是用 field function (form.()`) 畫出 form element。

  • Rendering - WTForms Documentation To render a field, simply call it, providing any values the widget expects as keyword arguments. 好特別的設定,通常 keyword arguments 對應到其他的 HTML attributes

Shell??

flask run 取代 Flask.run() ??

如何打包成 Docker image??

如何用 uWSGI 啟動 app?

  • uWSGI 是 Flask/WSGI application 跟 full-featured web server (例如 nginx、lighttpd 等) 介接的方式之一,uWSGI server 跟 web server 間是走 uwsgi protocol (小寫),在 Unix-like 上通常透過 socket 溝通;uWSGI server 會調用 application module/package 的 WSGI callable (Flask)。

    uwsgi --socket /tmp/yourapplication.sock --virtualenv venv \
          --manage-script-name --mount /path=yourapplication:app \
          --workers 4 --threads 4
    
    location = /yourapplication { rewrite ^ /yourapplication/; }
    location /yourapplication { try_files $uri @yourapplication; }
    location @yourapplication {
      include uwsgi_params;
      uwsgi_pass unix:/tmp/yourapplication.sock;
    }
    
  • 在 staging 環境,則可以搭配 FLASK_DEBUG=1--catch-exceptions 印出 backtrace:(若要在 uWSGI 下啟用 interactive debugger,要另外處理,跟 --catch-exceptions 無關)

    FLASK_DEBUG=1 \
    uwsgi --socket /tmp/yourapplication.sock --virtualenv venv \
          --manage-script-name --mount /path=yourapplication:app \
          --workers 4 --threads 4 \
          --catch-exceptions
    
  • 在本地端開發,也可以用 uWSGI 來模擬 staging/production 的環境,例如:(注意 --no-default-app 可以避免 app 在 / 服務,跟 staging/production 不同,可能無法及早發覺某些錯誤)

    FLASK_DEBUG=1 \
    uwsgi --http 127.0.0.1:5000 --virtualenv venv \
          --manage-script-name --mount /path=yourapplication:app \
          --workers 4 --threads 4 \
          --catch-exceptions --no-default-app
    

參考資料:

大型應用程式用 application package 比較好?

  • 用 application package (而非 application module),底下可以拆分成多個 module;不過 FLASK_APP=package_name 的用法,必須要 "安裝" 過才行,還好 pip install -e . (搭配 setup.py) 可以安裝成 edit mode,開發時期修改程式會自動 reload 不成問題。
  • 但用一個 startup module 帶出 package 不也是一樣? 而且不用安裝,搭配 blueprint 一樣可以把 route 拆到不同的 component。

參考資料:

疑難排解 {: #troubleshooting }

Error: The file/path provided (xxx) does not appear to exist.

Flask 0.11 後,執行 development server 建議執行 flask run,搭配 FLASK_APP 指向 Python module 以取得 application object。

$ FLASK_APP=myapp flask run # 假如單純是個 module
$ FLASK_APP=myapp flask run # 假如複雜一點是個 package (myapp/__init__.py)

第二種狀況會遇到下面的錯誤:

Error: The file/path provided (myapp) does not appear to exist.  Please verify the path is correct.  If app is not on PYTHONPATH, ensure the extension is .py

雖然 Larger Applications 說採用 package 時要用 pip install --editable . 安裝過才行,但追查過 flask/cli.py,發現 FLASK_APP=myapp/__init__.py flask run 的寫法也可以,或是改用 FLASK_APP=myapp python -m flask run

這項限制已在 PR #2414 處理掉,但要 Flask 1.0 才會釋出。

參考資料:

$ flask --version
Flask 0.12.2
Python 2.7.10 (default, Jul 15 2017, 17:16:57)
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.31)]

bin/flask:

# -*- coding: utf-8 -*-
import re
import sys

from flask.cli import main

if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(main())

lib/python2.7/site-packages/flask/cli.py:

def prepare_exec_for_file(filename):
    """Given a filename this will try to calculate the python path, add it
    to the search path and return the actual module name that is expected.
    """
    module = []

    # Chop off file extensions or package markers 原來也接受 package/__init__.py 的寫法
    if os.path.split(filename)[1] == '__init__.py':
        filename = os.path.dirname(filename)
    elif filename.endswith('.py'):
        filename = filename[:-3]
    else:
        raise NoAppException('The file provided (%s) does exist but is not a '
                             'valid Python file.  This means that it cannot '
                             'be used as application.  Please change the '
                             'extension to .py' % filename)
    filename = os.path.realpath(filename)

    # 往上找到沒有 `__init__.py` 的那一層
    dirpath = filename
    while 1:
        dirpath, extra = os.path.split(dirpath)
        module.append(extra)
        if not os.path.isfile(os.path.join(dirpath, '__init__.py')):
            break

    sys.path.insert(0, dirpath) # 調整 sys.path
    return '.'.join(module[::-1])

def locate_app(app_id):
    """Attempts to locate the application."""
    __traceback_hide__ = True
    if ':' in app_id:
        module, app_obj = app_id.split(':', 1)
    else:
        module = app_id
        app_obj = None

    # 這行可以看出 flask run 與 python -m flask run 的差異
    print "app_id = %r, cwd = %r, sys.path[:2] = %r" % (app_id, os.getcwd(), sys.path[:2])

    try:
        __import__(module)
    except ImportError:
        # Reraise the ImportError if it occurred within the imported module.
        # Determine this by checking whether the trace has a depth > 1.
        if sys.exc_info()[-1].tb_next:
            raise
        else:
            raise NoAppException('The file/path provided (%s) does not appear'
                                 ' to exist.  Please verify the path is '
                                 'correct.  If app is not on PYTHONPATH, '
                                 'ensure the extension is .py' % module)


    mod = sys.modules[module]
    if app_obj is None:
        app = find_best_app(mod) # 找 app 或 application 或型態為 Flask 者
    else:
        app = getattr(mod, app_obj, None)
        if app is None:
            raise RuntimeError('Failed to find application in module "%s"'
                               % module)

    return app

def find_default_import_path():
    app = os.environ.get('FLASK_APP')
    if app is None:
        return
    if os.path.isfile(app): # 是檔案才會調整 sys.path
        return prepare_exec_for_file(app)
    return app # 直接視為 module name
$ FLASK_APP=myapp flask run
app_id = 'myapp', cwd = '/Users/jeremykao/data/work/learning/flask/testing', sys.path[:2] = ['/Users/jeremykao/data/work/learning/flask/testing/venv-flask-testing/bin', '/Users/jeremykao/data/work/learning/flask/testing/venv-flask-testing/lib/python27.zip']
Usage: flask run [OPTIONS]

Error: The file/path provided (myapp) does not appear to exist.  Please verify the path is correct.  If app is not on PYTHONPATH, ensure the extension is .py

$ FLASK_APP=myapp python -m flask run
app_id = 'myapp', cwd = '/Users/jeremykao/data/work/learning/flask/testing', sys.path[:2] = ['', '/Users/jeremykao/data/work/learning/flask/testing/venv-flask-testing/lib/python27.zip']
  - Serving Flask app "myapp"
  - Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

flask runpython -m flask run 的差別在於 sys.path[0] 不同,不知道為什麼 sys.path[0] 會指向 flask 指令所在的目錄?

如何在 uWSGI 下啟用 interactive debugger?

  • FLASK_DEBUG=1 flask run 發生錯誤時會調出 interactive debugger,但同樣的 app 用 uwsgi --module myapp:app 啟用時,發生錯誤只會有 Internal Server Error? 大概是因為 flask run 會參考 FLASK_DEBUGapp.debug 設為 True,並啟用 interactive debugger 的關係?
  • uwsgi --catch-exceptions 搭配 FLASK_DEBUG=1 (或在程式裡明確做 app.debug = True) 確實可以在出錯時看到 traeback,但卻沒有 interactive runner,猜想 Flask instance 內部會參考 FLASK_DEBUG 要不要寫出 traceback,但 interactive debugger 則是 flask run 啟用的。
  • 在程式裡加上下面的程式,並用 UWSGI_DEBUG 控制 (避免使用 flask run 時套用兩層 debugger),就可以在 staging 環境用 UWSGI_DEBUG=1 uwsgi ... 啟用 app,出問題時可以直接 debug。
if os.getenv('UWSGI_DEBUG'):
    app.debug = True
    from werkzeug.debug import DebuggedApplication
    app.wsgi_app = DebuggedApplication(app.wsgi_app, evalex=True, pin_security=False)

參考資料:

  • python - Flask debug=True does not work when going through uWSGI - Stack Overflow https://stackoverflow.com/questions/10364854/ #ril

    • Edwardr: cannot use Flask's debug option with uWSGI, because it's not to be used in a forking environment. 呼應 Quick Start 裡 "Even though the interactive debugger does not work in forking environments (which makes it nearly impossible to use on production servers)" 的說法;在 uWSGI 裡可以用 --catch-exceptions 模擬 debugger? (只是印出 traceback)

    • gonz: 那是 "werkzeug error page",所以要用 DebuggedApplication middleware,另外 GaretJax 提到必須要用 single worker (--workers 1)? 試過多個 worker 倒是沒什麼問題

      from werkzeug.debug import DebuggedApplication
      app.wsgi_app = DebuggedApplication(app.wsgi_app, True)
      
  • Dubugging Flask applications under uWSGI (2012-10-28) https://codeseekah.com/2012/10/28/dubugging-flask-applications-under-uwsgi/ 不懂為什麼 Flask 的 debugger 在 uWSGI 下不能用? 這裡用 if app.debug: 來判斷要不要加上 DebuggedApplication,不過這仍會造成用 flask run 執行時有兩層 debugger。

  • web application - Flask debug=True exploitation - Information Security Stack Exchange https://security.stackexchange.com/questions/140677/ 出現 DebuggedApplication(app, evalex=True, pin_security=False) 的用法,原來 PIN 是可以關閉的。

  • Quickstart — Flask Documentation (0.12) http://flask.pocoo.org/docs/0.12/quickstart/ Even though the interactive debugger does not work in forking environments (which makes it nearly impossible to use on production servers) 好像有這麼回事? 上面 uWSGI 加上 DebuggedApplication 的做法,有時帶出的 debugger 是不太正常,但大部份時候又能作用 ...

如何將所有錯誤都導到 log 裡?

參考資料:

如何用 pdb 為 Flask app 除錯?

JSON

其他

manage.py 的作用是什麼?

如何實作 DB schema migration?

參考資料:

如何測試 Flask 應用程式?

安裝設置 {: #setup }

  • 建議用 virtualenv 安裝 Flask 套件,安裝完成有 flask 指令可以用。

參考資料:

  • Welcome | Flask (A Python Microframework) http://flask.pocoo.org/ pip install Flask 即可安裝。
  • Installation — Flask 1.0.2 documentation
    • 支援 Python 3.4+, Python 2.7 及 PyPy;建議採最新版的 Python 3
    • 會自動安裝 Werkzeug、Jinja、ItsDangerous 及 Clik,另外可以視需求安裝 Blinker、SimpleJSON、python-dotenv 及 Watchdog。
    • Werkzeug 實作了 WSGI,做為 application 與 web server 間的標準 Python interface。
    • Jinja 是一種 template language,用來畫 (render) web pages;Jinja 會帶出 MarkupSafe -- 將 untrusted input 做 escape,避免 injection 攻擊。
    • ItsDangerous 可以做數位簽章 (signing),Flask 用它來確保 session cookie 沒有被動過手腳 (integrity)。
    • Click 可以用來寫 CLI,它提供了 flask command,還可以自訂其他 management command
    • Blinker 讓 Flask 支援 signal 的概念 ??
    • SimpleJSON 相容於 Python 內建的 json module,但是更快;如果有安裝的話,Flask 會優先採用它
    • python-dotenv 讓 flask command 支援 dotenv (.env)
    • Watchdog 提供更好的 reloader 讓 development server 使用。
    • 在 dev/prod 都建議用 virtual environment 管理 dependencies;Python 3 內建 venv module,Python 2 則要加裝 virtualenv 套件。
  • Installation — Flask 0.12.4 documentation 支援 Python 2.6+ 與 Python 3,最快的方法 (kick-ass method) 是用 virtualenv。

參考資料 {: #reference }

書籍:

更多:

手冊: