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 db
, bcrypt
and 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 bcrypt
, login_manager
and 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()
And my unittest
create_app
method looks like this:
def create_app(self):
"""Create app for tests."""
app = create_app('config.Test')
return app
Similarly my 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.