Next try for a Python 3 fix
[jinja2.git] / jinja2 / runtime.py
index 5f8de1f1226acf5153583789693ab698cdab7a8b..43ffda80ae3360c6f7df8575ba20999e0cf03343 100644 (file)
 
     Runtime helpers.
 
-    :copyright: Copyright 2008 by Armin Ronacher.
-    :license: GNU GPL.
+    :copyright: (c) 2010 by the Jinja Team.
+    :license: BSD.
 """
-from types import FunctionType
-from jinja2.utils import Markup, partial
-from jinja2.exceptions import UndefinedError
+import sys
+from itertools import chain, imap
+from jinja2.nodes import EvalContext, _context_function_types
+from jinja2.utils import Markup, partial, soft_unicode, escape, missing, \
+     concat, internalcode, next, object_type_repr
+from jinja2.exceptions import UndefinedError, TemplateRuntimeError, \
+     TemplateNotFound
 
 
 # these variables are exported to the template runtime
-__all__ = ['LoopContext', 'TemplateContext', 'TemplateReference', 'Macro',
-           'Markup', 'missing', 'concat']
+__all__ = ['LoopContext', 'TemplateReference', 'Macro', 'Markup',
+           'TemplateRuntimeError', 'missing', 'concat', 'escape',
+           'markup_join', 'unicode_join', 'to_string', 'identity',
+           'TemplateNotFound']
+
+#: the name of the function that is used to convert something into
+#: a string.  2to3 will adopt that automatically and the generated
+#: code can take advantage of it.
+to_string = unicode
+
+#: the identity function.  Useful for certain things in the environment
+identity = lambda x: x
+
+
+def markup_join(seq):
+    """Concatenation that escapes if necessary and converts to unicode."""
+    buf = []
+    iterator = imap(soft_unicode, seq)
+    for arg in iterator:
+        buf.append(arg)
+        if hasattr(arg, '__html__'):
+            return Markup(u'').join(chain(buf, iterator))
+    return concat(buf)
+
+
+def unicode_join(seq):
+    """Simple args to unicode conversion and concatenation."""
+    return concat(imap(unicode, seq))
+
+
+def new_context(environment, template_name, blocks, vars=None,
+                shared=None, globals=None, locals=None):
+    """Internal helper to for context creation."""
+    if vars is None:
+        vars = {}
+    if shared:
+        parent = vars
+    else:
+        parent = dict(globals or (), **vars)
+    if locals:
+        # if the parent is shared a copy should be created because
+        # we don't want to modify the dict passed
+        if shared:
+            parent = dict(parent)
+        for key, value in locals.iteritems():
+            if key[:2] == 'l_' and value is not missing:
+                parent[key[2:]] = value
+    return Context(environment, parent, template_name, blocks)
 
 
-# special singleton representing missing values for the runtime
-missing = type('MissingType', (), {'__repr__': lambda x: 'missing'})()
+class TemplateReference(object):
+    """The `self` in templates."""
 
+    def __init__(self, context):
+        self.__context = context
 
-# concatenate a list of strings and convert them to unicode.
-concat = u''.join
+    def __getitem__(self, name):
+        blocks = self.__context.blocks[name]
+        wrap = self.__context.eval_ctx.autoescape and \
+               Markup or (lambda x: x)
+        return BlockReference(name, self.__context, blocks, 0)
 
+    def __repr__(self):
+        return '<%s %r>' % (
+            self.__class__.__name__,
+            self.__context.name
+        )
 
-class TemplateContext(object):
-    """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).
 
-    The context is immutable.  Modifications on `parent` must not happen and
-    modifications on `vars` are allowed from generated template code.  However
-    functions that are passed the template context may not modify the context
-    in any way.
+class Context(object):
+    """The template context holds the variables of a template.  It stores the
+    values passed to the template and also the names the template exports.
+    Creating instances is neither supported nor useful as it's created
+    automatically at various stages of the template evaluation and should not
+    be created by hand.
+
+    The context is immutable.  Modifications on :attr:`parent` **must not**
+    happen and modifications on :attr:`vars` are allowed from generated
+    template code only.  Template filters and global functions marked as
+    :func:`contextfunction`\s get the active context passed as first argument
+    and are allowed to access the context read-only.
+
+    The template context supports read only dict operations (`get`,
+    `keys`, `values`, `items`, `iterkeys`, `itervalues`, `iteritems`,
+    `__getitem__`, `__contains__`).  Additionally there is a :meth:`resolve`
+    method that doesn't fail with a `KeyError` but returns an
+    :class:`Undefined` object for missing variables.
     """
