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.
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
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
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
from django.utils.text import capfirst from django.newforms.forms import pretty_name
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)
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
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
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.
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.
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
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))