Today I wrote a little Django to Jinja2 template converter. While it can translate most of the builtin template tags into Jinja constructs it doesn't fully automate the process because you have to extend it for your own custom tags and it doesn't adapt your templates to the changed semantics. And these differences in semantics (and the underlying architecture) are something I want to discuss a bit here. Whenever someone mentions Jinja in the Django IRC channel you can be pretty sure that someone else will write something like "... if you don't have your logic under control" into the channel and position Jinja in the corner where failed concepts lurk around. Of course Jinja leaves more room for abuse than Django does⦠But this time this isn't actually what I want to talk about here :) First of all a small disclaimer: This article covers Jinja 2.0 and Django 1.0.
Hello {{ name|upper }}!
This is one of those templates that look and work exactly the same in Jinja and Django. First have a look what tokens the Jinja2 tokenizer yields:
>>> from jinja2 import Environment
>>> for token in Environment().lex("Hello {{ name|upper }}!"):
... print token
...
(1, 'data', u'Hello ')
(1, 'variable_begin', u'{{')
(1, 'whitespace', u' ')
(1, 'name', u'name')
(1, 'operator', u'|')
(1, 'name', u'upper')
(1, 'whitespace', u' ')
(1, 'variable_end', u'}}')
(1, 'data', u'!')
And here what Django outputs:
>>> from django.template import Lexer, StringOrigin
>>> origin = StringOrigin("Hello {{ name|upper }}!")
>>> for token in Lexer(origin.source, origin).tokenize():
... print token
...
So as you can see, whereas Jinja creates very tiny bits of the input string, Django only distinguishes between four different kinds of tokens: text, variables, blocks and line comments. While this is a lot easier to implement for the developer of the template engine, it doesn't have any advantages over the concept Jinja has chosen. It actually has a lot of negative side effects. For example it's impossible to write {{ '{% a block in a variable %}' }}
in Django. (I know you can use templatetag openblock
and templatetag closeblock
, but beautiful is something else). It also has the huge disadvantage that tag has to split up the contents of the tag itself which often causes different semantics and syntactic specialities in tags and that for the developer of such a tag it's hugely more work to do that. The former is probably the worse part of it. For example the url
tag in Django takes arguments separated by commas (that are not even allowed to be followed by whitespace) but cycle
expects arguments to be separated by whitespace.
The root of the problem is definitively the weak lexer of the Django template engine and I really think that should be replaced by something that yields proper tokens. That would simplify things for tag developers a lot and also lead to a more intuitive experience for template designers that can expect the same basic syntax rules everywhere.
{{ 1 + 2 + 3 }}
and the "cursor" of the parser is right before the first digit in the simple calculation, the parser parses this into Add(Add(Const(1), Const(2)), Const(3))
. This is useful because the developer of a custom tag doesn't have to deal with that, the Parser already knows how an Expression looks like. Now you could argue that calculations don't belong into templates and my point is not valid, but even in the Django template language you have expressions.
The only expression Django knows about are filter expressions. In Jinja2 the parser converts {{ var|escape|upper }}
into a proper filter node for you. Django provides a TokenParser for that which can do something very similar. However that parser is not used in every tag and has it's limitations too. Furthermore was that parser introduced long after the initial implementation of the template language which means that many core tags don't use it. Because in Jinja it's a matter of calling parser.parse_expression()
to get an expression called, the same requires a lot more typing and checking in Django. A lot of the tags that lurk around in various pastebins or websites don't even support filters but only variables in some places. Even worse, some people are evaluating the part between the block braces using eval() against the context object.
Again, this simple design of the parser helps nobody but the developers of the template engine. I've seen enough Django projects by now that have to write their own template tags because the core tags just don't do what they need, and in any case the process of developing the tag was more painful than it had to be.
With a newly implemented lexer that yields all tokens of a block or variable one after another a new parser could be implemented based on the design of the Jinja one. And by doing that one has the chance to specify some operators. Nobody is harmed if the templating language supports {% user.karma >= 20 and user.karma < 40 %}
and that hardly counts as logic in templates.
{% cycle "odd" "even" %}
inside a loop that iterates over 5 items. Start up your Django server, go to that page and hit refresh over and over again. You will notice that one time the output starts with "even", one time with "odd". The reason for that is that the node tree is shared. If you start up the application on a multithreaded server and hit it with tons of ab/siege requests you will even notice that you often get lists that look like "even even even odd even odd" or something similar. And that's not only for super, that also affects block tags. If you extend from a variable template block.super will probably point to a totally different template when the server is under high load.
This is unacceptable behaviour and should be fixed. I'm currently wiring up a patch for that as the ticket was changed from "thread in-safety" to "reset cycle tag after iteration" which shows that at least the editor of that ticket doesn't get the problem and is lurking around in the Django trac for too long.
The evaluation of a Jinja template doesn't work over the ast but by evaluating the previously generated Bytecode. And yes, it's thread safe but that's not the point.
test_jinja
/ test_django
are the functions that invoke the test rendering process. The reason why the Jinja graph is not joined is that the invocation of the bytecode Jinja generates doesn't count as regular call and the profiler is unable to connect those. So you have to think yourself the line between render -> and root. In both cases the template engine rendered the templates already a few houndred times before the profiler profiles one single call, so the templates are already parsed (and compiled in Jinja's case). If you are wondering why there seems to be the template parser active in the django graph, I'm wondering that too. You can have a look at the benchmark to see how it works. If you think the template parser invocation in that profiler output comes from the djangoext.py, you are wrong. That's what I suspected too. Turns out, even if I don't use the loader there but preload the template, it's still happening. So I take that as normal behaviour cause by template inheritance or something like that.
That profiler output shows only the rendering of a pretty normal template situation. Now imagine you have a query somewhere there because of django's lazy querysets. Now try to figure out what the heck is going on. I was running the profiler against the changeset rendering page in bitbucket and had a call tree so complex that it was impossible for me to figure out what was going on because of 400ms for that page, 300ms were spend in the template. Just that the template invoked mercurials diffing system. That's insane. That AST evaluator is seriously killing every possibility to get useful profiler information out of the system.
# these two variables (users and user) are used in the template
# without being initialized in the template.
l_users = context.resolve('users')
l_user = context.resolve('user')
yield u'
buffer.append(u'
context.resolve
. And that's something Django does for every variable access. Imagine you are three levels inside your template (a with, a loop and another loop) and now you try to access a variable inside your loop that was passed to the template. Django has to traverse the context four levels up to get to that data. That's very expensive. Especially compared to what Jinja does. A local variable in Python as used by Jinja does not end up in a dictionary unless locals() is called or frame.f_locals is accessed. And as long as it's not in a dictionary no hash code is calculated and no dict resizing takes place. Instead the name gets a number and a place to be. When the function is called Python has already reserved space for that variable. These fast-locals (the internal name for those) are blazingly fast compared to normal dict lookup already, and even faster compared to what django does to resolve variables and you can't get that without creating bytecode or generating Python code and compiling that.