December 03, 2009

The Framework Design Balancing-act

In the Python web development world, we have become very familiar with the word 'framework.' Most of us are using at least one, and some of us are guilty of writing our own, as well.

Recently I was involved in an IRC discussion on the merits of the various web frameworks, and naturally this is about as vicious a conversation as discussing text editors of choice. A topic which came up during that discussion was the ability to start writing your code with a minimum of writing configs or other 'glue'. The ability to just "load up and start writing your app" or "import and use" is often touted, but this often comes with a serious compromise. This balancing act I like to think of as the 'boilerplate dilemma'.

One framework which offers a great 'load up and go' ability is Django. By running their startproject command, one gets a skeleton project directory with a lot of things taken care of, some default settings and structure. With very little changes, you're writing your first view and template, and moving on with things. Want to change some of the behavior? There's probably a settings.py setting for that.

The way settings.py is found by the django framework is through an environment variable, DJANGO_SETTINGS_MODULE. (there's an alternate way to configure django, but it is not functionally much different) The django management script takes care of setting that for you, so most django users don't have to know about it and first. Now, when you import bits from django.db, you're getting the correct database wrapper because your settings specified them. Models know what database to create themselves in, and more. When you contrast this with sqlalchemy's db setup, which oft requires you passing around things such as MetaData and Engine instances, all of a sudden that DJANGO_SETTINGS_MODULE compromise doesn't seem so bad.

And now you have fallen into the trap. The issue with the Django config model is that your settings are now essentially bound to that interpreter, making it impossible for other code in the same interpreter to use different settings. For those who want to create reusable Django apps, it's hard to just ship your package and say to the user "Just import my module", they probably have to add it to INSTALLED_APPS, TEMPLATE_DIRS, and some other fun wiring. Sometimes the existence or lack thereof of a setting like APPEND_SLASH can make your application completely incompatible with the project which embeds it.

Let me quickly scoot over to a different way of handling your framework setup: It can be done via instantiation and dependency injection. Jinja2 is a very good example of this put into practice.

from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader('myapp/templates'))
env.globals['url']         = my_url_builder
env.globals['get_notices'] = get_notices
env.filters['escape_php']  = do_escape_php

With just a little configuration, you now have your own personal Jinja2 environment, which you can pass around as you like, and is separate from other Jinja2 environments which might be instantiated in the same process; you don't have to care. Sure, it looks like glue, but it's much more powerful. In addition, the design of Jinja2 allows for bits to be easily overridden and swapped out: different loaders, extensions, more, not to mention the ease of passing in mocks for testing.

The reason I've been pondering these issues so deeply is that I'm currently in the process of writing my own library/webapp, which I will make available on bitbucket soon-ish. What it will be is both a library you import/use, and an included optional WSGI application to view/manipulate the data it creates. For the WSGI application portion, I will be employing Jinja2 and Werkzeug. While it would not be impossible to use Django for this; the global-settings issue would ironically make it hard for Django users to include my standalone application.

I've no doubt in my mind that I will be using a dependency injection model of configuration for my library/app, which confers some additional benefits: People already using Jinja or wanting to customize bits and pieces will be able to pass in their own loader and/or environment to override my templates and tags, or even integrate them with their own base templates. At the same time, I can provide sensible defaults for those who don't need the customization.

And you can have your cake and eat it too. Using your own factory or a submodule of your project, you can support taking the django-settings to configure it:

from django.conf import settings

def django_factory(**kwargs):
    kwargs.setdefault('debug', settings.DEBUG)
    kwargs.setdefault('admin_emails', [x[1] for x in settings.ADMINS])
    if 'jinja_env' not in kwargs:
        kwargs['jinja_env'] = Environment(...)

    return MyFrameworkConfig(**kwargs)

Now you've created a highly reusable componentized application, which can even sense defaults from its users' framework, avoiding repetition.

So I appease you, library authors, don't succumb to the temptation to use module globals instead of passing around dependencies. There are those of us out there who'd like to use your libraries, and can't because you've locked your implementation to a restricted use-case when you didn't have to. Hopefully we don't have to make 'framework' become a dirty word.

Note: I want to make it clear that I'm not hating on Django, in fact I really like Django. As a full-stack framework, it gets a lot of things right, and to new users I will recommend it without hesitation. It's really fun to use, and the right thing to get people to learn how good a python framework can really be. But as a library author myself, sometimes I find it trying to support Django while also supporting other frameworks in my code.

Permalink | 2 comments
See older posts...