18.7.05. Python off the Rails

Django, the latest in the "we can be like Rails!" entries from the Python world, has been getting a lot of notice lately, while Subway is getting very little. And since Zope 3 and other such items aren't even mimicking Rails, they get zero notice, even though Zope 3 offers many similar benefits - extensively tested and testable, user interfaces easy to build off of introspection, separation of model code from views, controllers, as well as other site utilities (objects outside of the domain model that provide services such as authentication, database connection, object indexing and tracking, annotation, and so on).

I haven't run Subway. Or Rails. Or Django. On my desktop, I don't want to deal with an RDBMS if I don't have to, and most core setups and example applications for these apps seem geared towards requiring MySQL, despite whatever the frameworks authors may intend about where model objects come from. So this opinion is just from someone who likes to look at code and be impressed or disappointed.

Ruby on Rails impresses me, due to its extensive use of meta-programming to provide fairly natural language constructs when setting up ActiveRecord ORM objects. Subway impresses me because it provides a Rails inspired stack built on existing Python solutions - CherryPy (web/application server), SQLObject (object-relational mapper), Cheetah (templates). I think people have managed to get the ZODB object database and even an implementation Zope's Page Template system running with Subway, but I could be wrong. The nice thing is that they let these other systems do the so-called "heavy lifting" instead of writing yet-another-stack.

So things come down to how one describes their model objects, which is especially critical in an object-relational world (since you have to fit to the DBMS's constraints). It's equally applicable to the application itself though, as the constraints can be used for data storage / input form validation. Django seems stuck in the past. Maybe it's trying to be compatible with older versions of Python. To me, this is such an ugly "unpythonic" way of doing things. This comes from revision 28 of comments.py.

class Comment(meta.Model):
    db_table = 'comments'
    fields = (
        meta.ForeignKey(auth.User, raw_id_admin=True),
        meta.ForeignKey(core.ContentType, name='content_type_id', rel_name='content_type'),
        meta.IntegerField('object_id', 'object ID'),
        meta.CharField('headline', 'headline', maxlength=255, blank=True),
        meta.TextField('comment', 'comment', maxlength=3000),
        meta.PositiveSmallIntegerField('rating1', 'rating #1', blank=True, null=True),
        .....
        meta.BooleanField('valid_rating', 'is valid rating'),
        meta.DateTimeField('submit_date', 'date/time submitted', auto_now_add=True),
        meta.BooleanField('is_public', 'is public'),
        meta.IPAddressField('ip_address', 'IP address', blank=True, null=True),
        meta.BooleanField('is_removed', 'is removed',
            help_text='Check this box if the comment is inappropriate. A "This comment 
                       has been removed" message will be displayed instead.'),
        meta.ForeignKey(core.Site),
    )
    module_constants = {
        # used as shared secret between comment form and comment-posting script
        'COMMENT_SALT': 'ijw2f3_MRS_PIGGY_LOVES_KERMIT_avo#*5vv0(23j)(*',
    ....
It just goes on and on from there. It's not very Pythonic, but it is the way that many of us used to do things in the Python world. But now, thanks to meta-programming being more accessible in recent versions of the Python language, you can have a system that looks like this - from a comments example for Subway, revision 57:
from subway import model
import sqlobject as db # funky alias

class Comment(model.Model):

    _table = 'comment'

    email = db.StringCol(length=50)
    website = db.StringCol(length=50)

    screenName = db.StringCol(length=50, dbName='screen_name')        

    content = db.StringCol()
    parsedContent = db.StringCol()

    created = db.DateTimeCol(dbName='date_created')
Granted, it's a much smaller example than the one listed above. But you can see that at least here, you have the more familiar class and attribute construct - a = b. Personally, I'm not fond of the 'Col' (column) suffix, but it's an improvement over the Django implementation. This does not show any foreign keys or row/object ids, but I expect them to be similar.

For the record, Zope 3 doesn't really require anything like this when going against the ZODB. But most Zope 3 development, which is interface driven, uses schema descriptors when defining object interfaces. This might look like:

from zope.interface import Interface
import zope.schema

class IComment(Interface):
    email = zope.schema.TextLine(title='Email', required=True, max_length=50)
    website = zope.schema.URI(title='Web Site')
    
    screenName = zope.schema.TextLine(title='Screen Name', required=True)
    
    content = zope.schema.Text(title='Content', required=True)
    parsedContent = zope.schema.Text(title='Parsed Content')

    created = zope.schema.DateTime(title='Created', required=True)
Of course, when using the full power of Zope 3, you could get the created date from the Zope Dublin Core, and even the screen name you can get from that as well. But I didn't want to go into those specifics. Note that the above code on its own doesn't really do anything. It's just specifying an interface. We could define methods that implementations of IComment would have to implement as well if wanted.

One of Rails big showoff pieces is scaffolding, which generates user interfaces based on ActiveRecord objects. If you add a comments field to the database as a TEXT column, it can immediately show up in the scaffolding UI as an HTML TEXTAREA. Zope 3 can do similar, although there's no cool name for how it does it like 'scaffolding'. There are a few layers that work together in Zope to do this naked objects style approach. Zope 3, using interfaces heavily already, extends them with Schema support to allow developers to specify an objects properties. Input systems, such as HTML forms and their handlers, can use this information to present and validate input. It's a way of using dynamic typing effectively by also being able to write and implement more expressive validation beyond 'email is a string'. Now it's known that "email is a string that's required to have a value" and code elsewhere in the system can better depend on the "email" value being present. And since interface definition is independent of implementation, one could use the IComment interface describe above with the SQLObject implementation above it and declaring zope.interface.implements(IComment).

What is my point with all of this? In general, it's that I don't think that Django's present implementation is a good example of the best that Python can be when it comes to the type of systems inspired by Rails. I think Subway is a better implementation and as a developer, it certainly looks more enjoyable to work with. I've done the big huge list of fields = (String('title'), String('description'), ...) things in the past. They're not fun. Subway, Zope 3, and Nevow better employ the Python language, in my opinion. I don't yet see what merits all of the sudden attention Django is getting. Oh well. Bobo - Principia - Zope 1 - Zope 2 have been helping me implement solutions for almost ten years now, and Zope 3 will probably be my platform of choice for the next 5-10 years. While everyone else in Python land tries to find the next thing to copy, I'll be working on the same basic python object publishing concepts that blew me away back in '96.