-
Using a metaclass for registering template tags
Jan. 8, 2009 at 17:59:45 CETWriting 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?