+    __slots__ = ('parent', 'vars', 'environment', 'eval_ctx', 'exported_vars',
+                 'name', 'blocks', '__weakref__')
 
     def __init__(self, environment, parent, name, blocks):
         self.parent = parent
-        self.vars = vars = {}
+        self.vars = {}
         self.environment = environment
+        self.eval_ctx = EvalContext(self.environment, name)
         self.exported_vars = set()
         self.name = name
 
-        # bind functions to the context of environment if required
-        for name, obj in parent.iteritems():
-            if type(obj) is FunctionType:
-                if getattr(obj, 'contextfunction', 0):
-                    vars[name] = partial(obj, self)
-                elif getattr(obj, 'environmentfunction', 0):
-                    vars[name] = partial(obj, environment)
-
         # create the initial mapping of blocks.  Whenever template inheritance
         # takes place the runtime will update this mapping with the new blocks
         # from the template.
@@ -62,50 +126,97 @@ class TemplateContext(object):
         """Render a parent block."""
         try:
             blocks = self.blocks[name]
-            pos = blocks.index(current) - 1
-            if pos < 0:
-                raise IndexError()
+            index = blocks.index(current) + 1
+            blocks[index]
         except LookupError:
             return self.environment.undefined('there is no parent block '
-                                              'called %r.' % name)
-        render = lambda: Markup(concat(blocks[pos](self)))
-        render.__name__ = render.name = name
-        return render
+                                              'called %r.' % name,
+                                              name='super')
+        return BlockReference(name, self, blocks, index)
 
     def get(self, key, default=None):
-        """For dict compatibility"""
+        """Returns an item from the template context, if it doesn't exist
+        `default` is returned.
+        """
+        try:
+            return self[key]
+        except KeyError:
+            return default
+
+    def resolve(self, key):
+        """Looks up a variable like `__getitem__` or `get` but returns an
+        :class:`Undefined` object with the name of the name looked up.
+        """
         if key in self.vars:
             return self.vars[key]
         if key in self.parent:
             return self.parent[key]
-        return default
+        return self.environment.undefined(name=key)
 
     def get_exported(self):
         """Get a new dict with the exported variables."""
         return dict((k, self.vars[k]) for k in self.exported_vars)
 
-    def get_root(self):
-        """Return a new dict with all the non local variables."""
-        return dict(self.parent)
-
     def get_all(self):
-        """Return a copy of the complete context as dict."""
+        """Return a copy of the complete context as dict including the
+        exported variables.
+        """
         return dict(self.parent, **self.vars)
 
-    def clone(self):
-        """Return a copy of the context without the locals."""
-        return self.__class__(self.environment, self.parent,
-                              self.name, self.blocks)
+    @internalcode
+    def call(__self, __obj, *args, **kwargs):
+        """Call the callable with the arguments and keyword arguments
+        provided but inject the active context or environment as first
+        argument if the callable is a :func:`contextfunction` or
+        :func:`environmentfunction`.
+        """
+        if __debug__:
+            __traceback_hide__ = True
+        if isinstance(__obj, _context_function_types):
+            if getattr(__obj, 'contextfunction', 0):
+                args = (__self,) + args
+            elif getattr(__obj, 'evalcontextfunction', 0):
+                args = (__self.eval_ctx,) + args
+            elif getattr(__obj, 'environmentfunction', 0):
+                args = (__self.environment,) + args
+        return __obj(*args, **kwargs)
+
+    def derived(self, locals=None):
+        """Internal helper function to create a derived context."""
+        context = new_context(self.environment, self.name, {},
+                              self.parent, True, None, locals)
+        context.eval_ctx = self.eval_ctx
+        context.blocks.update((k, list(v)) for k, v in self.blocks.iteritems())
+        return context
+
+    def _all(meth):
+        proxy = lambda self: getattr(self.get_all(), meth)()
+        proxy.__doc__ = getattr(dict, meth).__doc__
+        proxy.__name__ = meth
+        return proxy
+
+    keys = _all('keys')
+    values = _all('values')
+    items = _all('items')
+
+    # not available on python 3
+    if hasattr(dict, 'iterkeys'):
+        iterkeys = _all('iterkeys')
+        itervalues = _all('itervalues')
+        iteritems = _all('iteritems')
+    del _all
 
     def __contains__(self, name):
         return name in self.vars or name in self.parent
 
     def __getitem__(self, key):
