Added support for `Environment.compile_expression`.
authorArmin Ronacher <armin.ronacher@active-4.com>
Sun, 2 Nov 2008 14:58:14 +0000 (15:58 +0100)
committerArmin Ronacher <armin.ronacher@active-4.com>
Sun, 2 Nov 2008 14:58:14 +0000 (15:58 +0100)
--HG--
branch : trunk

CHANGES
docs/api.rst
jinja2/environment.py
jinja2/lexer.py
jinja2/nodes.py
jinja2/parser.py
jinja2/utils.py
tests/test_various.py

diff --git a/CHANGES b/CHANGES
index 3b50265799f9122fe5a1b31d34e1b101b0693ec7..c2d64e97df140853db316e2a89e0b883f138dadf 100644 (file)
--- 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)
index ef53321bf674283ca78baa77afa8df8a0aa81f82..a12b6a1916532d5ce945bf7c6ee6135af36e5fb8 100644 (file)
@@ -115,7 +115,7 @@ useful if you want to dig deeper into Jinja2 or :ref:`develop extensions
 <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
 
index 519e9ecf5266e290927e2c2f0095f16af010eff2..4a9c9d102387bdb40e820c3a7b8c28310fe2dd73 100644 (file)
@@ -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.
index 14b7110e43a14fdc42de35dd962b8d832bdff4b4..6b2698365c130a0c17eaed2f9f918a90e6fae424 100644 (file)
@@ -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 = []
index ec2ed3e68ea318101615d9d0c83af2f6e4c9c709..405622a9de0b940aa7f7f1d2883d783dc9a58ae3 100644 (file)
@@ -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')
index d365d4c0d18acdd8298a76fb5e59f40656e1c795..d6f1b3647d96e909033eb358294f1832c07a400e 100644 (file)
@@ -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
index 249e36309d3552b1789ad2011b92f5082c18d3de..480c08662495f709a4a62082f224f27ed62e0646 100644 (file)
@@ -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
index aab5e7633ad4e8c5cbd2222ea89e25fc40ac6995..5a01037c0e104928e681145121f92c6a08872313 100644 (file)
@@ -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