From ba6e25a882a3ac862fc513e6c9cbfc83536c54ce Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 2 Nov 2008 15:58:14 +0100 Subject: [PATCH] Added support for `Environment.compile_expression`. --HG-- branch : trunk --- CHANGES | 3 ++ docs/api.rst | 2 +- jinja2/environment.py | 73 ++++++++++++++++++++++++++++++++++++++++--- jinja2/lexer.py | 13 +++++--- jinja2/nodes.py | 5 --- jinja2/parser.py | 5 +-- jinja2/utils.py | 6 ++++ tests/test_various.py | 13 +++++++- 8 files changed, 102 insertions(+), 18 deletions(-) diff --git a/CHANGES b/CHANGES index 3b50265..c2d64e9 100644 --- a/CHANGES +++ b/CHANGES @@ -41,6 +41,9 @@ Version 2.1 - added a joining helper called `joiner`. +- added a `compile_expression` method to the environment that allows compiling + of Jinja expressions into callable Python objects. + Version 2.0 ----------- (codename jinjavitus, released on July 17th 2008) diff --git a/docs/api.rst b/docs/api.rst index ef53321..a12b6a1 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -115,7 +115,7 @@ useful if you want to dig deeper into Jinja2 or :ref:`develop extensions `. .. autoclass:: Environment([options]) - :members: from_string, get_template, join_path, extend + :members: from_string, get_template, join_path, extend, compile_expression .. attribute:: shared diff --git a/jinja2/environment.py b/jinja2/environment.py index 519e9ec..4a9c9d1 100644 --- a/jinja2/environment.py +++ b/jinja2/environment.py @@ -9,6 +9,7 @@ :license: BSD, see LICENSE for more details. """ import sys +from jinja2 import nodes from jinja2.defaults import * from jinja2.lexer import get_lexer, TokenStream from jinja2.parser import Parser @@ -16,7 +17,8 @@ from jinja2.optimizer import optimize from jinja2.compiler import generate from jinja2.runtime import Undefined, Context from jinja2.exceptions import TemplateSyntaxError -from jinja2.utils import import_string, LRUCache, Markup, missing, concat +from jinja2.utils import import_string, LRUCache, Markup, missing, \ + concat, consume # for direct template usage we have up to ten living environments @@ -379,12 +381,12 @@ class Environment(object): return reduce(lambda s, e: e.preprocess(s, name, filename), self.extensions.itervalues(), unicode(source)) - def _tokenize(self, source, name, filename=None): + def _tokenize(self, source, name, filename=None, state=None): """Called by the parser to do the preprocessing and filtering for all the extensions. Returns a :class:`~jinja2.lexer.TokenStream`. """ source = self.preprocess(source, name, filename) - stream = self.lexer.tokenize(source, name, filename) + stream = self.lexer.tokenize(source, name, filename, state) for ext in self.extensions.itervalues(): stream = ext.filter_stream(stream) if not isinstance(stream, TokenStream): @@ -407,8 +409,8 @@ class Environment(object): if isinstance(source, basestring): source = self.parse(source, name, filename) if self.optimized: - node = optimize(source, self) - source = generate(node, self, name, filename) + source = optimize(source, self) + source = generate(source, self, name, filename) if raw: return source if filename is None: @@ -417,6 +419,48 @@ class Environment(object): filename = filename.encode('utf-8') return compile(source, filename, 'exec') + def compile_expression(self, source, undefined_to_none=True): + """A handy helper method that returns a callable that accepts keyword + arguments that appear as variables in the expression. If called it + returns the result of the expression. + + This is useful if applications want to use the same rules as Jinja + in template "configuration files" or similar situations. + + Example usage: + + >>> env = Environment() + >>> expr = env.compile_expression('foo == 42') + >>> expr(foo=23) + False + >>> expr(foo=42) + True + + Per default the return value is converted to `None` if the + expression returns an undefined value. This can be changed + by setting `undefined_to_none` to `False`. + + >>> env.compile_expression('var')() is None + True + >>> env.compile_expression('var', undefined_to_none=False)() + Undefined + + **new in Jinja 2.1** + """ + parser = Parser(self, source, state='variable') + try: + expr = parser.parse_expression() + if not parser.stream.eos: + raise TemplateSyntaxError('chunk after expression', + parser.stream.current.lineno, + None, None) + except TemplateSyntaxError, e: + e.source = source + raise e + body = [nodes.Assign(nodes.Name('result', 'store'), expr, lineno=1)] + template = self.from_string(nodes.Template(body, lineno=1)) + return TemplateExpression(template, undefined_to_none) + def join_path(self, template, parent): """Join a template with the parent. By default all the lookups are relative to the loader root so this method returns the `template` @@ -699,6 +743,25 @@ class TemplateModule(object): return '<%s %s>' % (self.__class__.__name__, name) +class TemplateExpression(object): + """The :meth:`jinja2.Environment.compile_expression` method returns an + instance of this object. It encapsulates the expression-like access + to the template with an expression it wraps. + """ + + def __init__(self, template, undefined_to_none): + self._template = template + self._undefined_to_none = undefined_to_none + + def __call__(self, *args, **kwargs): + context = self._template.new_context(dict(*args, **kwargs)) + consume(self._template.root_render_func(context)) + rv = context.vars['result'] + if self._undefined_to_none and isinstance(rv, Undefined): + rv = None + return rv + + class TemplateStream(object): """A template stream works pretty much like an ordinary python generator but it can buffer multiple items to reduce the number of total iterations. diff --git a/jinja2/lexer.py b/jinja2/lexer.py index 14b7110..6b26983 100644 --- a/jinja2/lexer.py +++ b/jinja2/lexer.py @@ -375,10 +375,10 @@ class Lexer(object): """Called for strings and template data to normlize it to unicode.""" return newline_re.sub(self.newline_sequence, value) - def tokenize(self, source, name=None, filename=None): + def tokenize(self, source, name=None, filename=None, state=None): """Calls tokeniter + tokenize and wraps it in a token stream. """ - stream = self.tokeniter(source, name, filename) + stream = self.tokeniter(source, name, filename, state) return TokenStream(self.wrap(stream, name, filename), name, filename) def wrap(self, stream, name=None, filename=None): @@ -426,7 +426,7 @@ class Lexer(object): token = operators[value] yield Token(lineno, token, value) - def tokeniter(self, source, name, filename=None): + def tokeniter(self, source, name, filename=None, state=None): """This method tokenizes the text and returns the tokens in a generator. Use this method if you just want to tokenize a template. """ @@ -434,7 +434,12 @@ class Lexer(object): pos = 0 lineno = 1 stack = ['root'] - statetokens = self.rules['root'] + if state is not None and state != 'root': + assert state in ('variable', 'block'), 'invalid state' + stack.append(state + '_begin') + else: + state = 'root' + statetokens = self.rules[stack[-1]] source_length = len(source) balancing_stack = [] diff --git a/jinja2/nodes.py b/jinja2/nodes.py index ec2ed3e..405622a 100644 --- a/jinja2/nodes.py +++ b/jinja2/nodes.py @@ -262,11 +262,6 @@ class CallBlock(Stmt): fields = ('call', 'args', 'defaults', 'body') -class Set(Stmt): - """Allows defining own variables.""" - fields = ('name', 'expr') - - class FilterBlock(Stmt): """Node for filter sections.""" fields = ('body', 'filter') diff --git a/jinja2/parser.py b/jinja2/parser.py index d365d4c..d6f1b36 100644 --- a/jinja2/parser.py +++ b/jinja2/parser.py @@ -23,9 +23,10 @@ class Parser(object): extensions and can be used to parse expressions or statements. """ - def __init__(self, environment, source, name=None, filename=None): + def __init__(self, environment, source, name=None, filename=None, + state=None): self.environment = environment - self.stream = environment._tokenize(source, name, filename) + self.stream = environment._tokenize(source, name, filename, state) self.name = name self.filename = filename self.closed = False diff --git a/jinja2/utils.py b/jinja2/utils.py index 249e363..480c086 100644 --- a/jinja2/utils.py +++ b/jinja2/utils.py @@ -136,6 +136,12 @@ def is_undefined(obj): return isinstance(obj, Undefined) +def consume(iterable): + """Consumes an iterable without doing anything with it.""" + for event in iterable: + pass + + def clear_caches(): """Jinja2 keeps internal caches for environments and lexers. These are used so that Jinja2 doesn't have to recreate environments and lexers all diff --git a/tests/test_various.py b/tests/test_various.py index aab5e76..5a01037 100644 --- a/tests/test_various.py +++ b/tests/test_various.py @@ -8,7 +8,7 @@ """ import gc from py.test import raises -from jinja2 import escape +from jinja2 import escape, is_undefined from jinja2.utils import Cycler from jinja2.exceptions import TemplateSyntaxError @@ -97,3 +97,14 @@ def test_cycler(): assert c.current == 2 c.reset() assert c.current == 1 + + +def test_expressions(env): + expr = env.compile_expression("foo") + assert expr() is None + assert expr(foo=42) == 42 + expr2 = env.compile_expression("foo", undefined_to_none=False) + assert is_undefined(expr2()) + + expr = env.compile_expression("42 + foo") + assert expr(foo=42) == 84