-        if key in self.vars:
-            return self.vars[key]
-        if key in self.parent:
-            return self.parent[key]
-        return self.environment.undefined(name=key)
+        """Lookup a variable or raise `KeyError` if the variable is
+        undefined.
+        """
+        item = self.resolve(key)
+        if isinstance(item, Undefined):
+            raise KeyError(key)
+        return item
 
     def __repr__(self):
         return '<%s %s of %r>' % (
@@ -115,44 +226,66 @@ class TemplateContext(object):
         )
 
 
-class TemplateReference(object):
-    """The `self` in templates."""
+# register the context as mapping if possible
+try:
+    from collections import Mapping
+    Mapping.register(Context)
+except ImportError:
+    pass
 
-    def __init__(self, context):
-        self.__context = context
 
-    def __getitem__(self, name):
-        func = self.__context.blocks[name][-1]
-        render = lambda: Markup(concat(func(self.__context)))
-        render.__name__ = render.name = name
-        return render
+class BlockReference(object):
+    """One block on a template reference."""
 
-    def __repr__(self):
-        return '<%s %r>' % (
-            self.__class__.__name__,
-            self._context.name
-        )
+    def __init__(self, name, context, stack, depth):
+        self.name = name
+        self._context = context
+        self._stack = stack
+        self._depth = depth
+
+    @property
+    def super(self):
+        """Super the block."""
+        if self._depth + 1 >= len(self._stack):
+            return self._context.environment. \
+                undefined('there is no parent block called %r.' %
+                          self.name, name='super')
+        return BlockReference(self.name, self._context, self._stack,
+                              self._depth + 1)
+
+    @internalcode
+    def __call__(self):
+        rv = concat(self._stack[self._depth](self._context))
+        if self._context.eval_ctx.autoescape:
+            rv = Markup(rv)
+        return rv
 
 
 class LoopContext(object):
     """A loop context for dynamic iteration."""
 
-    def __init__(self, iterable, enforce_length=False):
-        self._iterable = iterable
-        self._next = iter(iterable).next
-        self._length = None
+    def __init__(self, iterable, recurse=None):
+        self._iterator = iter(iterable)
+        self._recurse = recurse
         self.index0 = -1
-        if enforce_length:
-            len(self)
+
+        # try to get the length of the iterable early.  This must be done
+        # here because there are some broken iterators around where there
+        # __len__ is the number of iterations left (i'm looking at your
+        # listreverseiterator!).
+        try:
+            self._length = len(iterable)
+        except (TypeError, AttributeError):
+            self._length = None
 
     def cycle(self, *args):
-        """A replacement for the old ``{% cycle %}`` tag."""
+        """Cycles among the arguments with the current loop index."""
         if not args:
             raise TypeError('no items for cycling given')
         return args[self.index0 % len(args)]
 
     first = property(lambda x: x.index0 == 0)
-    last = property(lambda x: x.revindex0 == 0)
+    last = property(lambda x: x.index0 + 1 == x.length)
     index = property(lambda x: x.index0 + 1)
     revindex = property(lambda x: x.length - x.index0)
     revindex0 = property(lambda x: x.length - x.index)
@@ -161,22 +294,30 @@ class LoopContext(object):
         return self.length
 
     def __iter__(self):
-        return self
+        return LoopContextIterator(self)
 
-    def next(self):
-        self.index0 += 1
-        return self._next(), self
+    @internalcode
+    def loop(self, iterable):
+        if self._recurse is None:
+            raise TypeError('Tried to call non recursive loop.  Maybe you '
+                            "forgot the 'recursive' modifier.")
+        return self._recurse(iterable, self._recurse)
+
+    # a nifty trick to enhance the error message if someone tried to call
+    # the the loop without or with too many arguments.
+    __call__ = loop
+    del loop
 
     @property
     def length(self):
         if self._length is None:
-            try:
-                length = len(self._iterable)
-            except TypeError:
-                self._iterable = tuple(self._iterable)
-                self._next = iter(self._iterable).next
-                length = len(tuple(self._iterable)) + self.index0 + 1
-            self._length = length
+            # if was not possible to get the length of the iterator when
+            # the loop context was created (ie: iterating over a generator)
+            # we have to convert the iterable into a sequence and use the
+            # length of that.
+            iterable = tuple(self._iterator)
+            self._iterator = iter(iterable)
+            self._length = len(iterable) + self.index0 + 1
         return self._length
 
     def __repr__(self):
