Entries tagged with templatetags rss

Using regex to validate django templatetag arguments

When writing custom templatetags, the arguments from the template are passed as a string to the tag function and it is necessary to parse these in some way to get the individual arguments. For example, with {% mytag arg1 arg2 %}, the templatetag function receives "mytag arg1 arg2" as a string (called "token") and has to figure out what each of the substrings mean. When there's fixed arguments, as in the official documentation's examples, splitting on whitespace can be sufficient: tag_name, format_string = token.split_contents()

When the tag becomes more complex and offers optional arguments, the error checking gets a bit hairy and extremely repetitive. An example from some code I wrote a while back:

def first_gallery(parser, token): tag_name = "first_gallery" try: bits = token.contents.split() except ValueError: raise TemplateSyntaxError, "%r usage: {% %r for app.model primary_key [as gallery] %}" % (tag_name, tag_name) noa = len(bits) if noa < 4: raise TemplateSyntaxError, "%r tag takes at least 3 arguments" % tag_name if bits[1] != 'for': raise TemplateSyntaxError, "%r tag requires the first argument to be 'for'" % tag_name if noa > 4: if noa < 6 or noa > 6: raise TemplateSyntaxError, "%r tag requires exactly 5 arguments when specifying a variable name" % tag_name if bits[4] != 'as': raiseTemplateSyntaxError, "%r tag requires 4th argument to be 'as' when specifying a variable name" % tag_name return FirstGalleryNode(bits[2], bits[3], bits[5]) return FirstGalleryNode(bits[2], bits[3])

Pretty ugly. A much cleaner way to do this is with regular expressions. The downside of using regex is the loss of fine-grained error output to the user of the template tag. For me it's worth it; the regex shouldn't be much more complicated than those in urls.py, and the user should probably already be able to deal with those.

The tag we'll work with is {% nearby_images %}, which sets two context variables ("previous" and "next" by default) which contain images close to the specified one in a particular gallery. You'd use this most often when creating a carousel of thumbnails, as in (php)Gallery. The full syntax is: {% nearby_images [5 near] image in gallery [as previous, next] %}. This example gets ugly if we use the split()-based validation from above since both optional sections have to be accounted for and the magic index numbers shifted around appropriately.

The main regex features we'll use are grouping () and capturing (?P<name>pattern). The most simple part to write a pattern for are the required arguments, including the tag name: nearby_images (?P<image>\w+) in (?P<gallery>\w+). This regex uses the same principle as the urls.py regex in that it captures the bits between the parentheses with the name given in between the angle brackets. We'll use it like so:

>>> p = "nearby_images (?P<image>\w+) in (?P<gallery>\w+)" >>> m = re.match(p, "nearby_images myimage in mygallery") >>> m.groupdict() {'image': 'myimage', 'gallery': 'mygallery'}

Next, add in the optional section by using a parenthetical grouping followed by a '?': nearby_images ((?P<num>\w+) near )?(?P<image>\w+) in (?P<gallery>\w+), making sure to include the whitespace inside the grouping. This pattern can be used in both cases:

>>> p = "nearby_images ((?P<num>\w+) near )?(?P<image>\w+) in (?P<gallery>\w+)" >>> m = re.match(p, "nearby_images myimage in mygallery") >>> m.groupdict() {'image': 'myimage', 'num': None, 'gallery': 'mygallery'} >>> m = re.match(p, "nearby_images 6 near myimage in mygallery") >>> m.groupdict() {'image': 'myimage', 'num': '6', 'gallery': 'mygallery'}

Finally, add the last optional group: nearby_images ((?P<num>\w+) near )?(?P<image>\w+) in (?P<gallery>\w+)( as (?P<pervious>\w+), (?P<next>\w+))? which lets us do:

>>> p = "nearby_images ((?P<num>\w+) near )?(?P<image>\w+) in (?P<gallery>\w+)( as (?P<pervious>\w+), (?P<next>\w+))?" >>> m = re.match(p, "nearby_images 6 near myimage in mygallery as FOO, BAR") >>> m.groupdict() {'previous': 'FOO', 'image': 'myimage', 'num': '6', 'gallery': 'mygallery', 'next': 'BAR'}

So now we've got the arguments to the template node in a dict, and so long as our arguments match the function signature, everything is peachy. Our finished tag function looks like this:

def nearby_images(parser, token): pattern = re.compile("nearby_images ((?P<num>\w+) near )?(?P<image>\w+) in (?P<gallery>\w+)( as (?P<pervious>\w+), (?P<next>\w+))?") m = pattern.match(token.contents) if m: return NearbyImagesNode(**m.groupdict()) else: raise TemplateSyntaxError, "FAIL"

And the matching Node class:

class NearbyImagesNode(Node): def __init__(self, num, image, gallery, previous, next): self.image = image ...

Alternatively, one can use kwargs in the Node as well, but then each argument has to be pulled from the dict:

class NearbyImagesNode(Node): def __init__(self, **kwargs): self.image = kwargs['image'] ...

And that's it. Cleaner argument parsing, with the downside of less fine-grained error messages.