fíam

(rhymes with liam)

  • Using a metaclass for registering template tags

    Jan. 8, 2009 at 17:59:45 CET

    Writing your template tags requires too much boilerplate code. Period. I think we all agree on that. Let's see how we can improve it.

    A lot of times I find myself writing code like this (error handling omitted for clarity):

    @register.tag
    def my_menu(parser, token):
        tag_name, argument1, argument2 = token.split_contents()
    
        return MyMenuNode(argument1, argument2)
    
    class MyMenuNode(template.Node):
        def __init__(self, argument1, argument2):
             ...
    

    We are specifying the argument count and the template tag name twice, not exactly DRY friendly ;). After evaluating the solutions coming to my mind, I decided to go with a metaclass based approach, keeping the magic to a minimum.

    import inspect
    from django import template
    
    register = template.Library()
    
    class NodeType(type):
        def __init__(mcs, name, bases, dct):
            super(NodeType, mcs).__init__(name, bases, dct) 
            if not mcs.is_node(name, dct):
                tag_name = ''.join(char if char.islower() else '_%s' % char.lower() for char in name)[1:-5]
                init = mcs.get_init(bases, dct)
                (args, varargs, varkw, defaults) = inspect.getargspec(init)
                arg_count = len(args) # not exactly arg count, but this way we avoid adding one in tag_function
                def tag_function(parser, token):
                    arguments = token.split_contents()
                    if len(arguments) != arg_count:
                        raise template.TemplateSyntaxError('%s tag requires %d arguments' % (arguments[0], arg_count - 1)) 
                    return mcs(*arguments[1:])
    
                register.tag(name=tag_name, compile_function=tag_function)
    
        def is_node(mcs, name, dct):
            return name == 'Node' and dct['__module__'] == __name__
    
        def get_init(mcs, bases, dct):
            if '__init__' in dct:
                return dct['__init__']
    
            for base in bases:
                init = mcs.get_bases(base.__bases__, base.__dict__)
                if init:
                    return init
    
    
    class Node(template.Node):
        __metaclass__ = NodeType
    

    Inheriting from Node, we can now define a new tag just by creating a new class, which gets automatically registered:

    class MyMenuNode(Node):
        def __init__(self, request):
            self.request = template.Variable(request)
    
        def render(self, context):
            # do something with self.request
            return 'my menu markup'
    

    As soon as we load the file containing this code, we'll have a tag named my_menu defined, which takes the current request as its only argument.

    How do you deal with template tags? Do you copy and paste your boilerplate code or have you found a better solution than this one?