Making Template Tag Parsing Easier

In my previous post about template tags, I discussed the two steps required for template tags. Today I will be focusing on Parsing of template tags, and how they may be improved in the framework of Class Based Template Tags from yesterday. I have talked about problems with template parsing in the past as well. This post will offer 2 different approaches to making parsing better.

I would like to thank Cody and Chris who were involved in a slightly drunken conversation that led to these tags. Chris actually wrote the other neat parsing implementation that I will talk about today. Cody wrote the underpinnings of that implementation as well.

Note: Both of these approaches are more Proof of Concepts, and the code probably shows. Please don't knock implementations, and just think about the ideas housed within.

Parsing from above - A DSL approach

I'm going to go ahead and start talking about an approach to parsing template tags that was pointed out in yesterday's comments. It takes surlex which is made for easily parsing URL's, and applies it to the concept of parsing template tags.

In the tag_utils package, I looked at the tests, because they make great documentation. Here is an example of a tag definition.

p = ParsedNode('test', '<arg1:int> <arg2:string> <kw:kwarg>', test_expected) 
register.tag('test', p)

This defines a tag called test, which parses an int, string, and kwarg from a surlex expression. The third argument is a function that is executed on the arguments on rendering.

This allows you in your test_expected function, to act on the arguments that are defined inside of the surlex expression. A trivial example of the test_expected function is:

def test_expected(context, arg1, arg2, kw=None):
    print "Got %s and %s" % (arg1, arg2)

So if you called the tag {% test 1 racoon %}, it would print out Got 1 and racoon.

This is an interesting way to provide a sort of DSL on top of the current mess that is parsing of template tags. I really like how it reuses Surlex, which was made for parsing URLs. However, parsing template tags is a similar task, and it works well here too!

I could imagine this easily being bolted on to the approach from yesterday, which might allow for easier subclassing and reuse of the parsing functions.

Parsing based on keywords

An approach that I have talked about in the past is basically a subset of the above idea. It allows you to define kwarg type arguments for your tags, and have them magically parsed out for you. An example of this is my own SelfParsingTag. The following lines allow you to specify what arguments your tag will accept.

    def __init__(self, required_tags=[]):
        if not required_tags:
            self.required_tags = self._get_tags()
        else:
            self.required_tags = required_tags

    def _get_tags(self):
        return []

So you can either define the _get_tags function, or pass the allowed tags into the call when you make the tag. The following 2 bits of code are equivalent.

class GetContentTag(SelfParsingNode):
    def _get_tags(self):
        return ['as', 'for', 'limit']
register.tag('get_latest_content', GetContentTag())

#Is the same as the following:

class GetContentTag(SelfParsingNode):
    pass
register.tag('get_latest_content', GetContentTag(['as', 'for', 'limit']))

Once the Tag knows what it arguments it will be accepting, it parses them.

def parse_content(self, parser, token):
    parsed = parse_ttag(token, self.required_tags)
    for tag, val in parsed.items():
        setattr(self, '_' + tag, val)
    return parsed

This effectively sets a private varible on the tag to the value of the arg. So for example, if the tag was called {% sweet_tag for news.story as my_stories limit 10 %}, then self._for would equal news.story, and so on. It also returns the parsed values as a dictionary. There are a lot of improvements that could be made to parse_ttag, but it works as a basic implementation.

This approach allows us to implement a tag really easily. If you want a (silly) tag that just updated the context with whatever value you input, you could make a simple tag. It would be used {% my_tag with "awesome text" as context_var %}

class SimpleContextTag(SelfParsingTag):
    def _get_tags(self):
        return ['with', 'as']

    def render_content(self, tags, context):
        for tag in self.required_tags:
            context.update({tag['as']: tags['with']})

register.tag('my_tag', SimpleContextTag())

To implement the get_latest_object code from yesterday, we can skip all of the parsing steps.

class GetContentTag(SelfParsingTag):
    def _get_tags(self):
        return ['as', 'for', 'limit']

    def render_content(self, context):
        self.model = get_model(*self._for.split('.'))
        if self.model is None:
            raise template.TemplateSyntaxError("Generic content tag got invalid model: %s" % model)
        query_set = self.model._default_manager.all()
        context[self._as] = list(query_set[:self._limit])

register.tag('get_latest_object', GetContentTag())

Which is better?

