Writing simple templatetags is only marginally less painful that writing complex tags. There's a good deal of boilerplate code that goes into writing tags:

class SomeObjectsNode(Node):
    def __init__(self, queryset, var_name, limit=5):
        self.queryset = queryset
        self.var_name = var_name
        self.limit = int(limit)

    def render(self, context):
        queryset = template.Variable(self.queryset).resolve(context)
        try:
            objects = self.queryset[:self.limit]
        except:
            objects = None
        context[self.var_name] = objects
        return ''

@register.tag
def get_some_objects(parser, token):
    """
    Get me some objects

    usage:
    {% get_some_objects from queryset as [var_name] (limit [limit]) %}
    """
    bits = token.split_contents()
    if len(bits) not in (4, 6):
        raise TemplateSyntaxError('%s takes 2 or 3 arguments' % (bits[0]))
    elif len(bits) == 4:
        return LatestEntriesNode(bits[1], bits[3])
    elif len(bits) == 6:
        return LatestEntriesNode(bits[1], bits[3], bits[5])

When you get down to the core of it, though, this is what you want to express:

def get_latest_entries(context, queryset, var_name, limit=5):
    try:
        objects = queryset[:limit]
    except:
        objects = None
    context[var_name] = objects

I wrote django-simpletag with the goal of making this work. There are a few quirks, but the simpletag implementation would look like this:

register = template.Library()

@simpletag(register)
def get_latest_entries(context, queryset, __as, _var_name, __limit, _limit=5):
    try:
        objects = queryset[:_limit]
    except:
        objects = None
    context[_var_name] = objects

So what's up with the underscores.

Yeah they're pretty gross, but rather than doing things in the decorator, I opted to put them right there on the function. Templatetags seem to generally take 3 kinds of arguments: string literals that matter (var_name), throwaways that should probably be validated anyways (as), and template variables that have to be evaluated. I've accomodated for these types by a convention of underscore prefixes. A single underscore, like varname should be evaluated as a string literal. A double-underscore, like __as, is for those throwaways that make the templatetag operations clear - it should be validated that, if present, the user entered the right thing (__as == 'as'). Lastly, no underscores means evaluate as a template variable. Default arguments can be supplied, but are limited to the same rules python uses (so no default args before required args).

How it works

It's really simple. The simpletag decorator registers a templatetag that uses your functions name. The callable passed into the templatetag registry is actually a class that wraps your function in parsing and validation logic. inspect.getargspec is used to introspect the arguments you've defined for your function. This is all of the code:

import inspect
from django import template

def simpletag(register):
    def inner(func):
        register.tag(func.__name__, SimpleTag(func))
        return func
    return inner

class SimpleTag(object):
    def __init__(self, func):
        self.func = func

        # introspect the function, reading its arguments
        args, _, _, defaults = inspect.getargspec(self.func)
        assert args[0] == 'context', 'First argument of a simpletag must be "context"'

        # ignore the first arg, which is always 'context'
        self.args = args[1:]

        if defaults:
            self.num_defaults = len(defaults)
        else:
            self.num_defaults = 0

    def __call__(self, parser, token):
        bits = token.split_contents()[1:]

        if len(bits) > len(self.args):
            self._error('expects no more than %d arguments' % (len(self.args)))

        for indx, arg in enumerate(self.args):
            if arg.startswith('__'):
                try:
                    named_bit = bits[indx]
                    if named_bit != arg[2:]:
                        self._error('expected argument %d to be "%s"' % \
                            (indx + 1, arg[2:]))
                except IndexError:
                    # append an empty string to the bits
                    bits.append('')

        if len(bits) < len(self.args) - self.num_defaults:
            self._error('expects at least %d arguments' % \
                (len(self.args) - self.num_defaults - 1))

        return SimpleNode(self.func, bits, self.args)

    def _error(self, message):
        raise template.TemplateSyntaxError('%s %s' % \
            (self.func.__name__, message))

class SimpleNode(template.Node):
    def __init__(self, func, params, args):
        self.func = func
        self.params = params
        self.args = args

    def render(self, context):
        for indx, bit in enumerate(self.params):
            if not self.args[indx].startswith('_'):
                self.params[indx] = template.Variable(bit).resolve(context)
        return_value = self.func(context, *self.params)
        return return_value or ''

django-simpletag is up on github, if you'd like to take a peek.

Note

I recognize it's sort of a rite-of-passage to attempt to make templatetag writing less painful. There are a lot of other ways to do it, and Eric Holscher has written some great posts on the subject. Alex Gaynor has written what I think is probably the neatest solution, specifying arguments in the decorator. As always, I welcome your feedback!

Related links

Comments (5)

Carl Meyer

My favorite is django-tagcon (http://github.com/korpios/django-tagcon). It allows a similar level of conciseness, but with a syntax that I find cleaner than either this or templatetag-sugar. Also, tags are classes so that I can reuse code via subclassing.

February 15, 2010 at 9:19 a.m. ( )

Charles Leifer

Carl - thanks for the link, I hadn't seen that library before nor had I considered a declarative approach. Very cool!

February 15, 2010 at 1:45 p.m. ( )

Vinicius Mendes

What do you think about putting something like kwargs in the end of the method?

def tag_name(context, kwargs):

So you allow the use of this:

{% tag_name user=request.user %}

February 21, 2010 at 8:10 a.m. ( )

Charles Leifer

So while reading Pro Django I learned that Django actually has most of this functionality already. D'oh! The relevant bit is a decorator that is actually called simple_tag.

Check it out:

February 27, 2010 at 7:59 p.m. ( )

Sam

I'd like to humbly submit my own contribution to template tag simplification. It's called fancy_tag. It takes Django's simple_tag and adds "as " support, keyword argument support, and *args/kwargs support.

You can get it here: http://github.com/trapeze/fancy_tag/

March 1, 2010 at 1:08 p.m. ( )

Commenting has been disabled for this entry