Finished support for newstyle gettext translations
[jinja2.git] / jinja2 / nodes.py
index 9eb54606db217224e5a7aa217919196ec9a41036..8b5f89a4730fc643581c944f045d223c86534e53 100644 (file)
@@ -9,14 +9,17 @@
     `get_nodes` used by the parser and translator in order to normalize
     python and jinja nodes.
 
-    :copyright: 2008 by Armin Ronacher.
+    :copyright: (c) 2010 by the Jinja Team.
     :license: BSD, see LICENSE for more details.
 """
 import operator
-from copy import copy
 from itertools import chain, izip
 from collections import deque
-from jinja2.utils import Markup
+from jinja2.utils import Markup, MethodType, FunctionType
+
+
+#: the types we support for context functions
+_context_function_types = (FunctionType, MethodType)
 
 
 _binop_to_func = {
@@ -68,6 +71,36 @@ class NodeType(type):
         return type.__new__(cls, name, bases, d)
 
 
+class EvalContext(object):
+    """Holds evaluation time information.  Custom attributes can be attached
+    to it in extensions.
+    """
+
+    def __init__(self, environment, template_name=None):
+        if callable(environment.autoescape):
+            self.autoescape = environment.autoescape(template_name)
+        else:
+            self.autoescape = environment.autoescape
+        self.volatile = False
+
+    def save(self):
+        return self.__dict__.copy()
+
+    def revert(self, old):
+        self.__dict__.clear()
+        self.__dict__.update(old)
+
+
+def get_eval_context(node, ctx):
+    if ctx is None:
+        if node.environment is None:
+            raise RuntimeError('if no eval context is passed, the '
+                               'node must have an attached '
+                               'environment.')
+        return EvalContext(node.environment)
+    return ctx
+
+
 class Node(object):
     """Baseclass for all Jinja2 nodes.  There are a number of nodes available
     of different types.  There are three major types:
@@ -147,37 +180,15 @@ class Node(object):
             return result
 
     def find_all(self, node_type):
-        """Find all the nodes of a given type."""
+        """Find all the nodes of a given type.  If the type is a tuple,
+        the check is performed for any of the tuple items.
+        """
         for child in self.iter_child_nodes():
             if isinstance(child, node_type):
                 yield child
             for result in child.find_all(node_type):
                 yield result
 
-    def copy(self):
-        """Return a deep copy of the node."""
-        result = object.__new__(self.__class__)
-        for field, value in self.iter_fields():
-            if isinstance(value, Node):
-                new_value = value.copy()
-            elif isinstance(value, list):
-                new_value = []
-                for item in value:
-                    if isinstance(item, Node):
-                        item = item.copy()
-                    else:
-                        item = copy(item)
-                    new_value.append(item)
-            else:
-                new_value = copy(value)
-            setattr(result, field, new_value)
-        for attr in self.attributes:
-            try:
-                setattr(result, attr, getattr(self, attr))
-            except AttributeError:
-                pass
-        return result
-
     def set_ctx(self, ctx):
         """Reset the context of a node and all child nodes.  Per default the
         parser will all generate nodes that have a 'load' context as it's the
@@ -287,11 +298,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')
@@ -299,12 +305,12 @@ class FilterBlock(Stmt):
 
 class Block(Stmt):
     """A node that represents a block."""
-    fields = ('name', 'body')
+    fields = ('name', 'body', 'scoped')
 
 
 class Include(Stmt):
     """A node that represents the include tag."""
-    fields = ('template', 'with_context')
+    fields = ('template', 'with_context', 'ignore_missing')
 
 
 class Import(Stmt):
@@ -316,7 +322,7 @@ class FromImport(Stmt):
     """A node that represents the from import tag.  It's important to not
     pass unsafe names to the name attribute.  The compiler translates the
     attribute lookups directly into getattr calls and does *not* use the
-    subscribe callback of the interface.  As exported variables may not
+    subscript callback of the interface.  As exported variables may not
     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.
@@ -340,19 +346,16 @@ class Expr(Node):
     """Baseclass for all expressions."""
     abstract = True
 
