there is now a workaround in the compiler that makes sure it's possible to call things with python keywords. {{ foo(class=42) }} works again

--HG--
branch : trunk
This commit is contained in:
Armin Ronacher 2008-04-26 16:26:52 +02:00
parent de6bf71e8f
commit 2feed1d5e2
7 changed files with 269 additions and 116 deletions

View File

@ -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 @@ jinja_template = JinjaEnvironment(
</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 @@ django_template = DjangoTemplate("""\
</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 @@ mako_template = MakoTemplate("""\
</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 @@ genshi_template = GenshiTemplate("""\
</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 @@ cheetah_template = CheetahTemplate("""\
</html>\
""", searchList=[dict(context)])
def test_jinja():
jinja_template.render(context)
def test_cheetah():
unicode(cheetah_template)
def test_django():
c = DjangoContext(context)
c['navigation'] = [('index.html', 'Index'), ('downloads.html', 'Downloads'), ('products.html', 'Products')]
django_template.render(c)
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_mako():
mako_template.render(**context)
def test_tenjin():
from tenjin.helpers import escape, to_str
tenjin_template.render(context, locals())
def test_genshi():
genshi_template.generate(**context).render('html', strip_whitespace=False)
def test_cheetah():
unicode(cheetah_template)
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 File

@ -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 @@ class Frame(object):
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 @@ class FrameIdentifierVisitor(NodeVisitor):
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_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_Test(self, node):
self.generic_visit(node)
self.identifiers.tests.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_Macro(self, node):
self.identifiers.declared_locally.add(node.name)
def visit_Macro(self, node, visit_ident, visit_deps):
if visit_ident:
self.identifiers.declared_locally.add(node.name)
def visit_Import(self, node):
self.generic_visit(node)
self.identifiers.declared_locally.add(node.target)
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):
self.generic_visit(node)
self.identifiers.declared_locally.update(node.names)
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)
# stop traversing at instructions that have their own scope.
visit_Block = visit_CallBlock = visit_FilterBlock = \
visit_For = lambda s, n: None
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)
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 @@ class CodeGenerator(NodeVisitor):
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 @@ class CodeGenerator(NodeVisitor):
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 @@ class CodeGenerator(NodeVisitor):
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 @@ class CodeGenerator(NodeVisitor):
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 @@ class CodeGenerator(NodeVisitor):
# 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 @@ class CodeGenerator(NodeVisitor):
# 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 @@ class CodeGenerator(NodeVisitor):
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 @@ class CodeGenerator(NodeVisitor):
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 File

@ -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 File

@ -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 @@ class TransExtension(Extension):
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 File

@ -92,17 +92,18 @@ class Node(object):
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 File

@ -13,10 +13,10 @@ from jinja2 import nodes
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 @@ class Parser(object):
'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 File

@ -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 @@ class Macro(object):
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 @@ class Macro(object):
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>' % (