Browse files

there is now a workaround in the compiler that makes sure it's possib…

…le to call things with python keywords. {{ foo(class=42) }} works again

--HG--
branch : trunk
  • Loading branch information...
1 parent de6bf71 commit 2feed1d5e249329b8f09e0392fc741f44e6948a4 @mitsuhiko mitsuhiko committed Apr 26, 2008
Showing with 270 additions and 117 deletions.
  1. +96 −29 examples/bench.py
  2. +123 −55 jinja2/compiler.py
  3. +6 −7 jinja2/environment.py
  4. +2 −2 jinja2/ext.py
  5. +11 −8 jinja2/nodes.py
  6. +11 −2 jinja2/parser.py
  7. +21 −14 jinja2/runtime.py
View
125 examples/bench.py
@@ -1,12 +1,11 @@
+"""
+ This benchmark compares some python templating engines with Jinja 2 so
+ that we get a picture of how fast Jinja 2 is for a semi real world
+ template. If a template engine is not installed the test is skipped.
+"""
import sys
-from django.conf import settings
-settings.configure()
-from django.template import Template as DjangoTemplate, Context as DjangoContext
-from jinja2 import Environment as JinjaEnvironment
-from mako.template import Template as MakoTemplate
-from genshi.template import MarkupTemplate as GenshiTemplate
-from Cheetah.Template import Template as CheetahTemplate
from timeit import Timer
+from jinja2 import Environment as JinjaEnvironment
context = {
'page_title': 'mitsuhiko\'s benchmark',
@@ -51,7 +50,17 @@
</html>\
""")
-django_template = DjangoTemplate("""\
+def test_jinja():
+ jinja_template.render(context)
+
+try:
+ from django.conf import settings
+ settings.configure()
+ from django.template import Template as DjangoTemplate, Context as DjangoContext
+except ImportError:
+ test_django = None
+else:
+ django_template = DjangoTemplate("""\
<!doctype html>
<html>
<head>
@@ -81,7 +90,18 @@
</html>\
""")
-mako_template = MakoTemplate("""\
+ def test_django():
+ c = DjangoContext(context)
+ c['navigation'] = [('index.html', 'Index'), ('downloads.html', 'Downloads'),
+ ('products.html', 'Products')]
+ django_template.render(c)
+
+try:
+ from mako.template import Template as MakoTemplate
+except ImportError:
+ test_mako = None
+else:
+ mako_template = MakoTemplate("""\
<!doctype html>
<html>
<head>
@@ -111,7 +131,15 @@
</html>\
""")
-genshi_template = GenshiTemplate("""\
+ def test_mako():
+ mako_template.render(**context)
+
+try:
+ from genshi.template import MarkupTemplate as GenshiTemplate
+except ImportError:
+ test_genshi = None
+else:
+ genshi_template = GenshiTemplate("""\
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://genshi.edgewall.org/">
<head>
<title>${page_title}</title>
@@ -137,7 +165,15 @@
</html>\
""")
-cheetah_template = CheetahTemplate("""\
+ def test_genshi():
+ genshi_template.generate(**context).render('html', strip_whitespace=False)
+
+try:
+ from Cheetah.Template import Template as CheetahTemplate
+except ImportError:
+ test_cheetah = None
+else:
+ cheetah_template = CheetahTemplate("""\
#import cgi
<!doctype html>
<html>
@@ -168,32 +204,63 @@
</html>\
""", searchList=[dict(context)])
-def test_jinja():
- jinja_template.render(context)
-
-def test_django():
- c = DjangoContext(context)
- c['navigation'] = [('index.html', 'Index'), ('downloads.html', 'Downloads'), ('products.html', 'Products')]
- django_template.render(c)
-
-def test_mako():
- mako_template.render(**context)
+ def test_cheetah():
+ unicode(cheetah_template)
-def test_genshi():
- genshi_template.generate(**context).render('html', strip_whitespace=False)
+try:
+ import tenjin
+except ImportError:
+ test_tenjin = None
+else:
+ tenjin_template = tenjin.Template()
+ tenjin_template.convert("""\
+<!doctype html>
+<html>
+ <head>
+ <title>${page_title}</title>
+ </head>
+ <body>
+ <div class="header">
+ <h1>${page_title}</h1>
+ </div>
+ <ul class="navigation">
+<?py for href, caption in [('index.html', 'Index'), ('downloads.html', 'Downloads'), ('products.html', 'Products')]: ?>
+ <li><a href="${href}">${caption}</a></li>
+<?py #end ?>
+ </ul>
+ <div class="table">
+ <table>
+<?py for row in table: ?>
+ <tr>
+<?py for cell in row: ?>
+ <td>#{cell}</td>
+<?py #end ?>
+ </tr>
+<?py #end ?>
+ </table>
+ </div>
+ </body>
+</html>\
+""")
-def test_cheetah():
- unicode(cheetah_template)
+ def test_tenjin():
+ from tenjin.helpers import escape, to_str
+ tenjin_template.render(context, locals())
-sys.stdout.write('\r%s\n%s\n%s\n' % (
+sys.stdout.write('\r' + '\n'.join((
'=' * 80,
'Template Engine BigTable Benchmark'.center(80),
+ '-' * 80,
+ __doc__,
'-' * 80
-))
-for test in 'jinja', 'mako', 'django', 'genshi', 'cheetah':
+)) + '\n')
+for test in 'jinja', 'tenjin', 'mako', 'django', 'genshi', 'cheetah':
+ if locals()['test_' + test] is None:
+ sys.stdout.write(' %-20s*not installed*\n' % test)
+ continue
t = Timer(setup='from __main__ import test_%s as bench' % test,
stmt='bench()')
sys.stdout.write('> %-20s<running>' % test)
sys.stdout.flush()
- sys.stdout.write('\r %-20s%.4f ms\n' % (test, t.timeit(number=100) / 100))
+ sys.stdout.write('\r %-20s%.4f ms\n' % (test, t.timeit(number=20) / 20))
sys.stdout.write('=' * 80 + '\n')
View
178 jinja2/compiler.py
@@ -10,7 +10,9 @@
"""
from copy import copy
from random import randrange
+from keyword import iskeyword
from cStringIO import StringIO
+from itertools import chain
from jinja2 import nodes
from jinja2.visitor import NodeVisitor, NodeTransformer
from jinja2.exceptions import TemplateAssertionError
@@ -163,14 +165,19 @@ def copy(self):
rv.name_overrides = self.name_overrides.copy()
return rv
- def inspect(self, nodes, hard_scope=False):
- """Walk the node and check for identifiers. If the scope
- is hard (eg: enforce on a python level) overrides from outer
- scopes are tracked differently.
+ def inspect(self, nodes, with_depenencies=False, hard_scope=False):
+ """Walk the node and check for identifiers. If the scope is hard (eg:
+ enforce on a python level) overrides from outer scopes are tracked
+ differently.
+
+ Per default filters and tests (dependencies) are not tracked. That's
+ the case because filters and tests are absolutely immutable and so we
+ can savely use them in closures too. The `Template` and `Block`
+ visitor visits the frame with dependencies to collect them.
"""
visitor = FrameIdentifierVisitor(self.identifiers, hard_scope)
for node in nodes:
- visitor.visit(node)
+ visitor.visit(node, True, with_depenencies)
def inner(self):
"""Return an inner frame."""
@@ -193,41 +200,63 @@ def __init__(self, identifiers, hard_scope):
self.identifiers = identifiers
self.hard_scope = hard_scope
- def visit_Name(self, node):
+ def visit_Name(self, node, visit_ident, visit_deps):
"""All assignments to names go through this function."""
- if node.ctx in ('store', 'param'):
- self.identifiers.declared_locally.add(node.name)
- elif node.ctx == 'load':
- if not self.identifiers.is_declared(node.name, self.hard_scope):
+ if visit_ident:
+ if node.ctx in ('store', 'param'):
+ self.identifiers.declared_locally.add(node.name)
+ elif node.ctx == 'load' and not \
+ self.identifiers.is_declared(node.name, self.hard_scope):
self.identifiers.undeclared.add(node.name)
- def visit_Filter(self, node):
- self.generic_visit(node)
- self.identifiers.filters.add(node.name)
-
- def visit_Test(self, node):
- self.generic_visit(node)
- self.identifiers.tests.add(node.name)
+ def visit_Filter(self, node, visit_ident, visit_deps):
+ if visit_deps:
+ self.generic_visit(node, visit_ident, True)
+ self.identifiers.filters.add(node.name)
- def visit_Macro(self, node):
- self.identifiers.declared_locally.add(node.name)
+ def visit_Test(self, node, visit_ident, visit_deps):
+ if visit_deps:
+ self.generic_visit(node, visit_ident, True)
+ self.identifiers.tests.add(node.name)
- def visit_Import(self, node):
- self.generic_visit(node)
- self.identifiers.declared_locally.add(node.target)
+ def visit_Macro(self, node, visit_ident, visit_deps):
+ if visit_ident:
+ self.identifiers.declared_locally.add(node.name)
- def visit_FromImport(self, node):
- self.generic_visit(node)
- self.identifiers.declared_locally.update(node.names)
+ def visit_Import(self, node, visit_ident, visit_deps):
+ if visit_ident:
+ self.generic_visit(node, True, visit_deps)
+ self.identifiers.declared_locally.add(node.target)
+
+ def visit_FromImport(self, node, visit_ident, visit_deps):
+ if visit_ident:
+ self.generic_visit(node, True, visit_deps)
+ for name in node.names:
+ if isinstance(name, tuple):
+ self.identifiers.declared_locally.add(name[1])
+ else:
+ self.identifiers.declared_locally.add(name)
- def visit_Assign(self, node):
+ def visit_Assign(self, node, visit_ident, visit_deps):
"""Visit assignments in the correct order."""
- self.visit(node.node)
- self.visit(node.target)
+ self.visit(node.node, visit_ident, visit_deps)
+ self.visit(node.target, visit_ident, visit_deps)
+
+ def visit_For(self, node, visit_ident, visit_deps):
+ """Visiting stops at for blocks. However the block sequence
+ is visited as part of the outer scope.
+ """
+ if visit_ident:
+ self.visit(node.iter, True, visit_deps)
+ if visit_deps:
+ for child in node.iter_child_nodes(exclude=('iter',)):
+ self.visit(child, False, True)
- # stop traversing at instructions that have their own scope.
- visit_Block = visit_CallBlock = visit_FilterBlock = \
- visit_For = lambda s, n: None
+ def ident_stop(self, node, visit_ident, visit_deps):
+ if visit_deps:
+ self.generic_visit(node, False, True)
+ visit_CallBlock = visit_FilterBlock = ident_stop
+ visit_Block = lambda s, n, a, b: None
class CompilerExit(Exception):
@@ -344,10 +373,10 @@ def newline(self, node=None, extra=0):
def signature(self, node, frame, have_comma=True, extra_kwargs=None):
"""Writes a function call to the stream for the current node.
Per default it will write a leading comma but this can be
- disabled by setting have_comma to False. If extra_kwargs is
- given it must be a string that represents a single keyword
- argument call that is inserted at the end of the regular
- keyword argument calls.
+ disabled by setting have_comma to False. The extra keyword
+ arguments may not include python keywords otherwise a syntax
+ error could occour. The extra keyword arguments should be given
+ as python dict.
"""
have_comma = have_comma and [True] or []
def touch_comma():
@@ -356,20 +385,53 @@ def touch_comma():
else:
have_comma.append(True)
+ # if any of the given keyword arguments is a python keyword
+ # we have to make sure that no invalid call is created.
+ kwarg_workaround = False
+ for kwarg in chain((x.key for x in node.kwargs), extra_kwargs or ()):
+ if iskeyword(kwarg):
+ kwarg_workaround = True
+ break
+
for arg in node.args:
touch_comma()
self.visit(arg, frame)
- for kwarg in node.kwargs:
- touch_comma()
- self.visit(kwarg, frame)
- if extra_kwargs is not None:
- touch_comma()
- self.write(extra_kwargs)
+
+ if not kwarg_workaround:
+ for kwarg in node.kwargs:
+ touch_comma()
+ self.visit(kwarg, frame)
+ if extra_kwargs is not None:
+ for key, value in extra_kwargs.iteritems():
+ touch_comma()
+ self.write('%s=%s' % (key, value))
if node.dyn_args:
touch_comma()
self.write('*')
self.visit(node.dyn_args, frame)
- if node.dyn_kwargs:
+
+ if kwarg_workaround:
+ touch_comma()
+ if node.dyn_kwargs is not None:
+ self.write('**dict({')
+ else:
+ self.write('**{')
+ for kwarg in node.kwargs:
+ self.write('%r: ' % kwarg.key)
+ self.visit(kwarg.value, frame)
+ self.write(', ')
+ if extra_kwargs is not None:
+ for key, value in extra_kwargs.iteritems():
+ touch_comma()
+ self.write('%r: %s, ' % (key, value))
+ if node.dyn_kwargs is not None:
+ self.write('}, **')
+ self.visit(node.dyn_kwargs, frame)
+ self.write(')')
+ else:
+ self.write('}')
+
+ elif node.dyn_kwargs is not None:
touch_comma()
self.write('**')
self.visit(node.dyn_kwargs, frame)
@@ -448,6 +510,10 @@ def function_scoping(self, node, frame):
func_frame.accesses_caller = False
func_frame.arguments = args = ['l_' + x.name for x in node.args]
+ if 'caller' in func_frame.identifiers.undeclared:
+ func_frame.accesses_caller = True
+ func_frame.identifiers.add_special('caller')
+ args.append('l_caller')
if 'kwargs' in func_frame.identifiers.undeclared:
func_frame.accesses_kwargs = True
func_frame.identifiers.add_special('kwargs')
@@ -456,17 +522,14 @@ def function_scoping(self, node, frame):
func_frame.accesses_varargs = True
func_frame.identifiers.add_special('varargs')
args.append('l_varargs')
- if 'caller' in func_frame.identifiers.undeclared:
- func_frame.accesses_caller = True
- func_frame.identifiers.add_special('caller')
- args.append('l_caller')
return func_frame
# -- Visitors
def visit_Template(self, node, frame=None):
assert frame is None, 'no root frame allowed'
- self.writeline('from jinja2.runtime import *')
+ from jinja2.runtime import __all__ as exported
+ self.writeline('from jinja2.runtime import ' + ', '.join(exported))
self.writeline('name = %r' % self.name)
# do we have an extends tag at all? If not, we can save some
@@ -491,7 +554,7 @@ def visit_Template(self, node, frame=None):
# process the root
frame = Frame()
- frame.inspect(node.body)
+ frame.inspect(node.body, with_depenencies=True)
frame.toplevel = frame.rootlevel = True
self.indent()
self.pull_locals(frame, indent=False)
@@ -513,7 +576,7 @@ def visit_Template(self, node, frame=None):
# at this point we now have the blocks collected and can visit them too.
for name, block in self.blocks.iteritems():
block_frame = Frame()
- block_frame.inspect(block.body)
+ block_frame.inspect(block.body, with_depenencies=True)
block_frame.block = name
block_frame.identifiers.add_special('super')
block_frame.name_overrides['super'] = 'context.super(%r, ' \
@@ -627,21 +690,25 @@ def visit_FromImport(self, node, frame):
self.visit(node.template, frame)
self.write(', %r).include(context)' % self.name)
for name in node.names:
+ if isinstance(name, tuple):
+ name, alias = name
+ else:
+ alias = name
self.writeline('l_%s = getattr(included_template, '
- '%r, missing)' % (name, name))
- self.writeline('if l_%s is missing:' % name)
+ '%r, missing)' % (alias, name))
+ self.writeline('if l_%s is missing:' % alias)
self.indent()
self.writeline('l_%s = environment.undefined(%r %% '
'included_template.name)' %
- (name, 'the template %r does not export '
+ (alias, 'the template %r does not export '
'the requested name ' + repr(name)))
self.outdent()
if frame.toplevel:
- self.writeline('context[%r] = l_%s' % (name, name))
+ self.writeline('context[%r] = l_%s' % (alias, alias))
def visit_For(self, node, frame):
loop_frame = frame.inner()
- loop_frame.inspect(node.iter_child_nodes())
+ loop_frame.inspect(node.iter_child_nodes(exclude=('iter',)))
extended_loop = bool(node.else_) or \
'loop' in loop_frame.identifiers.undeclared
if extended_loop:
@@ -774,7 +841,8 @@ def visit_CallBlock(self, node, frame):
self.writeline('yield ', node)
else:
self.writeline('%s.append(' % frame.buffer, node)
- self.visit_Call(node.call, call_frame, extra_kwargs='caller=caller')
+ self.visit_Call(node.call, call_frame,
+ extra_kwargs={'caller': 'caller'})
if frame.buffer is not None:
self.write(')')
View
13 jinja2/environment.py
@@ -395,21 +395,20 @@ class IncludedTemplate(object):
"""Represents an included template."""
def __init__(self, template, context):
- body = Markup(concat(template.root_render_func(context)))
+ self._body_stream = tuple(template.root_render_func(context))
self.__dict__.update(context.get_exported())
- self._name = template.name
- self._rendered_body = body
+ self.__name__ = template.name
- __html__ = lambda x: x._rendered_body
- __unicode__ = lambda x: unicode(x._rendered_body)
+ __html__ = lambda x: Markup(concat(x._body_stream))
+ __unicode__ = lambda x: unicode(concat(x._body_stream))
def __str__(self):
- return unicode(self._rendered_body).encode('utf-8')
+ return unicode(self).encode('utf-8')
def __repr__(self):
return '<%s %r>' % (
self.__class__.__name__,
- self._name
+ self.__name__
)
View
4 jinja2/ext.py
@@ -13,7 +13,7 @@
from collections import deque
from jinja2 import nodes
from jinja2.environment import get_spontaneous_environment
-from jinja2.runtime import Undefined
+from jinja2.runtime import Undefined, concat
from jinja2.parser import statement_end_tokens
from jinja2.exceptions import TemplateAssertionError
from jinja2.utils import import_string
@@ -190,7 +190,7 @@ def _parse_block(self, parser, allow_pluralize):
else:
assert False, 'internal parser error'
- return referenced, u''.join(buf)
+ return referenced, concat(buf)
def _make_node(self, singular, plural, variables, plural_expr):
"""Generates a useful node from the data provided."""
View
19 jinja2/nodes.py
@@ -92,17 +92,18 @@ def __init__(self, *args, **kw):
raise TypeError('unknown keyword argument %r' %
iter(kw).next())
- def iter_fields(self):
+ def iter_fields(self, exclude=()):
"""Iterate over all fields."""
for name in self.fields:
- try:
- yield name, getattr(self, name)
- except AttributeError:
- pass
+ if name not in exclude:
+ try:
+ yield name, getattr(self, name)
+ except AttributeError:
+ pass
- def iter_child_nodes(self):
+ def iter_child_nodes(self, exclude=()):
"""Iterate over all child nodes."""
- for field, item in self.iter_fields():
+ for field, item in self.iter_fields(exclude):
if isinstance(item, list):
for n in item:
if isinstance(n, Node):
@@ -243,7 +244,7 @@ class Macro(Stmt):
class CallBlock(Stmt):
"""A node that represents am extended macro call."""
- fields = ('call', 'args', 'defaults', 'body')
+ fields = ('call', 'body')
class Set(Stmt):
@@ -279,6 +280,8 @@ class FromImport(Stmt):
start with double underscores (which the parser asserts) this is not a
problem for regular Jinja code, but if this node is used in an extension
extra care must be taken.
+
+ The list of names may contain tuples if aliases are wanted.
"""
fields = ('template', 'names')
View
13 jinja2/parser.py
@@ -13,10 +13,10 @@
from jinja2.exceptions import TemplateSyntaxError
+statement_end_tokens = set(['variable_end', 'block_end', 'in'])
_statement_keywords = frozenset(['for', 'if', 'block', 'extends', 'print',
'macro', 'include', 'from', 'import'])
_compare_operators = frozenset(['eq', 'ne', 'lt', 'lteq', 'gt', 'gteq', 'in'])
-statement_end_tokens = set(['variable_end', 'block_end', 'in'])
_tuple_edge_tokens = set(['rparen']) | statement_end_tokens
@@ -178,8 +178,17 @@ def parse_from(self):
'underscores can not be '
'imported', target.lineno,
self.filename)
- node.names.append(target.name)
self.stream.next()
+ if self.stream.current.test('name:as'):
+ self.stream.next()
+ alias = self.stream.expect('name')
+ if not nodes.Name(alias.value, 'store').can_assign():
+ raise TemplateSyntaxError('can\'t name imported '
+ 'object %r.' % alias.value,
+ alias.lineno, self.filename)
+ node.names.append((target.name, alias.value))
+ else:
+ node.names.append(target.name)
if self.stream.current.type is not 'comma':
break
else:
View
35 jinja2/runtime.py
@@ -9,12 +9,14 @@
:license: GNU GPL.
"""
from types import FunctionType
+from itertools import izip
from jinja2.utils import Markup, partial
from jinja2.exceptions import UndefinedError
+# these variables are exported to the template runtime
__all__ = ['LoopContext', 'StaticLoopContext', 'TemplateContext',
- 'Macro', 'Markup', 'missing', 'concat']
+ 'Macro', 'Markup', 'missing', 'concat', 'izip']
# special singleton representing missing values for the runtime
@@ -34,18 +36,18 @@ class TemplateContext(object):
def __init__(self, environment, parent, name, blocks):
self.parent = parent
- self.vars = {}
+ self.vars = vars = {}
self.environment = environment
self.exported_vars = set()
self.name = name
# bind functions to the context of environment if required
- for name, obj in self.parent.iteritems():
+ for name, obj in parent.iteritems():
if type(obj) is FunctionType:
if getattr(obj, 'contextfunction', 0):
- self.vars[name] = partial(obj, self)
+ vars[name] = partial(obj, self)
elif getattr(obj, 'environmentfunction', 0):
- self.vars[name] = partial(obj, environment)
+ vars[name] = partial(obj, environment)
# create the initial mapping of blocks. Whenever template inheritance
# takes place the runtime will update this mapping with the new blocks
@@ -223,17 +225,18 @@ def __init__(self, environment, func, name, arguments, defaults,
self._func = func
self.name = name
self.arguments = arguments
+ self.argument_count = len(arguments)
self.defaults = defaults
self.catch_kwargs = catch_kwargs
self.catch_varargs = catch_varargs
self.caller = caller
def __call__(self, *args, **kwargs):
- arg_count = len(self.arguments)
- if not self.catch_varargs and len(args) > arg_count:
+ self.argument_count = len(self.arguments)
+ if not self.catch_varargs and len(args) > self.argument_count:
raise TypeError('macro %r takes not more than %d argument(s)' %
(self.name, len(self.arguments)))
- arguments = {}
+ arguments = []
for idx, name in enumerate(self.arguments):
try:
value = args[idx]
@@ -242,24 +245,28 @@ def __call__(self, *args, **kwargs):
value = kwargs.pop(name)
except KeyError:
try:
- value = self.defaults[idx - arg_count]
+ value = self.defaults[idx - self.argument_count]
except IndexError:
value = self._environment.undefined(
'parameter %r was not provided' % name)
- arguments['l_' + name] = value
+ arguments.append(value)
+
+ # it's important that the order of these arguments does not change
+ # if not also changed in the compiler's `function_scoping` method.
+ # the order is caller, keyword arguments, positional arguments!
if self.caller:
caller = kwargs.pop('caller', None)
if caller is None:
caller = self._environment.undefined('No caller defined')
- arguments['l_caller'] = caller
+ arguments.append(caller)
if self.catch_kwargs:
- arguments['l_kwargs'] = kwargs
+ arguments.append(kwargs)
elif kwargs:
raise TypeError('macro %r takes no keyword argument %r' %
(self.name, iter(kwargs).next()))
if self.catch_varargs:
- arguments['l_varargs'] = args[arg_count:]
- return self._func(**arguments)
+ arguments.append(args[self.argument_count:])
+ return self._func(*arguments)
def __repr__(self):
return '<%s %s>' % (

0 comments on commit 2feed1d

Please sign in to comment.