Using Declarative Syntax, Part 3

by Marty Alchin on November 12, 2007 about Django

Continuing where we left off yesterday, it’s time for some more sugary syntactic goodness.

We now have a Widget class to work with, but the API specifies a number of preferences that widgets can utilize, as necessary. These are not only an important part of the widget’s design; they’re probably the most interesting (and most complicated) part of our framework’s declarative syntax. As such, this post will be dedicated entirely to getting them to work in a Django-friendly manner, without worrying much about individual preference types.

You may be wondering why we left Widget dangling, without really adding any functionality to it, while we’re now moving on to something seemingly unrelated. The fact is, adding to Widget (more accurately, WidgetBase, for our needs) requires a firm grasp on how the preferences should work. After all, WidgetBase will process preferences, so we need to know how Preference will work before it can do so.

Preference

In order to reuse as much code as possible, all preferences will extend a common base class, called Preference, which will be placed in prefs.py, which was described yesterday. There will be a lot added to this class over time, but for now, we’ll start with just the basics.

class Preference(object):
    "A base class used to identify Preference instances"
    creation_counter = 0

    def __init__(self, label=None):
        self.label = label

        # Increase the creation counter, and save our local copy.
        self.creation_counter = Preference.creation_counter
        Preference.creation_counter += 1

    def __cmp__(self, other):
        # This is needed because bisect does not take a comparison function.
        return cmp(self.creation_counter, other.creation_counter)

There are a couple important things going on here. The easiest to mention is that, by including label as the first argument to __init__ (after self, of course), preferences can be instantiated very similarly to Django’s model fields:

enabled = widgets.Preference("Turn it on")

Also note that there’s no handling for individual preference types yet.

The other important thing is what’s handled by the rest of the code in that snippet. If it doesn’t make sense already (don’t feel bad if it doesn’t), it’s all designed to help determine the order in which the preferences were defined. As I mentioned yesterday, metaclasses get an attrs argument, which is a Python dictionary. Unfortunately, standard Python dictionaries don’t keep track of the order in which their items were added, so we can’t rely solely on the dictionary to handle preferences. Well, we could, but then we could never really be sure what order the preferences would be displayed. If that doesn’t bother you, feel free to ignore this stuff, but it’s generally best to handle it properly.

The real key here is the creation_counter. It starts at 0 and increments each time a preference is instantiated. Since Python always executes code in the order it’s defined in the source code, this is a very reliable way to know how the preferences were defined. In __init__, each preference stores the value of creation_counter at the time it was called, and increments it. So if you had just one widget with three preferences, they’d have creation_counter values of 1, 2 and 3.

Assigning names

One problem you may have already noticed is that Preference, as it stands, doesn’t have any code to use the name it was given in the Widget subclass. The truth is, at this point, it can’t. The assignment in the class happens outside after the preference has been initialized, so there’s no way to figure out what name it was given without a little help.

First, let’s set up a method to set the name once we have one, and later we’ll get to the actual process of getting the name itself. The following method accepts a name and uses it to set not only the name, but also the label if it wasn’t already set explicitly. Again, this falls in line with how Django does it.

    def set_name(self, name):
        self.name = name
        if self.label is None:
            self.label = capfirst(pretty_name(name))

When setting the label, this makes use of a couple utility functions provided by Django. Make sure to import them at the top of prefs.py:

from django.utils.text import capfirst
from django.newforms.forms import pretty_name

Processing preferences

Okay, now it’s time to finally start doing some real work with these things. Remember back to the metaclass we built yesterday? Adding a few lines to its __new__ method will allow it to know about any declared preferences and handle them properly. Place the following code just prior to the return line of that method:

        cls.preferences = []
        for key, attr in attrs.items():
            if isinstance(attr, Preference):
                # Populate a list of prefences that were declared
                attr.set_name(key)
                cls.preferences.insert(bisect(cls.preferences, attr), attr)

Similarly to set_name above, this new code uses Python’s bisect module, as well as the Preference class from prefs.py, so be sure to import them at the top of base.py:

from bisect import bisect

from widgets.prefs import Preference

With this new code, the metaclass can inspect the attrs dictionary, figure out which attributes are preferences, and store them away in a list that’s ordered according to the order in which they were defined in source. Also note that the metaclass knows what name each preference was given, and calls set_name accordingly.

Bringing it all together

We haven’t touched on __init__.py yet, but now that we have some code in each of the other files, it’s time to fill it with all the code it’s ever likely to need:

from widgets.base import Widget
from widgets.prefs import *

Yup, that’s it. In order to serve as a single import point, all it has to do is pull in the appropriate classes from the other two files into a single module namespace. This will allow external code to simply call import widgets and get everything they need to make a widget.

Where we stand

Believe it or not, we’ve actually managed to perform all the “magic” associated with the declarative syntax. There’s more to do before this Netvibes app can be considered complete, but the rest of the code is concerned with details that are specific to this particular app. If you’d like to stop now and run with this in your own framework, you’re more than welcome to do so. I’ll continue on, filling out some of the app-specific details, to illustrate how easily it is to expand on the code we’ve already written.

Speaking of code already written, here’s a recap of what we’ve done so far. In a widgets directory somewhere on your PYTHONPATH, you should have three files. __init__.py was just listed in its entirety, while the other two are provided below, with all the little snippets sewn together.

base.py

from bisect import bisect

from widgets.prefs import Preference

class WidgetBase(type):
    def __new__(cls, name, bases, attrs):
        # If this isn't a subclass of Widget, don't do anything special.
        try:
            if not filter(lambda b: issubclass(b, Widget), bases):
                return super(WidgetBase, cls).__new__(cls, name, bases, attrs)
        except NameError:
            # 'Widget' isn't defined yet, meaning we're looking at our own
            # Widget class, defined below.
            return super(WidgetBase, cls).__new__(cls, name, bases, attrs)

        cls.preferences = []
        for key, attr in attrs.items():
            if isinstance(attr, Preference):
                # Populate a list of prefences that were declared
                attr.set_name(key)
                cls.preferences.insert(bisect(cls.preferences, attr), attr)

        return type.__new__(cls, name, bases, attrs)

class Widget(object):
    __metaclass__ = WidgetBase

prefs.py

from django.utils.text import capfirst
from django.newforms.forms import pretty_name

class Preference(object):
    "A base class used to identify Preference instances"
    creation_counter = 0

    def __init__(self, label=None):
        self.label = label

        # Increase the creation counter, and save our local copy.
        self.creation_counter = Preference.creation_counter
        Preference.creation_counter += 1

    def __cmp__(self, other):
        # This is needed because bisect does not take a comparison function.
        return cmp(self.creation_counter, other.creation_counter)

    def set_name(self, name):
        self.name = name
        if self.label is None:
            self.label = capfirst(pretty_name(name))