From 8edbe4959f1c4b0d0daa5cf94dd534723578a1a6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 10 Apr 2008 20:43:43 +0200 Subject: [PATCH] fixed one bug with blocks, one to go --HG-- branch : trunk --- jinja2/compiler.py | 47 ++++++++++++++++++++++++++++++++++--------- jinja2/environment.py | 24 +++++++++------------- jinja2/filters.py | 22 ++++++-------------- jinja2/optimizer.py | 30 ++++++++++++++++++++++++++- jinja2/parser.py | 6 ++++-- jinja2/runtime.py | 14 ++++++++++--- jinja2/utils.py | 13 ++++++++++++ test.py | 27 +++++++++---------------- 8 files changed, 121 insertions(+), 62 deletions(-) diff --git a/jinja2/compiler.py b/jinja2/compiler.py index 44087f1..48c9d99 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -257,11 +257,17 @@ class CodeGenerator(NodeVisitor): assert frame is None, 'no root frame allowed' self.writeline('from jinja2.runtime import *') self.writeline('filename = %r' % self.filename) - self.writeline('template_context = TemplateContext(global_context, ' - 'filename)') + + # find all blocks + for block in node.find_all(nodes.Block): + if block.name in self.blocks: + raise TemplateAssertionError('block %r defined twice' % + block.name, block.lineno, + self.filename) + self.blocks[block.name] = block # generate the root render function. - self.writeline('def root(context=template_context):', extra=1) + self.writeline('def root(context):', extra=1) self.indent() self.writeline('parent_root = None') self.outdent() @@ -285,17 +291,13 @@ class CodeGenerator(NodeVisitor): block_frame = Frame() block_frame.inspect(block.body) block_frame.block = name + print block_frame.identifiers.__dict__ self.writeline('def block_%s(context):' % name, block, 1) self.pull_locals(block_frame) self.blockvisit(block.body, block_frame, True) def visit_Block(self, node, frame): """Call a block and register it for the template.""" - if node.name in self.blocks: - raise TemplateAssertionError("the block '%s' was already defined" % - node.name, node.lineno, - self.filename) - self.blocks[node.name] = node self.writeline('for event in block_%s(context):' % node.name) self.indent() self.writeline('yield event') @@ -429,6 +431,11 @@ class CodeGenerator(NodeVisitor): def visit_Output(self, node, frame): self.newline(node) + if self.environment.finalize is unicode: + finalizer = 'unicode' + else: + finalizer = 'context.finalize' + # try to evaluate as many chunks as possible into a static # string at compile time. body = [] @@ -450,7 +457,7 @@ class CodeGenerator(NodeVisitor): self.writeline('yield %s' % repr(u''.join(item))) else: self.newline(item) - self.write('yield unicode(') + self.write('yield %s(' % finalizer) self.visit(item, frame) self.write(')') @@ -469,7 +476,11 @@ class CodeGenerator(NodeVisitor): for idx, argument in enumerate(arguments): if idx: self.write(', ') + if finalizer != 'unicode': + self.write('(') self.visit(argument, frame) + if finalizer != 'unicode': + self.write(')') self.write(idx == 0 and ',)' or ')') def visit_Assign(self, node, frame): @@ -514,6 +525,24 @@ class CodeGenerator(NodeVisitor): self.visit(item, frame) self.write(idx == 0 and ',)' or ')') + def visit_List(self, node, frame): + self.write('[') + for idx, item in enumerate(node.items): + if idx: + self.write(', ') + self.visit(item, frame) + self.write(']') + + def visit_Dict(self, node, frame): + self.write('{') + for idx, item in enumerate(node.items): + if idx: + self.write(', ') + self.visit(item.key, frame) + self.write(': ') + self.visit(item.value, frame) + self.write('}') + def binop(operator): def visitor(self, node, frame): self.write('(') diff --git a/jinja2/environment.py b/jinja2/environment.py index 77e0d03..8ba4fce 100644 --- a/jinja2/environment.py +++ b/jinja2/environment.py @@ -10,13 +10,11 @@ """ from jinja2.lexer import Lexer from jinja2.parser import Parser -from jinja2.runtime import Undefined from jinja2.defaults import DEFAULT_FILTERS, DEFAULT_TESTS, DEFAULT_NAMESPACE class Environment(object): - """ - The Jinja environment. + """The Jinja environment. The core component of Jinja is the `Environment`. It contains important shared variables like configuration, filters, tests, @@ -32,8 +30,7 @@ class Environment(object): comment_end_string='#}', trim_blocks=False, template_charset='utf-8'): - """ - Here the possible initialization parameters: + """Here the possible initialization parameters: ========================= ============================================ `block_start_string` the string marking the begin of a block. @@ -68,26 +65,25 @@ class Environment(object): self.tests = DEFAULT_TESTS.copy() self.globals = DEFAULT_NAMESPACE.copy() - # the factory that creates the undefined object - self.undefined_factory = Undefined + # if no finalize function/method exists we default to unicode. The + # compiler check if the finalize attribute *is* unicode, if yes no + # finalizaion is written where it can be avoided. + if not hasattr(self, 'finalize'): + self.finalize = unicode # create lexer self.lexer = Lexer(self) def parse(self, source, filename=None): - """ - Parse the sourcecode and return the abstract syntax tree. This tree - of nodes is used by the `translators`_ to convert the template into + """Parse the sourcecode and return the abstract syntax tree. This tree + of nodes is used by the compiler to convert the template into executable source- or bytecode. - - .. _translators: translators.txt """ parser = Parser(self, source, filename) return parser.parse() def lex(self, source, filename=None): - """ - Lex the given sourcecode and return a generator that yields tokens. + """Lex the given sourcecode and return a generator that yields tokens. The stream returned is not usable for Jinja but can be used if Jinja templates should be processed by other tools (for example syntax highlighting etc) diff --git a/jinja2/filters.py b/jinja2/filters.py index 8b66ce8..1c3ffcb 100644 --- a/jinja2/filters.py +++ b/jinja2/filters.py @@ -15,15 +15,14 @@ try: except ImportError: itemgetter = lambda a: lambda b: b[a] from urllib import urlencode, quote -from jinja.utils import escape +from jinja2.utils import escape _striptags_re = re.compile(r'(|<[^>]*>)') def contextfilter(f): - """ - Decorator for marking context dependent filters. The current context + """Decorator for marking context dependent filters. The current context argument will be passed as first argument. """ f.contextfilter = True @@ -60,17 +59,13 @@ def do_replace(s, old, new, count=None): def do_upper(s): - """ - Convert a value to uppercase. - """ - return s.upper() + """Convert a value to uppercase.""" + return unicode(s).upper() def do_lower(s): - """ - Convert a value to lowercase. - """ - return s.lower() + """Convert a value to lowercase.""" + return unicode(s).lower() def do_escape(s, attribute=False): @@ -82,11 +77,6 @@ def do_escape(s, attribute=False): This method will have no effect it the value is already escaped. """ - # XXX: Does this still exists? - #if isinstance(s, TemplateData): - # return s - if hasattr(s, '__html__'): - return s.__html__() return escape(unicode(s), attribute) diff --git a/jinja2/optimizer.py b/jinja2/optimizer.py index e16961f..167f6eb 100644 --- a/jinja2/optimizer.py +++ b/jinja2/optimizer.py @@ -26,6 +26,7 @@ from jinja2.runtime import subscribe, LoopContext class ContextStack(object): """Simple compile time context implementation.""" + undefined = object() def __init__(self, initial=None): self.stack = [{}] @@ -44,10 +45,24 @@ class ContextStack(object): except KeyError: return default + def undef(self, name): + if name in self: + self[name] = self.undefined + + def __contains__(self, key): + try: + self[key] + except KeyError: + return False + return True + def __getitem__(self, key): for level in reversed(self.stack): if key in level: - return level[key] + rv = level[key] + if rv is self.undefined: + raise KeyError(key) + return rv raise KeyError(key) def __setitem__(self, key, value): @@ -66,6 +81,13 @@ class Optimizer(NodeTransformer): def visit_Block(self, node, context): return self.generic_visit(node, context.blank()) + def visit_Macro(self, node, context): + context.push() + try: + return self.generic_visit(node, context) + finally: + context.pop() + def visit_For(self, node, context): """Loop unrolling for iterable constant values.""" try: @@ -73,6 +95,9 @@ class Optimizer(NodeTransformer): # we only unroll them if they have a length and are iterable iter(iterable) len(iterable) + # we also don't want unrolling if macros are defined in it + if node.find(nodes.Macro) is not None: + raise TypeError() except (nodes.Impossible, TypeError): return self.generic_visit(node, context) @@ -124,6 +149,9 @@ class Optimizer(NodeTransformer): def visit_Name(self, node, context): if node.ctx != 'load': + # something overwrote the variable, we can no longer use + # the constant from the context + context.undef(node.name) return node try: return nodes.Const.from_untrusted(context[node.name], diff --git a/jinja2/parser.py b/jinja2/parser.py index a7f0e98..74cc421 100644 --- a/jinja2/parser.py +++ b/jinja2/parser.py @@ -55,7 +55,6 @@ class Parser(object): if token_type in _statement_keywords: return getattr(self, 'parse_' + token_type)() elif token_type is 'call': - self.stream.next() return self.parse_call_block() lineno = self.stream.current expr = self.parse_tuple() @@ -179,7 +178,10 @@ class Parser(object): def parse_call_block(self): node = nodes.CallBlock(lineno=self.stream.expect('call').lineno) - node.call = self.parse_call() + node.call = self.parse_expression() + if not isinstance(node.call, nodes.Call): + raise TemplateSyntaxError('expected call', node.lineno, + self.filename) node.body = self.parse_statements(('endcall',), drop_needle=True) return node diff --git a/jinja2/runtime.py b/jinja2/runtime.py index 5101d0f..b80a488 100644 --- a/jinja2/runtime.py +++ b/jinja2/runtime.py @@ -33,9 +33,17 @@ def subscribe(obj, argument): return Undefined(obj, argument) +class TemplateData(unicode): + """Marks data as "coming from the template". This is used to let the + system know that this data is already processed if a finalization is + used.""" + + def __html__(self): + return self + + class TemplateContext(dict): - """ - Holds the variables of the local template or of the global one. It's + """Holds the variables of the local template or of the global one. It's not save to use this class outside of the compiled code. For example update and other methods will not work as they seem (they don't update the exported variables for example). @@ -177,7 +185,7 @@ class Macro(object): arguments['l_' + name] = value if self.catch_all: arguments['l_arguments'] = kwargs - return u''.join(self.func(**arguments)) + return TemplateData(u''.join(self.func(**arguments))) class Undefined(object): diff --git a/jinja2/utils.py b/jinja2/utils.py index 3f6395e..23a3b15 100644 --- a/jinja2/utils.py +++ b/jinja2/utils.py @@ -8,3 +8,16 @@ :copyright: 2008 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ + + +def escape(obj, attribute=False): + """HTML escape an object.""" + if hasattr(obj, '__html__'): + return obj.__html__() + s = unicode(obj) \ + .replace('&', '&') \ + .replace('>', '>') \ + .replace('<', '<') + if attribute: + s = s.replace('"', '"') + return s diff --git a/test.py b/test.py index 7ab5b96..0629c14 100644 --- a/test.py +++ b/test.py @@ -5,24 +5,17 @@ from jinja2.compiler import generate env = Environment() ast = env.parse(""" -{% block body %} - {% b = 23 %} - {% macro foo(a) %}[{{ a }}|{{ b }}|{{ c }}]{% endmacro %} - {% for item in seq %} - {{ foo(item) }} - {%- endfor %} +hallo +{% block root %} + inhalt + {% x = 3 %} + {% block inner %} + {% x = x + 2 %} + {{ x }} + {% endblock %} + {{ x }} {% endblock %} +ende """) -print ast -print source = generate(ast, env, "foo.html") print source -print - -# execute the template -code = compile(source, 'jinja://foo.html', 'exec') -context = {'seq': range(5), 'c': 'foobar'} -namespace = {'global_context': context} -exec code in namespace -for event in namespace['root'](context): - sys.stdout.write(event) -- 2.26.2