Today, continuing the series, get ready for some code. The first thing we’ll need to do is lay out some files to work with.
I previously described the syntax in three separate parts:
Thankfully, those can map very nicely to three different files. These files will all be placed in a widgets directory somewhere on the PYTHONPATH, representing the entire framework.
__init__.py: It’s necessary anyway, and it can be imported as simply import widgets. So we’ll use this to bring in components from the other files into one import location.base.py: This will contain the `Widget“ base class and everything necessary to support it.prefs.py: Since the main attribute type we’ll be using are preferences, this module will contain all the attribute classes.The first thing to do is set up a base class to extend. This may look fairly complicated, and it will encompass the rest of this post, but it’s really not that bad. The real trick here is that we’ll need to use a metaclass. A full explanation of metaclasses is being the scope of this article, and maybe someday I’ll do it justice, but for now, just know that it’s a necessary part of the process. The best way to sum up what you’re missing is with a quote from Tim Peters:
Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t (the people who actually need them know with certainty that they need them, and don’t need an explanation about why).
What it basically boils down to is that we need to set up two different classes just to make one work properly. First, we’ll start with the metaclass, which we’ll call WidgetBase, keeping with Django’s naming scheme. The following code is pretty much copied out of Django itself, changing Model to Widget:
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)
There’s a lot going on here, and I apologize for not explaining it more fully, but here are the important bits. This class will be called any time a Widget class definition is processed, including Widget itself. The code above makes sure that Widget and any classes that don’t subclass Widget just get processed like any other class. Any additional code in the __new__ method can then safely assume that it’s processing a subclass of Widget, which is exactly what we want. For now, we’ll just return the the new class, as it was defined, with no additional processing:
return type.__new__(cls, name, bases, attrs)
When working with the rest of the metaclass code, it’s important to know what arguments you’re dealing with. As you can see from the code above, there are four arguments passed to __new__:
cls: The actual class object that was processed by Pythonname: The name of the class, as it was defined in sourcebases: A tuple of classes that were defined as base classes (for this app, this can be safely ignored)attrs: A dictionary containing all the things that were specified in the class declaration (this is the real key)Now, to create the foundation of the base Widget class, we’ll create just a basic class and tell it to use WidgetBase as its metaclass:
class Widget(object): __metaclass__ = WidgetBase
While this doesn’t yet do anything special, it gets some of the most compliated stuff out of the way, leaving us free to explore more useful code in future articles.