The other day a friend of mine was trying out flask-peewee and he had some questions about the best way to structure his app to avoid triggering circular imports. For someone new to flask, this can be a bit of a puzzler, especially if you're coming from django which automatically imports your modules. In this post I'll walk through how I like to structure my flask apps to avoid circular imports. In my examples I'll be showing how to use "flask-peewee", but the same technique should be applicable for other flask plugins.
I'll walk through the modules I commonly use in my apps, then show how to tie them all together and provide a single entrypoint into your app.
I use a structure that may look familiar to users of the django framework:
In a little bit I'll get to the reason "main.py" is the secret sauce, for now though I'll focus on the other bits. Rather than go alphabetically, as I did in the previous list, I will go through these models in order of their precedence in the "import chain" we'll be setting up:
Every flask application needs an "app.py", whether you call it that or not. It's
the place where your
Flask instance lives. I like to keep my app.py very
thin, so that it only contains things that are central to the entire project.
I do this because, as you'll see, we will import
from app pretty much everywhere.
In addition to my
Flask object, I also set up a Database, a cache (if I'm
using one), logging handlers, and any global template filters in
""" I keep app.py very thin. """ from flask import Flask # flask-peewee database, but could be SQLAlchemy instead. from flask_peewee.db import Database app = Flask(__name__) app.config.from_object('config.Configuration') db = Database(app) # Here I would set up the cache, a task queue, etc.
That's it! This may seem odd if you've seen the flask "hello world", which contains URL routing, view functions, etc. As you'll see, we will be putting those in a different module.
Now that we've got a Database object, we can create our model classes. This
will introduce the first "import dependency", because we need to import the
database we created in
app.py. Here is what a small models file might look
""" models imports app, but app does not import models so we haven't created any loops. """ import datetime from flask_peewee.auth import BaseUser # provides password helpers.. from peewee import * from app import db class User(db.Model, BaseUser): username = CharField() password = CharField() email = CharField() join_date = DateTimeField(default=datetime.datetime.now) active = BooleanField(default=True) admin = BooleanField(default=False) def __unicode__(self): return self.username
After creating a
User model, we can set up authentication for the app.
Auth takes an app, a database and a
User model as its
parameters. It provides free login/logout functionality, a way to get the
logged-in user, and a decorator to mark a view as "login required".
""" auth imports app and models, but none of those import auth so we're OK """ from flask_peewee.auth import Auth # Login/logout views, etc. from app import app, db from models import User auth = Auth(app, db, user_model=User)
Most of the sites I use will have an "Admin area", where dynamic content can be created, edited and deleted. Less often I might add a REST-ful API to expose models, but for completeness I'll show how they both work since they're pretty similar.
""" admin imports app, auth and models, but none of these import admin so we're OK """ from flask_peewee.admin import Admin, ModelAdmin from app import app, db from auth import auth from models import User admin = Admin(app, auth) auth.register_admin(admin) # or you could admin.register(User, ModelAdmin) -- you would also register # any other models here.
And here is
""" api imports app, auth and models, but none of these import api. """ from flask_peewee.rest import RestAPI, RestResource, UserAuthentication from app import app from auth import auth from models import User user_auth = UserAuthentication(auth) # instantiate our api wrapper and tell it to use HTTP basic auth using # the same credentials as our auth system. If you prefer this could # instead be a key-based auth, or god forbid some open auth protocol. api = RestAPI(app, default_auth=user_auth) class UserResource(RestResource): exclude = ('password', 'email',) # register our models so they are exposed via /api/<model>/ api.register(User, UserResource, auth=admin_auth)
Lastly, the views. The views are responsible for mapping urls to functions, and so they generally need to reference the app, authentication and models.
""" views imports app, auth, and models, but none of these import views """ from flask import render_template # ...etc , redirect, request, url_for from app import app from auth import auth from models import User @app.route('/') def homepage(): return render_template('foo.html') @app.route('/private/') @auth.login_required def private_view(): # ... user = auth.get_logged_in_user() return render_tempate(...)
These modules are all fairly self-contained, but we have no mechanism to ensure that all are imported when we run our application. We need to import all the modules, though, to capture all the great module-level side-effects. For that purpose I tie everything together with a module named "main.py".
""" this is the "secret sauce" -- a single entry-point that resolves the import dependencies. If you're using blueprints, you can import your blueprints here too. then when you want to run your app, you point to main.py or `main.app` """ from app import app, db from auth import * from admin import admin from api import api from models import * from views import * admin.setup() api.setup() if __name__ == '__main__': app.run()
main.py should be treated as the entry-point into your application from
here on out. If you are running a WSGI server, you would therefore want to
point it at
main.app as opposed to
app.app, if that makes sense.
Thanks for reading this post, I hope you found it useful. Please feel free to leave any questions or comments.
For a more complete example, check out the flask-peewee example project -- it's a very small twitter clone.
Commenting has been closed, but please feel free to contact me