Added support for `Environment.compile_expression`.
--HG--
branch : trunk
diff --git a/CHANGES b/CHANGES
index 3b50265..c2d64e9 100644
--- a/CHANGES
+++ b/CHANGES
@@ -41,6 +41,9 @@
- 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 @@
<jinja-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.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 @@
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 @@
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 @@
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 @@
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 @@
"""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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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