@@ -187,6 +328,22 @@ class LoopContext(object):
         )
 
 
+class LoopContextIterator(object):
+    """The iterator for a loop context."""
+    __slots__ = ('context',)
+
+    def __init__(self, context):
+        self.context = context
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        ctx = self.context
+        ctx.index0 += 1
+        return next(ctx._iterator), ctx
+
+
 class Macro(object):
     """Wraps a macro."""
 
@@ -194,32 +351,29 @@ class Macro(object):
                  catch_kwargs, catch_varargs, caller):
         self._environment = environment
         self._func = func
+        self._argument_count = len(arguments)
         self.name = name
         self.arguments = arguments
-        self.argument_count = len(arguments)
         self.defaults = defaults
         self.catch_kwargs = catch_kwargs
         self.catch_varargs = catch_varargs
         self.caller = caller
 
+    @internalcode
     def __call__(self, *args, **kwargs):
-        self.argument_count = len(self.arguments)
-        if not self.catch_varargs and len(args) > self.argument_count:
-            raise TypeError('macro %r takes not more than %d argument(s)' %
-                            (self.name, len(self.arguments)))
         arguments = []
         for idx, name in enumerate(self.arguments):
             try:
                 value = args[idx]
-            except IndexError:
+            except:
                 try:
                     value = kwargs.pop(name)
-                except KeyError:
+                except:
                     try:
-                        value = self.defaults[idx - self.argument_count]
-                    except IndexError:
+                        value = self.defaults[idx - self._argument_count]
+                    except:
                         value = self._environment.undefined(
-                            'parameter %r was not provided' % name)
+                            'parameter %r was not provided' % name, name=name)
             arguments.append(value)
 
         # it's important that the order of these arguments does not change
@@ -228,15 +382,19 @@ class Macro(object):
         if self.caller:
             caller = kwargs.pop('caller', None)
             if caller is None:
-                caller = self._environment.undefined('No caller defined')
+                caller = self._environment.undefined('No caller defined',
+                                                     name='caller')
             arguments.append(caller)
         if self.catch_kwargs:
             arguments.append(kwargs)
         elif kwargs:
             raise TypeError('macro %r takes no keyword argument %r' %
-                            (self.name, iter(kwargs).next()))
+                            (self.name, next(iter(kwargs))))
         if self.catch_varargs:
-            arguments.append(args[self.argument_count:])
+            arguments.append(args[self._argument_count:])
+        elif len(args) > self._argument_count:
+            raise TypeError('macro %r takes not more than %d argument(s)' %
+                            (self.name, len(self.arguments)))
         return self._func(*arguments)
 
     def __repr__(self):
@@ -246,52 +404,65 @@ class Macro(object):
         )
 
 
-def fail_with_undefined_error(self, *args, **kwargs):
-    """Regular callback function for undefined objects that raises an
-    `UndefinedError` on call.
-    """
-    if self._undefined_hint is None:
-        if self._undefined_obj is None:
-            hint = '%r is undefined' % self._undefined_name
-        elif not isinstance(self._undefined_name, basestring):
-            hint = '%r object has no element %r' % (
-                self._undefined_obj.__class__.__name__,
-                self._undefined_name
-            )
-        else:
-            hint = '%r object has no attribute %r' % (
-                self._undefined_obj.__class__.__name__,
-                self._undefined_name
-            )
-    else:
-        hint = self._undefined_hint
-    raise UndefinedError(hint)
-
-
 class Undefined(object):