To be truthful, I like the Surlex approach better than my own. It seems to have a lot of the benefits of mine, but with added flexibility. However, that does come with the implementation being a bit more complex. It brings some really neat ideas forward about how template tags might be handled differently. It allows for optional arguments, does basic type checking (based on it's regex nature), and ensures that the order of the arguments is the same.

I could imagine some kind of dispatch based template tag scheme that has a list of URLs, basically like the URLConf and view structure. I think that this problem has a lot more depth to it, and hopefully by pointing out a couple of different ways of solving it, and looking at it, we can improve the situation.




Comments

1 Alex Gaynor says...

From my perspective none of these APIs give enough control. What I'd look for would be something more like:

@register.tag([Contstant(&#39;for&#39;), Variable(), Constant(&#39;by&#39;), Variable(), Optional([Constant(&#39;as&#39;), Name()])
def get_vote(context, args, kwargs):
    pass

Then an invocation like:

{% get_vote for obj by user as vote %}

Would result in args=[obj, user, "vote"]. Variable and Name would take an option parameter for their name that turns them into a kwarg.

Posted at 2:18 a.m. on November 5, 2009

2 Jason Christa says...

I actually use the pattern Alex proposes a lot. I think your class based approach could eliminate all of the boiler plate code for that scenario.

Posted at 2:12 p.m. on November 5, 2009

3 Cody Soyland says...

Some really good ideas here. Alex's proposal is an interesting addition as well. I do think however that the DSL approach has the potential of being nearly as flexible if the API is modified somewhat. Surlex allows "macros" that can be used to extend the language with regex patterns. There is also a notion of optional arguments in parentheses, for example: &lt;slug:s&gt;( as &lt;varname:string&gt;) will match {% tag foo as bar %} or {% tag foo %}.

If the tag_utils API exposed surlex macros and a decorator were provided for syntactic sugar, I would propose an api more like this:

@maketag(&#39;&lt;slug:s&gt;( as &lt;varname:var&gt;)&#39;).with(var=&#39;[a-z_]+&#39;)
def tag(slug, varname=None):
    do_stuff()

Posted at 5:20 p.m. on November 5, 2009

4 Kyle Fox says...

I wrote my own little parser that uses a format similar to python functions. That way you don't have to bother screwing around with bits like 'for', etc.

Instead of:

{% get_vote for obj by user as vote %}

I go:

{% get_vote for=obj by=user as vote %}

Or with the args in a different order:

{% get_vote by=user for=obj as vote %}

It really makes parsing trivial if you just decide your template tags will follow the same format. You don't lose flexibility by not supporting those make-believe operators like "for".

It also makes using template tags much easier, IMO, not just implementing them.

This evening I'll try to remember to post some code to github to show exactly what I mean.

Posted at 8:26 p.m. on November 5, 2009

5 Charlie La Mothe says...

I'd like to mention that djblets (reviewboard-born django helper library) also has some nifty decorators for writing template tags.

The @blocktag decorator can be used as simply as: from djblets.util.decorators import blocktag

@register.tag
@blocktag
def my_block_tag(context, nodelist, some_arg, another_arg, optional_arg=None):
    # ...
    return nodelist.render(context)

Posted at 5:08 a.m. on November 6, 2009

6 Charlie La Mothe says...

Possibly blog bug: tried to preview my note, blog said I needed to enter an email address. Entered one and clicked preview again but it was posted immediately without showing a preview.

Posted at 5:10 a.m. on November 6, 2009

Comments support markdown

Comments are closed.

Comments have been close for this post.

About this post

Posted at 11:55 p.m. on November 3, 2009

Comments: 6

Tags: , , , , , ,

Search Blog


Recent Posts

A simple Perl IRCBot

2 months Ago (Comments: 0)

Correct way to handle default model fields.

3 months, 3 weeks Ago (Comments: 8)

More Posts...

Projects


Friends


Categories


Tag Cloud

abstract aggregator book classbased community conferences conventions core dash debugging deployment designers django djangocon doctest education eurodjangocon fixtures idea ideas iowa kong largeproblems lawrence mediaphormedia mentor middleware migrations music packaging parsing pdb philosophy politics pony post-a-day postaday09 practical pretty production project projects python ramblings reusable review school screencast setuptools software solutions south sphinx ssh students talk teaching template-tags templates templatetags testing testing-series testmaker tip tips tutorial umw unittest

Archive


I may not have gone where I intended to go, but I think I have ended up where I intended to be.

- Douglas Adams