Flask Application Factories
I was running some tests on a Flask application recently and ran into some weird errors.
I was using Flask-WTF to create a form with a select box that was populated from the database.
I was using Flask-Script to run my tests but because I had included
app in my
manage.py file the app was being initalised before any of my test scripts did anything like set configuration values for my tests. So either the database wasn't there to populate the select box, or if it was, I'd get a database error when the test scripts tried to repopulate that table that contained unique database constraints.
After much head-scratching and a post on StackOverflow I realised the solution was to use Application Factories.
Where I was once creating the app in the application's
__init__.py like so:
from flask import Flask app = Flask(__name__)
I moved the creation of the app into a function that can be called from elsewhere and pass in the necessary config details like so (taken from the Flask website):
def create_app(config_filename): app = Flask(__name__) app.config.from_pyfile(config_filename) from yourapplication.model import db db.init_app(app) from yourapplication.views.admin import admin from yourapplication.views.frontend import frontend app.register_blueprint(admin) app.register_blueprint(frontend) return app
However I think it's a little more helpful if I flesh this out in a little more detail.
Here's roughly what my
__init__.py file ended up looking like:
import os from flask import Flask, request from flask.ext.sqlalchemy import SQLAlchemy from flask.ext.bcrypt import Bcrypt from flask.ext.login import LoginManager from .errors import ErrorHandler db = SQLAlchemy() bcrypt = Bcrypt() login_manager = LoginManager() def create_app(config=None): """Create and return app.""" app = Flask(__name__) if config is None and 'PROJECT_SETTINGS' in os.environ: app.config.from_object(os.environ['PROJECT_SETTINGS']) else: app.config.from_object(config) db.app = app db.init_app(app) # populated regions table required for adverts form. Must be imported # before blueprints from data.generator import Generator generator = Generator() generator.create_regions() bcrypt.init_app(app) login_manager.init_app(app) if not app.debug: ErrorHandler(app) load_blueprints(app) return app def load_blueprints(app): """Load blueprints.""" from .users.views import users_blueprint from .oauth.views import oauth_blueprint from .pages.views import pages_blueprint app.register_blueprint(users_blueprint, url_prefix='/users') app.register_blueprint(oauth_blueprint, url_prefix='/oauth') app.register_blueprint(pages_blueprint)
I'll be merging these changes into my Flask Template when I get the chance.
So, firstly we have our imports as you would expect. Then I create global variables for
login_manager. Notice that I don't pass
app to any of these. I'm able to do this because these extensions have all been kind enough to follow the Flask Extension guidelines and therefore have an
init_app method which allows for the module to be initialised but waits for the app to be loaded in.
I then create the app leaving open the possibility of a system variable defining the configuration.
Next, I need to get the database up and running. I'm not 100% sure why, but Flask-SQLAlchemy requires you to set the
app attribute before running
init_app. I then populate the regions table so I don't get an error when the form is created (don't worry about the
Generator business, it's what I use to generate test data).
I then initialise the
ErrorHandler and finally load in my blueprints (which is in a separate function for clarity, and return the app.
Now we should be good to go.
I've done away with Flask-Script and instead just created a script directory with various scripts in it. For example, my
test.py script looks like this:
import unittest def test(): """Run unit tests.""" tests = unittest.TestLoader().discover('tests', pattern='*.py') unittest.TextTestRunner(verbosity=1).run(tests) if __name__ == "__main__": test()
create_app method looks like this:
def create_app(self): """Create app for tests.""" app = create_app('config.Test') return app
runserver.py script looks like this:
from project import create_app app = create_app() if __name__ == "__main__": app.run()
It took me a while to piece all that together so hopefully it helps someone.