-    def as_const(self):
+    def as_const(self, eval_ctx=None):
         """Return the value of the expression as constant or raise
-        :exc:`Impossible` if this was not possible:
+        :exc:`Impossible` if this was not possible.
 
-        >>> Add(Const(23), Const(42)).as_const()
-        65
-        >>> Add(Const(23), Name('var', 'load')).as_const()
-        Traceback (most recent call last):
-          ...
-        Impossible
+        An :class:`EvalContext` can be provided, if none is given
+        a default context is created which requires the nodes to have
+        an attached environment.
 
-        This requires the `environment` attribute of all nodes to be
-        set to the environment that created the nodes.
+        .. versionchanged:: 2.4
+           the `eval_ctx` parameter was added.
         """
         raise Impossible()
 
@@ -367,10 +370,11 @@ class BinExpr(Expr):
     operator = None
     abstract = True
 
-    def as_const(self):
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
         f = _binop_to_func[self.operator]
         try:
-            return f(self.left.as_const(), self.right.as_const())
+            return f(self.left.as_const(eval_ctx), self.right.as_const(eval_ctx))
         except:
             raise Impossible()
 
@@ -381,10 +385,11 @@ class UnaryExpr(Expr):
     operator = None
     abstract = True
 
-    def as_const(self):
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
         f = _uaop_to_func[self.operator]
         try:
-            return f(self.node.as_const())
+            return f(self.node.as_const(eval_ctx))
         except:
             raise Impossible()
 
@@ -400,7 +405,8 @@ class Name(Expr):
     fields = ('name', 'ctx')
 
     def can_assign(self):
-        return self.name not in ('true', 'false', 'none')
+        return self.name not in ('true', 'false', 'none',
+                                 'True', 'False', 'None')
 
 
 class Literal(Expr):
@@ -416,7 +422,7 @@ class Const(Literal):
     """
     fields = ('value',)
 
-    def as_const(self):
+    def as_const(self, eval_ctx=None):
         return self.value
 
     @classmethod
@@ -431,6 +437,16 @@ class Const(Literal):
         return cls(value, lineno=lineno, environment=environment)
 
 
+class TemplateData(Literal):
+    """A constant template string."""
+    fields = ('data',)
+
+    def as_const(self, eval_ctx=None):
+        if get_eval_context(self, eval_ctx).autoescape:
+            return Markup(self.data)
+        return self.data
+
+
 class Tuple(Literal):
     """For loop unpacking and some other things like multiple arguments
     for subscripts.  Like for :class:`Name` `ctx` specifies if the tuple
@@ -438,8 +454,9 @@ class Tuple(Literal):
     """
     fields = ('items', 'ctx')
 
-    def as_const(self):
-        return tuple(x.as_const() for x in self.items)
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return tuple(x.as_const(eval_ctx) for x in self.items)
 
     def can_assign(self):
         for item in self.items:
@@ -452,8 +469,9 @@ class List(Literal):
     """Any list literal such as ``[1, 2, 3]``"""
     fields = ('items',)
 
-    def as_const(self):
-        return [x.as_const() for x in self.items]
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return [x.as_const(eval_ctx) for x in self.items]
 
 
 class Dict(Literal):
@@ -462,22 +480,28 @@ class Dict(Literal):
     """
     fields = ('items',)
 
-    def as_const(self):
-        return dict(x.as_const() for x in self.items)
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return dict(x.as_const(eval_ctx) for x in self.items)
 
 
 class Pair(Helper):
     """A key, value pair for dicts."""
     fields = ('key', 'value')
 
-    def as_const(self):
-        return self.key.as_const(), self.value.as_const()
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return self.key.as_const(eval_ctx), self.value.as_const(eval_ctx)
 
 
 class Keyword(Helper):
     """A key, value pair for keyword arguments where key is a string."""
     fields = ('key', 'value')
 
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return self.key, self.value.as_const(eval_ctx)
+
 
 class CondExpr(Expr):
     """A conditional expression (inline if expression).  (``{{
@@ -485,10 +509,16 @@ class CondExpr(Expr):
     """
     fields = ('test', 'expr1', 'expr2')
 
-    def as_const(self):
-        if self.test.as_const():
-            return self.expr1.as_const()
-        return self.expr2.as_const()
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        if self.test.as_const(eval_ctx):
+            return self.expr1.as_const(eval_ctx)
+
+        # if we evaluate to an undefined object, we better do that at runtime
+        if self.expr2 is None:
+            raise Impossible()
+
+        return self.expr2.as_const(eval_ctx)
 
 
 class Filter(Expr):
