How can I use an app-factory in Flask / WSGI serve

2019-01-31 04:35发布

问题:

A question on app callables, WSGI servers and Flask circular imports

I am (possibly) confused. I want to safely create Flask / WSGI apps from app-factories and still be able to use them in WSGI servers easily.

tl;dr

  1. Can I safely avoid creating an app on import of init (as recommended)and instead create it later (ie with a factory method)

  2. How do I make that app work neatly with a WSGI server? Especially when I am passing in the config and other settings not pulling them from ENV

For example::

def make_app(configdict, appname):
    app = Flask(appname)
    app.config.update(configdict)
    init_db(configdict)
    set_app_in_global_namespace(app)

    #importing now will allow from pkg import app        
    from mypackage import views

    return app

I would like to use the above "factory", because I want to easily contorl config for testing etc.

I then presumably want to create a wsgi.py module that provides the app to a WSGI server.

So eventually things look a bit like this

init.py::

app = None

def make_app(configdict, appname):
    flaskapp = Flask(appname)
    flaskapp.config.update(configdict)
    init_db(configdict)

    global app
    app = flaskapp    

    #importing now will allow from pkg import app        
    from mypackage import views

    return flaskapp

wsgi.py::

from mypackage import app

app = make_app(configfromsomewhere, "myname")

uWSGI::

uwsgi --module=mypackage.wsgi:app

But still wsgi.py is NOT something I can call like wsgi.py --settings=x --host=10.0.0.1 So I don't really know how to pass the config in.

I am asking because while this seems ... OK ... it also is a bit messy.

Life was easier when everything was in the ENV.

And not only but also:

So what is unsafe about using an app-factory

The advice given here <http://flask.pocoo.org/docs/patterns/packages>_ is ::

1. the Flask application object creation has to be in the
__init__.py file. That way each module can import it safely and
the __name__ variable will resolve to the correct package.

2. all the view functions (the ones with a route() decorator on
  top) have to be imported in the __init__.py file. Not the object
  itself, but the module it is in. Import the view module after
  the application object is created.

re: 2., clearly the route decorator expects certain abilities from an instantiated app and cannot function without them. Thats fine.

re: 1., OK we need the name correct. But what is unsafe ? And why? Is it unsafe to import and use the app if it is uninitialised? Well it will break but thats not unsafe. Is it the much vaunted thread-local? Possibly. But if I am plucking app instances willy-nilly from random modules I should expect trouble.

Implications - we do not reference the app object from anything other than the views - essentially we keep our modularisation nice and tight, and pass around dicts, error objects, or even WebObs.

http://flask.pocoo.org/docs/patterns/appdispatch http://flask.pocoo.org/docs/deploying/#deployment http://flask.pocoo.org/docs/patterns/packages/#larger-applications http://flask.pocoo.org/docs/becomingbig

回答1:

According to the Flask Documentation, an application factory is good because:

  1. Testing. You can have instances of the application with different settings to test every case.

  2. Multiple instances. Imagine you want to run different versions of the same application. Of course you could have multiple instances with different configs set up in your webserver, but if you use factories, you can have multiple instances of the same application running in the same application process which can be handy.

But, as is stated in the Other Testing Tricks section of the documentation, if you're using application factories the functions before_request() and after_request() will be not automatically called.

In the next paragraphs I will show how I've been using the application factory pattern with the uWSGI application server and nginx (I've only used those, but I can try to help you configure it with another server).

The Application Factory

So, let's say you have your application inside the folder yourapplication and inside it there's the __init__.py file:

import os
from flask import Flask

def create_app(cfg=None):
    app = Flask(__name__)

    load_config(app, cfg)

    # import all route modules
    # and register blueprints

    return app

def load_config(app, cfg):
    # Load a default configuration file
    app.config.from_pyfile('config/default.cfg')

    # If cfg is empty try to load config file from environment variable
    if cfg is None and 'YOURAPPLICATION_CFG' in os.environ:
        cfg = os.environ['YOURAPPLICATION_CFG']

    if cfg is not None:
        app.config.from_pyfile(cfg)

Now you need a file to create an instance of the app:

from yourapplication import create_app

app = create_app()

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

In the code above I'm assuming there's an environment variable set with the path to the config file, but you could give the config path to the factory, like this:

app = create_app('config/prod.cfg')

Alternatively, you could have something like a dictionary with environments and corresponding config files:

CONFIG_FILES = {'development': 'config/development.cfg',
                'test'       : 'config/test.cfg',
                'production' : 'config/production.cfg' }

In this case, the load_config function would look like this:

def load_config(app, env):
    app.config.from_pyfile('config/default.cfg')

    var = "YOURAPPLICATION_ENV"
    if env is None and var in os.environ:
        env = os.environ[var]

    if env in CONFIG_FILES:
        app.config.from_pyfile(CONFIG_FILES[env])

Nginx and uWSGI

Here's an example of a configuration file for nginx:

server {
    listen             80;
    server_name        yourapplication.com;
    access_log         /var/www/yourapplication/logs/access.log;
    error_log          /var/www/yourapplication/logs/error.log;

    location / {
        try_files $uri @flask;
    }

    location @flask {
        include        uwsgi_params;
        uwsgi_pass     unix:/tmp/yourapplication.sock;

        # /env is the virtualenv directory
        uwsgi_param    UWSGI_PYHOME                /var/www/yourapplication/env;

        # the path where the module run is located
        uwsgi_param    UWSGI_CHDIR                 /var/www/yourapplication;

        # the name of the module to be called
        uwsgi_param    UWSGI_MODULE                run;

        # the variable declared in the run module, an instance of Flask
        uwsgi_param    UWSGI_CALLABLE              app;
    }
}

And the uWSGI configuration file looks like this:

[uwsgi]
plugins=python
vhost=true
socket=/tmp/yourapplication.sock
env = YOURAPPLICATION_ENV=production
logto = /var/www/yourapplication/logs/uwsgi.log

How to use before_request() and after_request()

The problem with those functions is that if your are calling them in other modules, those modules cannot be imported before the application has been instantiated. Again, the documentation has something to say about that:

The downside is that you cannot use the application object in the blueprints at import time. You can however use it from within a request. How do you get access to the application with the config? Use current_app:

from flask import current_app, Blueprint, render_template
admin = Blueprint('admin', __name__, url_prefix='/admin')

@admin.route('/')
def index():
    return render_template(current_app.config['INDEX_TEMPLATE'])

Or you could consider creating an extension, then you could import the class without any existent instances of Flask, as the class extension would only use the Flask instance after being created.