A Design Pattern Idea For Python Micro Web Frameworks

I often find myself prototyping new ideas using either the Bottle or Flask “microframeworks”. While they do facilitate quick and flexible design, one of the usual design characteristics of both of these frameworks I dislike is the use of a global application objects and globally decorated functions as “views” for routes in both of these frameworks. An example of this style:

from bottle import Bottle

app = Bottle()

# ....

@app.route("/", name="index_view", method=["GET"])
def index_view():
    return "Hello!"

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8080)

For applications contained in a single file, with few or no outside dependencies (and likely few or no tests), this is OK. However, consider once this broken into two files, one main.py:


from bottle import Bottle
from views import *

app = Bottle()

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8080)

and views:

from main import app
import random

random.seed()

@app.route("/", name="index_view", method=["GET"])
def index_view():
    return "Hello!"

@app.route("/randomNumber", name="random_number", method=["GET"])
def random_number():
     return "Random Number: {0}".format(random.random())

Notice that we now depend on circular imports to load our views. Circular imports can often cause unexpected behavior, and are usually a design smell in an application (some good further discussion on circular imports in Python can be found here). The decorator and view function style, in which arguments only come from URL parameters, for Bottle and Flask (along with the global nature of the functions) also makes injecting objects like database connections difficult. In Flask, applications will often make use of the thread local reference to current_app in combination with the g attribute, by adding at will to this object, in order to get at dependencies like database connections. I find this often leads to messy and confusing applications, where it’s unclear when (or if) a dependency has or has not been added.

As an alternative, I had started by making my views as closures as a means to
restrict when the views are decorated, and provide a clear and easy means to add dependencies like database connections (views.py):

# Ideally your queries should not live in the same place as your views.
def select_thing_count(db_connection):
    # In a real application you would be doing a db query here.
    return {"thingCount": 400}

def create_index_view(app):
    @app.route("/", name="index_view", method=["GET"])
    def index_view():
        return "Hello!"
    return index_view

def create_status_view(app, db_connection):
    @app.route("/status", name="status_view", method=["GET"])
    def status_view():
        thing_count_dict = select_thing_count(db_conection)
        return "Thing count: {0}".format(thing_count_dict["thingCount"]
    return status_view

We can similarly alter main.py to remove the global application object, and instead construct the application inside of a function:

from bottle import Bottle
from views import create_status_view, create_index_view

def create_db_connection(db_connection_info):
    # Do your db specific stuff to instantiate a connection
    # instead of returning None
    db_connection = None
    return db_connection

def create_my_app(db_connection_info):
    db_connection = create_db_connection(db_connection_info)
 
    app = Bottle()
    create_index_view(app, db_connection)
    create_status_view(app, db_connection)
    return app

if __name__ == "__main__":
     # In a real application load your specific
     # DB connection data, probably from an argument parser or
     # similar.
     db_connection_info = {
        "host": "127.0.0.1", 
        "user": "foo", 
        "password": "bar", 
        "database_name": "baz"
     }
     my_app = create_my_app(db_connection_info)
     my_app.run(host="127.0.0.1", port=8080)

This significantly clarifies the application and view lifecycle, and makes it easier to understand what dependencies are present and where they come from (like our database connection). Additionally, dependency injection for tests becomes again easier to understand and do in this context. Using closures in this way works relatively well for small applications, but is a little clunky and gets painful when you have many related views that can be grouped together.

In order to further refine this, especially when there are many views, I use a similar pattern, using a class instead of a closure for similar effect (views.py):

# queries.py is omitted, but should be any 
# database queries, (or anything similar) you use for your views
from queries import load_all_things, load_all_users, load_user_by_id
from procedures import format_thing, format_user

class MyApplicationViews(object):
    def __init__(self, db_connection):
        self.db_connection = db_connection

    def attach_to_app(self, app):
        app.route(
            "/", name="index_view",
            method=["GET"], callback=self.index_view
        )
        app.route(
            "/things", name="get_all_things_view", 
            method=["GET"], callback=self.get_all_things_view
        )
        app.route(
            "/users", name="get_all_users_view", 
            method=["GET"], callback=self.get_all_users_view
        )
        app.route(
            "/user/", name="get_user_by_id_view",
            method=["GET"], callback=self.get_user_by_id_view
        )
        # ... route the rest of the views here.

    def index_view(self):
         return "Hello!"

    def get_all_things_view(self):
         things = load_all_things(self.db_connection)
         formatted_things = [format_thing(t) for t in things]
         return "Things: " + "\n".join(formatted_things)
   
    def get_all_users_view(self):
         users = load_all_users(self.db_connection)
         formatted_users = [format_user(u) for u in users]
         return "Users: " + "\n".join(formatted_users)
    
    def get_user_by_id_view(self, user_id):
          user = load_user_by_id(self.db_connection, user_id)
          if not user:
              return "No such user: {0}".format(user_id)
          else:
              return format_user(user)

    # ... more view methods here

Adding further views are easier, and there’s a clear precedent how to group like views and add extra dependencies (again like our database connection). Similar to before, our main.py file loads the view class, and applies it in the application creation function:

from bottle import Bottle
from views import MyApplicationViews

def create_db_connection(db_connection_info):
    # Do your db specific stuff to instantiate a connection
    # instead of returning None
    db_connection = None
    return db_connection

def create_my_app(db_connection_info):
    db_connection = create_db_connection(db_connection_info)
 
    app = Bottle()
    views = MyApplicationViews(db_connection)
    views.attach_to_app(app)
    return app

if __name__ == "__main__":
     # In a real application load your specific
     # DB connection data, probably from an argument parser or
     # similar.
     db_connection_info = {
        "host": "127.0.0.1", 
        "user": "foo", 
        "password": "bar", 
        "database_name": "baz"
     }
     my_app = create_my_app(db_connection_info)
     my_app.run(host="127.0.0.1", port=8080)

The class based definition makes reusing and simplifying code much easier, and provides a clear and easy way to split types of views (i.e. all routes for a REST API go into a single “views” class, routes for statically generated pages go into a different “views” class, etc.). I find that this leads to cleaner and easier to maintain code, which is especially important in microframeworks, which have an extreme amount of freedom in design (and thus the easy ability to make a mess).

Questions or comments? Think there might be a better way to do this or improve upon this? Post below!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s