@@ -500,30 +530,37 @@ class Filter(Expr):
     """
     fields = ('node', 'name', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs')
 
-    def as_const(self, obj=None):
-        if self.node is obj is None:
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        if eval_ctx.volatile or self.node is None:
             raise Impossible()
-        filter = self.environment.filters.get(self.name)
-        if filter is None or getattr(filter, 'contextfilter', False):
+        # we have to be careful here because we call filter_ below.
+        # if this variable would be called filter, 2to3 would wrap the
+        # call in a list beause it is assuming we are talking about the
+        # builtin filter function here which no longer returns a list in
+        # python 3.  because of that, do not rename filter_ to filter!
+        filter_ = self.environment.filters.get(self.name)
+        if filter_ is None or getattr(filter_, 'contextfilter', False):
             raise Impossible()
-        if obj is None:
-            obj = self.node.as_const()
-        args = [x.as_const() for x in self.args]
-        if getattr(filter, 'environmentfilter', False):
+        obj = self.node.as_const(eval_ctx)
+        args = [x.as_const(eval_ctx) for x in self.args]
+        if getattr(filter_, 'evalcontextfilter', False):
+            args.insert(0, eval_ctx)
+        elif getattr(filter_, 'environmentfilter', False):
             args.insert(0, self.environment)
-        kwargs = dict(x.as_const() for x in self.kwargs)
+        kwargs = dict(x.as_const(eval_ctx) for x in self.kwargs)
         if self.dyn_args is not None:
             try:
-                args.extend(self.dyn_args.as_const())
+                args.extend(self.dyn_args.as_const(eval_ctx))
             except:
                 raise Impossible()
         if self.dyn_kwargs is not None:
             try:
-                kwargs.update(self.dyn_kwargs.as_const())
+                kwargs.update(self.dyn_kwargs.as_const(eval_ctx))
             except:
                 raise Impossible()
         try:
-            return filter(obj, *args, **kwargs)
+            return filter_(obj, *args, **kwargs)
         except:
             raise Impossible()
 
@@ -544,25 +581,31 @@ class Call(Expr):
     """
     fields = ('node', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs')
 
-    def as_const(self):
-        obj = self.node.as_const()
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        if eval_ctx.volatile:
+            raise Impossible()
+        obj = self.node.as_const(eval_ctx)
 
         # don't evaluate context functions
-        args = [x.as_const() for x in self.args]
-        if getattr(obj, 'contextfunction', False):
-            raise Impossible()
-        elif getattr(obj, 'environmentfunction', False):
-            args.insert(0, self.environment)
+        args = [x.as_const(eval_ctx) for x in self.args]
+        if isinstance(obj, _context_function_types):
+            if getattr(obj, 'contextfunction', False):
+                raise Impossible()
+            elif getattr(obj, 'evalcontextfunction', False):
+                args.insert(0, eval_ctx)
+            elif getattr(obj, 'environmentfunction', False):
+                args.insert(0, self.environment)
 
-        kwargs = dict(x.as_const() for x in self.kwargs)
+        kwargs = dict(x.as_const(eval_ctx) for x in self.kwargs)
         if self.dyn_args is not None:
             try:
-                args.extend(self.dyn_args.as_const())
+                args.extend(self.dyn_args.as_const(eval_ctx))
             except:
                 raise Impossible()
         if self.dyn_kwargs is not None:
             try:
-                kwargs.update(self.dyn_kwargs.as_const())
+                kwargs.update(self.dyn_kwargs.as_const(eval_ctx))
             except:
                 raise Impossible()
         try:
@@ -571,18 +614,37 @@ class Call(Expr):
             raise Impossible()
 
 
-class Subscript(Expr):
-    """Subscribe an expression by an argument.  This node performs a dict
-    and an attribute lookup on the object whatever succeeds.
-    """
+class Getitem(Expr):
+    """Get an attribute or item from an expression and prefer the item."""
     fields = ('node', 'arg', 'ctx')
 
-    def as_const(self):
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        if self.ctx != 'load':
+            raise Impossible()
+        try:
+            return self.environment.getitem(self.node.as_const(eval_ctx),
+                                            self.arg.as_const(eval_ctx))
+        except:
+            raise Impossible()
+
+    def can_assign(self):
+        return False
+
+
+class Getattr(Expr):
+    """Get an attribute or item from an expression that is a ascii-only
+    bytestring and prefer the attribute.
+    """
+    fields = ('node', 'attr', 'ctx')
+
+    def as_const(self, eval_ctx=None):
         if self.ctx != 'load':
             raise Impossible()
         try:
-            return self.environment.subscribe(self.node.as_const(),
-                                              self.arg.as_const())
+            eval_ctx = get_eval_context(self, eval_ctx)
+            return self.environment.getattr(self.node.as_const(eval_ctx),
+                                            self.attr)
         except:
             raise Impossible()
 
@@ -596,11 +658,12 @@ class Slice(Expr):
     """
     fields = ('start', 'stop', 'step')
 
-    def as_const(self):
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
         def const(obj):
             if obj is None:
-                return obj
-            return obj.as_const()
+                return None
+            return obj.as_const(eval_ctx)
         return slice(const(self.start), const(self.stop), const(self.step))
 
 
@@ -610,8 +673,9 @@ class Concat(Expr):
     """
     fields = ('nodes',)
 
-    def as_const(self):
-        return ''.join(unicode(x.as_const()) for x in self.nodes)
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return ''.join(unicode(x.as_const(eval_ctx)) for x in self.nodes)
 
 
 class Compare(Expr):
@@ -620,11 +684,12 @@ class Compare(Expr):
     """
     fields = ('expr', 'ops')
 
-    def as_const(self):
-        result = value = self.expr.as_const()
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        result = value = self.expr.as_const(eval_ctx)
         try:
             for op in self.ops:
-                new_value = op.expr.as_const()
+                new_value = op.expr.as_const(eval_ctx)
                 result = _cmpop_to_func[op.op](value, new_value)
                 value = new_value
         except:
@@ -683,16 +748,18 @@ class And(BinExpr):
     """Short circuited AND."""
     operator = 'and'
 
-    def as_const(self):
-        return self.left.as_const() and self.right.as_const()
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return self.left.as_const(eval_ctx) and self.right.as_const(eval_ctx)
 
 
 class Or(BinExpr):
     """Short circuited OR."""
     operator = 'or'
 
-    def as_const(self):
-        return self.left.as_const() or self.right.as_const()
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return self.left.as_const(eval_ctx) or self.right.as_const(eval_ctx)
 
 
 class Not(UnaryExpr):
@@ -757,12 +824,38 @@ class MarkSafe(Expr):
     """Mark the wrapped expression as safe (wrap it as `Markup`)."""
     fields = ('expr',)
 
-    def as_const(self):
-        return Markup(self.expr.as_const())
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return Markup(self.expr.as_const(eval_ctx))
+
+
+class MarkSafeIfAutoescape(Expr):
+    """Mark the wrapped expression as safe (wrap it as `Markup`) but
+    only if autoescaping is active.
+
+    .. versionadded:: 2.5
+    """
+    fields = ('expr',)
+
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        expr = self.expr.as_const(eval_ctx)
+        if eval_ctx.autoescape:
+            return Markup(expr)
+        return expr
 
 
 class ContextReference(Expr):
-    """Returns the current template context."""
+    """Returns the current template context.  It can be used like a
+    :class:`Name` node, with a ``'load'`` ctx and will return the
+    current :class:`~jinja2.runtime.Context` object.
+
+    Here an example that assigns the current template name to a
+    variable named `foo`::
+
+        Assign(Name('foo', ctx='store'),
+               Getattr(ContextReference(), 'name'))
+    """
 
 
 class Continue(Stmt):
@@ -773,6 +866,30 @@ class Break(Stmt):
     """Break a loop."""
 
 
+class Scope(Stmt):
+    """An artificial scope."""
+    fields = ('body',)
+
+
+class EvalContextModifier(Stmt):
+    """Modifies the eval context.  For each option that should be modified,
+    a :class:`Keyword` has to be added to the :attr:`options` list.
+
+    Example to change the `autoescape` setting::
+
+        EvalContextModifier(options=[Keyword('autoescape', Const(True))])
+    """
+    fields = ('options',)
+
+
+class ScopedEvalContextModifier(EvalContextModifier):
+    """Modifies the eval context and reverts it later.  Works exactly like
+    :class:`EvalContextModifier` but will only modify the
+    :class:`~jinja2.nodes.EvalContext` for nodes in the :attr:`body`.
+    """
+    fields = ('body',)
+
+
 # make sure nobody creates custom nodes
 def _failing_new(*args, **kwargs):
     raise TypeError('can\'t create custom node types')