Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
132 lines (91 sloc) 4.62 KB
layout title
default
MetaClassAutoURLS

MetaClassAutoURLS

web.py maps URLs regexes to classes via a list like so:

class Default(object):
    ''' Action for / '''        def GET(self):
        pass

class Login(object):
    ''' Action for /login '''        def GET(self):
        pass

    def POST(self):
        pass

urls = ["/", "Default", "/login", "Login"]

One of the disadvantages of this approach is that for large applications, the urls list can get very long and tedious to maintain. Things get worse during long refactorings/reorgnanizations because it's easy to typo a class or module name or just forget to update the urls list. After spending a couple hours debugging just this problem and then smacking myself on the forehead for missing such a simple mistake, I thought to myself, There's got to be a way to automate this! After some research, I found that metaclasses can do exactly what I want:

urls = [ ]

class ActionMetaClass(type):
    def __init__(klass, name, bases, attrs):
        urls.append(attrs["url"])
        urls.append("%s.%s" % (klass.__module__, name))

class Default(object):
    __metaclass__ = ActionMetaClass
    url = "/"
    def GET(self):
        pass

class Login(object):
    __metaclass__ = ActionMetaClass
    url = "/login"
    def GET(self):
        pass

    def POST(self):
        pass

Of course, this is a simple example that leaves a lot to be desired (handling attrs lacking a url key, for instance), but it should get you on your way.


AaronSw writes: That's a clever idea. One improvement might be to have the classes inherit from a class with the metaclass set, so you don't have that unsightly __metaclass__ every time.

xunil writes: Yeah, I went w/ this approach in the end since I found that I wanted each action class to inherit some common functionality anyway. You can view the source for my incomplete site which uses a metaclass and decorators.


May be it's better to do it with decorators?

xunil writes: I originally tried to use decorators to do this since I was already familiar w/ them, but unless I'm missing something, decorators don't work on classes eg.

@action("/")
class Default:
    def GET(self):
        pass

That won't work. The PEP for decorators describes it, but apparently it was never incorporated into the language (perhaps b/c metaclasses are available and do the same thing).


I couldn't figure out how to get this to work with URL classes in separate files, so I did something a little different, based on this Python Cookbook recipe:

import os, web

urls = [ ]

for aaa in os.listdir(os.getcwd()):
    module_name, ext = os.path.splitext(aaa)
    if module_name.startswith('cgi_') and ext == '.py':
        module = __import__(module_name)
        urls.append(module.url[0])
        urls.append(module_name + "." + module.url[1])

if __name__ == "__main__":
    web.run(urls)

And then e.g. cgi_hello.py (in the same directory) would be:

import web
url = ('/(.*)', 'hello')

class hello:
    def GET(self, name):
        i = web.input(times=1)
        if not name: name = 'world'            for c in xrange(int(i.times)): print 'Hello,', name+'!'

Is this a terrible way to do it?


xunil writes: This is not altogether a bad idea, but it again decouples the URL information from the class, making it a module-level tuple or list. You could combine your importing logic w/ my metaclass idea, actually, and achieve the same thing as me eg.

import os, web

for aaa in os.listdir(os.getcwd()):
    module_name, ext = os.path.splitext(aaa)
    if module_name.startswith('cgi_') and ext == '.py':
        module = __import__(module_name)

if __name__ == "__main__":
    import metaclass
    web.run(metaclass.urls)

metaclass.py:

urls = [ ]

class ActionMetaClass(type):
    def __init__(klass, name, bases, attrs):
        urls.append(attrs["url"])
        urls.append("%s.%s" % (klass.__module__, name))

cgi_hello.py:

import web
from metaclass import ActionMetaClass

class hello:
    __metaclass__ = ActionMetaClass
    url = '/(.*)'
    def GET(self, name):
        i = web.input(times=1)
        if not name: name = 'world'            for c in xrange(int(i.times)): print 'Hello,', name+'!'

Jump to Line
Something went wrong with that request. Please try again.