-    """The default undefined implementation.  This undefined implementation
-    can be printed and iterated over, but every other access will raise a
-    `NameError`.  Custom undefined classes must subclass this.
+    """The default undefined type.  This undefined type can be printed and
+    iterated over, but every other access will raise an :exc:`UndefinedError`:
+
+    >>> foo = Undefined(name='foo')
+    >>> str(foo)
+    ''
+    >>> not foo
+    True
+    >>> foo + 42
+    Traceback (most recent call last):
+      ...
+    UndefinedError: 'foo' is undefined
     """
-    __slots__ = ('_undefined_hint', '_undefined_obj', '_undefined_name')
+    __slots__ = ('_undefined_hint', '_undefined_obj', '_undefined_name',
+                 '_undefined_exception')
 
-    def __init__(self, hint=None, obj=None, name=None):
+    def __init__(self, hint=None, obj=missing, name=None, exc=UndefinedError):
         self._undefined_hint = hint
         self._undefined_obj = obj
         self._undefined_name = name
+        self._undefined_exception = exc
+
+    @internalcode
+    def _fail_with_undefined_error(self, *args, **kwargs):
+        """Regular callback function for undefined objects that raises an
+        `UndefinedError` on call.
+        """
+        if self._undefined_hint is None:
+            if self._undefined_obj is missing:
+                hint = '%r is undefined' % self._undefined_name
+            elif not isinstance(self._undefined_name, basestring):
+                hint = '%s has no element %r' % (
+                    object_type_repr(self._undefined_obj),
+                    self._undefined_name
+                )
+            else:
+                hint = '%r has no attribute %r' % (
+                    object_type_repr(self._undefined_obj),
+                    self._undefined_name
+                )
+        else:
+            hint = self._undefined_hint
+        raise self._undefined_exception(hint)
 
     __add__ = __radd__ = __mul__ = __rmul__ = __div__ = __rdiv__ = \
-    __realdiv__ = __rrealdiv__ = __floordiv__ = __rfloordiv__ = \
+    __truediv__ = __rtruediv__ = __floordiv__ = __rfloordiv__ = \
     __mod__ = __rmod__ = __pos__ = __neg__ = __call__ = \
     __getattr__ = __getitem__ = __lt__ = __le__ = __gt__ = __ge__ = \
-        fail_with_undefined_error
+    __int__ = __float__ = __complex__ = __pow__ = __rpow__ = \
+        _fail_with_undefined_error
 
     def __str__(self):
-        return self.__unicode__().encode('utf-8')
-
-    def __repr__(self):
-        return 'Undefined'
+        return unicode(self).encode('utf-8')
 
+    # unicode goes after __str__ because we configured 2to3 to rename
+    # __unicode__ to __str__.  because the 2to3 tree is not designed to
+    # remove nodes from it, we leave the above __str__ around and let
+    # it override at runtime.
     def __unicode__(self):
         return u''
 
@@ -305,17 +476,31 @@ class Undefined(object):
     def __nonzero__(self):
         return False
 
+    def __repr__(self):
+        return 'Undefined'
+
 
 class DebugUndefined(Undefined):
-    """An undefined that returns the debug info when printed."""
+    """An undefined that returns the debug info when printed.
+
+    >>> foo = DebugUndefined(name='foo')
+    >>> str(foo)
+    '{{ foo }}'
+    >>> not foo
+    True
+    >>> foo + 42
+    Traceback (most recent call last):
+      ...
+    UndefinedError: 'foo' is undefined
+    """
     __slots__ = ()
 
     def __unicode__(self):
         if self._undefined_hint is None:
-            if self._undefined_obj is None:
+            if self._undefined_obj is missing:
                 return u'{{ %s }}' % self._undefined_name
             return '{{ no such element: %s[%r] }}' % (
-                self._undefined_obj.__class__.__name__,
+                object_type_repr(self._undefined_obj),
                 self._undefined_name
             )
         return u'{{ undefined value printed: %s }}' % self._undefined_hint
@@ -325,10 +510,24 @@ class StrictUndefined(Undefined):
     """An undefined that barks on print and iteration as well as boolean
     tests and all kinds of comparisons.  In other words: you can do nothing
     with it except checking if it's defined using the `defined` test.
+
+    >>> foo = StrictUndefined(name='foo')
+    >>> str(foo)
+    Traceback (most recent call last):
+      ...
+    UndefinedError: 'foo' is undefined
+    >>> not foo
+    Traceback (most recent call last):
+      ...
+    UndefinedError: 'foo' is undefined
+    >>> foo + 42
+    Traceback (most recent call last):
+      ...
+    UndefinedError: 'foo' is undefined
     """
     __slots__ = ()
-    __iter__ = __unicode__ = __len__ = __nonzero__ = __eq__ = __ne__ = \
-        fail_with_undefined_error
+    __iter__ = __unicode__ = __str__ = __len__ = __nonzero__ = __eq__ = \
+        __ne__ = __bool__ = Undefined._fail_with_undefined_error
 
 
 # remove remaining slots attributes, after the metaclass did the magic they