From: Armin Ronacher Date: Sat, 26 Apr 2008 14:26:52 +0000 (+0200) Subject: there is now a workaround in the compiler that makes sure it's possible to call thing... X-Git-Tag: 2.0rc1~132 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=2feed1d5e249329b8f09e0392fc741f44e6948a4;p=jinja2.git 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 --- diff --git a/examples/bench.py b/examples/bench.py index 9073758..287f831 100644 --- a/examples/bench.py +++ b/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 @@ jinja_template = JinjaEnvironment( \ """) -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("""\ @@ -81,7 +90,18 @@ django_template = DjangoTemplate("""\ \ """) -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("""\ @@ -111,7 +131,15 @@ mako_template = MakoTemplate("""\ \ """) -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("""\ ${page_title} @@ -137,7 +165,15 @@ genshi_template = GenshiTemplate("""\ \ """) -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 @@ -168,32 +204,63 @@ cheetah_template = CheetahTemplate("""\ \ """, 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("""\ + + + + ${page_title} + + +
+

${page_title}

+
+ +
+ + + + + + + + +
#{cell}
+
+ +\ +""") -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' % 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') diff --git a/jinja2/compiler.py b/jinja2/compiler.py index bc5163b..8541cba 100644 --- a/jinja2/compiler.py +++ b/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 @@ 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_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 @@ 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(')') diff --git a/jinja2/environment.py b/jinja2/environment.py index 5325c2b..f61c740 100644 --- a/jinja2/environment.py +++ b/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__ ) diff --git a/jinja2/ext.py b/jinja2/ext.py index 2c61bb0..e480b00 100644 --- a/jinja2/ext.py +++ b/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 @@ 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.""" diff --git a/jinja2/nodes.py b/jinja2/nodes.py index 3aed350..69a156f 100644 --- a/jinja2/nodes.py +++ b/jinja2/nodes.py @@ -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') diff --git a/jinja2/parser.py b/jinja2/parser.py index daa7a0d..34a3140 100644 --- a/jinja2/parser.py +++ b/jinja2/parser.py @@ -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: diff --git a/jinja2/runtime.py b/jinja2/runtime.py index 8f0e1cc..11924cf 100644 --- a/jinja2/runtime.py +++ b/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 @@ 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>' % (