Be Prepared

I like Flask. No, really, I do. Yesterday, during a lightning talk, I claimed that I love it, and if I don't love it, then at least I love the form and function of it. I wouldn't marry it, since I don't think being married to a microframework for Web applications would provide any tax benefits. Maybe I'm getting off-topic?

Flask is built on Werkzeug, and directly uses Werkzeug routes for view lookup and URL building. As a result, anything that can be done to Werkzeug can be done to Flask. A little-known ability of Werkzeug is the ability to add new URL converters. An example and explanation is provided in the Werkzeug documentation on converters. I decided to build some cool converters which would automate some of the work I have to do when working with certain objects.

Without further ado, I would like to present ModelConverter, a class which can convert a segment of a URL representing a text field on a model into an instance of that model, and vice versa.

from __future__ import with_statement

from werkzeug.routing import BaseConverter, ValidationError

from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound

class ModelConverter(BaseConverter):
    """
    Converts a URL segment to and from a SQLAlchemy model.

    Rather than use an initializer, this class should be subclassed and
    have the `model` and `field` class attributes filled in. `model` is
    the Flask-SQLAlchemy model to use for queries, and `field` is the
    field on the model to use for lookups.

    The field to use should be Unicode or bytes.
    """

    def to_python(self, value):
        try:
            with self.app.test_request_context():
                obj = self.model.query.filter_by(**{self.field: value}).one()
        except (MultipleResultsFound, NoResultFound):
            raise ValidationError()

        return obj

    def to_url(self, value):
        return getattr(value, self.field)

This particular flavor uses an inheritance-based approach in order to avoid clobbering BaseConverter's initializer, but a compositional approach works too. A make_model_converter convenience method can provide the glue needed to specialize the converter. To apply it to the Flask application, merely modify the URL map after application creation:

from converters import make_model_converter
from models import Character

app = Flask(__name__)

app.url_map.converters["character"] = make_model_converter(app, Character,
    "slug")

And now you can create cool things along the lines of:

@app.route("/<character:c>")
def character(c):
    return render_template("character.html", c=c)

There is one caveat with this technique: the model instances retrieved this way will be detached from SQLAlchemy and the current session will not know about them. If you need to look up any lazily-loaded data on the models, you will need to add them to the current session first. For example, assuming Character.friends is a lazily-loaded one-to-many mapping:

@app.route("/<character:c>/friends")
def character_and_friends(c):
    db.session.add(c)
    return render_template("friends.html", c=c, friends=c.friends)

Today's snippets are all real-world snippets from DCoN, and can be seen in the converters.py and views.py source files.

~ C.

Last modified on 2012-02-15 20:34:00

Valid CSS!