Various cleanups and added custom cycler.
[jinja2.git] / jinja2 / utils.py
index 195a9420c27f79dc74afad9a354dad7b355095bc..338db4a15f0c6d05ba220dc38acfc868e160e802 100644 (file)
@@ -9,10 +9,13 @@
     :license: BSD, see LICENSE for more details.
 """
 import re
-import string
+import sys
+import errno
+try:
+    from thread import allocate_lock
+except ImportError:
+    from dummy_thread import allocate_lock
 from collections import deque
-from copy import deepcopy
-from functools import update_wrapper
 from itertools import imap
 
 
@@ -24,6 +27,125 @@ _punctuation_re = re.compile(
     )
 )
 _simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$')
+_striptags_re = re.compile(r'(<!--.*?-->|<[^>]*>)')
+_entity_re = re.compile(r'&([^;]+);')
+_letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
+_digits = '0123456789'
+
+# special singleton representing missing values for the runtime
+missing = type('MissingType', (), {'__repr__': lambda x: 'missing'})()
+
+
+# concatenate a list of strings and convert them to unicode.
+# unfortunately there is a bug in python 2.4 and lower that causes
+# unicode.join trash the traceback.
+_concat = u''.join
+try:
+    def _test_gen_bug():
+        raise TypeError(_test_gen_bug)
+        yield None
+    _concat(_test_gen_bug())
+except TypeError, _error:
+    if not _error.args or _error.args[0] is not _test_gen_bug:
+        def concat(gen):
+            try:
+                return _concat(list(gen))
+            except:
+                # this hack is needed so that the current frame
+                # does not show up in the traceback.
+                exc_type, exc_value, tb = sys.exc_info()
+                raise exc_type, exc_value, tb.tb_next
+    else:
+        concat = _concat
+    del _test_gen_bug, _error
+
+
+# ironpython without stdlib doesn't have keyword
+try:
+    from keyword import iskeyword as is_python_keyword
+except ImportError:
+    _py_identifier_re = re.compile(r'^[a-zA-Z_][a-zA-Z0-9]*$')
+    def is_python_keyword(name):
+        if _py_identifier_re.search(name) is None:
+            return False
+        try:
+            exec name + " = 42"
+        except SyntaxError:
+            return False
+        return True
+
+
+# common types.  These do exist in the special types module too which however
+# does not exist in IronPython out of the box.
+class _C(object):
+    def method(self): pass
+def _func():
+    yield None
+FunctionType = type(_func)
+GeneratorType = type(_func())
+MethodType = type(_C.method)
+CodeType = type(_C.method.func_code)
+try:
+    raise TypeError()
+except TypeError:
+    _tb = sys.exc_info()[2]
+    TracebackType = type(_tb)
+    FrameType = type(_tb.tb_frame)
+del _C, _tb, _func
+
+
+def contextfunction(f):
+    """This decorator can be used to mark a function or method context callable.
+    A context callable is passed the active :class:`Context` as first argument when
+    called from the template.  This is useful if a function wants to get access
+    to the context or functions provided on the context object.  For example
+    a function that returns a sorted list of template variables the current
+    template exports could look like this::
+
+        @contextfunction
+        def get_exported_names(context):
+            return sorted(context.exported_vars)
+    """
+    f.contextfunction = True
+    return f
+
+
+def environmentfunction(f):
+    """This decorator can be used to mark a function or method as environment
+    callable.  This decorator works exactly like the :func:`contextfunction`
+    decorator just that the first argument is the active :class:`Environment`
+    and not context.
+    """
+    f.environmentfunction = True
+    return f
+
+
+def is_undefined(obj):
+    """Check if the object passed is undefined.  This does nothing more than
+    performing an instance check against :class:`Undefined` but looks nicer.
+    This can be used for custom filters or tests that want to react to
+    undefined variables.  For example a custom default filter can look like
+    this::
+
+        def default(var, default=''):
+            if is_undefined(var):
+                return default
+            return var
+    """
+    from jinja2.runtime import Undefined
+    return isinstance(obj, Undefined)
+
+
+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
+    the time.  Normally you don't have to care about that but if you are
+    messuring memory consumption you may want to clean the caches.
+    """
+    from jinja2.environment import _spontaneous_environments
+    from jinja2.lexer import _lexer_cache
+    _spontaneous_environments.clear()
+    _lexer_cache.clear()
 
 
 def import_string(import_name, silent=False):
@@ -52,6 +174,17 @@ def import_string(import_name, silent=False):
             raise
 
 
+def open_if_exists(filename, mode='r'):
+    """Returns a file descriptor for the filename if that file exists,
+    otherwise `None`.
+    """
+    try:
+        return file(filename, mode)
+    except IOError, e:
+        if e.errno not in (errno.ENOENT, errno.EISDIR):
+            raise
+
+
 def pformat(obj, verbose=False):
     """Prettyprint an object.  Either use the `pretty` library or the
     builtin `pprint`.
@@ -89,7 +222,7 @@ def urlize(text, trim_url_limit=None, nofollow=False):
                 '@' not in middle and
                 not middle.startswith('http://') and
                 len(middle) > 0 and
-                middle[0] in string.letters + string.digits and (
+                middle[0] in _letters + _digits and (
                     middle.endswith('.org') or
                     middle.endswith('.net') or
                     middle.endswith('.com')
@@ -108,17 +241,108 @@ def urlize(text, trim_url_limit=None, nofollow=False):
     return u''.join(words)
 
 
+def generate_lorem_ipsum(n=5, html=True, min=20, max=100):
+    """Generate some lorem impsum for the template."""
+    from jinja2.constants import LOREM_IPSUM_WORDS
+    from random import choice, random, randrange
+    words = LOREM_IPSUM_WORDS.split()
+    result = []
+
+    for _ in xrange(n):
+        next_capitalized = True
+        last_comma = last_fullstop = 0
+        word = None
+        last = None
+        p = []
+
+        # each paragraph contains out of 20 to 100 words.
+        for idx, _ in enumerate(xrange(randrange(min, max))):
+            while True:
+                word = choice(words)
+                if word != last:
+                    last = word
+                    break
+            if next_capitalized:
+                word = word.capitalize()
+                next_capitalized = False
+            # add commas
+            if idx - randrange(3, 8) > last_comma:
+                last_comma = idx
+                last_fullstop += 2
+                word += ','
+            # add end of sentences
+            if idx - randrange(10, 20) > last_fullstop:
+                last_comma = last_fullstop = idx
+                word += '.'
+                next_capitalized = True
+            p.append(word)
+
+        # ensure that the paragraph ends with a dot.
+        p = u' '.join(p)
+        if p.endswith(','):
+            p = p[:-1] + '.'
+        elif not p.endswith('.'):
+            p += '.'
+        result.append(p)
+
+    if not html:
+        return u'\n\n'.join(result)
+    return Markup(u'\n'.join(u'<p>%s</p>' % escape(x) for x in result))
+
+
 class Markup(unicode):
-    """Marks a string as being safe for inclusion in HTML/XML output without
+    r"""Marks a string as being safe for inclusion in HTML/XML output without
     needing to be escaped.  This implements the `__html__` interface a couple
-    of frameworks and web applications use.
+    of frameworks and web applications use.  :class:`Markup` is a direct
+    subclass of `unicode` and provides all the methods of `unicode` just that
+    it escapes arguments passed and always returns `Markup`.
 
     The `escape` function returns markup objects so that double escaping can't
-    happen.  If you want to use autoescaping in Jinja just set the finalizer
-    of the environment to `escape`.
+    happen.  If you want to use autoescaping in Jinja just enable the
+    autoescaping feature in the environment.
+
+    The constructor of the :class:`Markup` class can be used for three
+    different things:  When passed an unicode object it's assumed to be safe,
+    when passed an object with an HTML representation (has an `__html__`
+    method) that representation is used, otherwise the object passed is
+    converted into a unicode string and then assumed to be safe:
+
+    >>> Markup("Hello <em>World</em>!")
+    Markup(u'Hello <em>World</em>!')
+    >>> class Foo(object):
+    ...  def __html__(self):
+    ...   return '<a href="#">foo</a>'
+    ... 
+    >>> Markup(Foo())
+    Markup(u'<a href="#">foo</a>')
+
+    If you want object passed being always treated as unsafe you can use the
+    :meth:`escape` classmethod to create a :class:`Markup` object:
+
+    >>> Markup.escape("Hello <em>World</em>!")
+    Markup(u'Hello &lt;em&gt;World&lt;/em&gt;!')
+
+    Operations on a markup string are markup aware which means that all
+    arguments are passed through the :func:`escape` function:
+
+    >>> em = Markup("<em>%s</em>")
+    >>> em % "foo & bar"
+    Markup(u'<em>foo &amp; bar</em>')
+    >>> strong = Markup("<strong>%(text)s</strong>")
+    >>> strong % {'text': '<blink>hacker here</blink>'}
+    Markup(u'<strong>&lt;blink&gt;hacker here&lt;/blink&gt;</strong>')
+    >>> Markup("<em>Hello</em> ") + "<foo>"
+    Markup(u'<em>Hello</em> &lt;foo&gt;')
     """
     __slots__ = ()
 
+    def __new__(cls, base=u'', encoding=None, errors='strict'):
+        if hasattr(base, '__html__'):
+            base = base.__html__()
+        if encoding is None:
+            return unicode.__new__(cls, base)
+        return unicode.__new__(cls, base, encoding, errors)
+
     def __html__(self):
         return self
 
@@ -133,9 +357,9 @@ class Markup(unicode):
         return NotImplemented
 
     def __mul__(self, num):
-        if not isinstance(num, (int, long)):
-            return NotImplemented
-        return self.__class__(unicode.__mul__(self, num))
+        if isinstance(num, (int, long)):
+            return self.__class__(unicode.__mul__(self, num))
+        return NotImplemented
     __rmul__ = __mul__
 
     def __mod__(self, arg):
@@ -167,27 +391,86 @@ class Markup(unicode):
         return map(self.__class__, unicode.splitlines(self, *args, **kwargs))
     splitlines.__doc__ = unicode.splitlines.__doc__
 
+    def unescape(self):
+        r"""Unescape markup again into an unicode string.  This also resolves
+        known HTML4 and XHTML entities:
+
+        >>> Markup("Main &raquo; <em>About</em>").unescape()
+        u'Main \xbb <em>About</em>'
+        """
+        from jinja2.constants import HTML_ENTITIES
+        def handle_match(m):
+            name = m.group(1)
+            if name in HTML_ENTITIES:
+                return unichr(HTML_ENTITIES[name])
+            try:
+                if name[:2] in ('#x', '#X'):
+                    return unichr(int(name[2:], 16))
+                elif name.startswith('#'):
+                    return unichr(int(name[1:]))
+            except ValueError:
+                pass
+            return u''
+        return _entity_re.sub(handle_match, unicode(self))
+
+    def striptags(self):
+        r"""Unescape markup into an unicode string and strip all tags.  This
+        also resolves known HTML4 and XHTML entities.  Whitespace is
+        normalized to one:
+
+        >>> Markup("Main &raquo;  <em>About</em>").striptags()
+        u'Main \xbb About'
+        """
+        stripped = u' '.join(_striptags_re.sub('', self).split())
+        return Markup(stripped).unescape()
+
+    @classmethod
+    def escape(cls, s):
+        """Escape the string.  Works like :func:`escape` with the difference
+        that for subclasses of :class:`Markup` this function would return the
+        correct subclass.
+        """
+        rv = escape(s)
+        if rv.__class__ is not cls:
+            return cls(rv)
+        return rv
+
     def make_wrapper(name):
         orig = getattr(unicode, name)
         def func(self, *args, **kwargs):
-            args = list(args)
-            for idx, arg in enumerate(args):
-                if hasattr(arg, '__html__') or isinstance(arg, basestring):
-                    args[idx] = escape(arg)
-            for name, arg in kwargs.iteritems():
-                if hasattr(arg, '__html__') or isinstance(arg, basestring):
-                    kwargs[name] = escape(arg)
+            args = _escape_argspec(list(args), enumerate(args))
+            _escape_argspec(kwargs, kwargs.iteritems())
             return self.__class__(orig(self, *args, **kwargs))
-        return update_wrapper(func, orig, ('__name__', '__doc__'))
+        func.__name__ = orig.__name__
+        func.__doc__ = orig.__doc__
+        return func
+
     for method in '__getitem__', '__getslice__', 'capitalize', \
                   'title', 'lower', 'upper', 'replace', 'ljust', \
-                  'rjust', 'lstrip', 'rstrip', 'partition', 'center', \
-                  'strip', 'translate', 'expandtabs', 'rpartition', \
-                  'swapcase', 'zfill':
+                  'rjust', 'lstrip', 'rstrip', 'center', 'strip', \
+                  'translate', 'expandtabs', 'swapcase', 'zfill':
         locals()[method] = make_wrapper(method)
+
+    # new in python 2.5
+    if hasattr(unicode, 'partition'):
+        partition = make_wrapper('partition'),
+        rpartition = make_wrapper('rpartition')
+
+    # new in python 2.6
+    if hasattr(unicode, 'format'):
+        format = make_wrapper('format')
+
     del method, make_wrapper
 
 
+def _escape_argspec(obj, iterable):
+    """Helper for various string-wrapped functions."""
+    for key, value in iterable:
+        if hasattr(value, '__html__') or isinstance(value, basestring):
+            obj[key] = escape(value)
+    return obj
+
+
 class _MarkupEscapeHelper(object):
     """Helper for Markup.__mod__"""
 
@@ -197,27 +480,31 @@ class _MarkupEscapeHelper(object):
     __getitem__ = lambda s, x: _MarkupEscapeHelper(s.obj[x])
     __unicode__ = lambda s: unicode(escape(s.obj))
     __str__ = lambda s: str(escape(s.obj))
-    __repr__ = lambda s: str(repr(escape(s.obj)))
+    __repr__ = lambda s: str(escape(repr(s.obj)))
     __int__ = lambda s: int(s.obj)
     __float__ = lambda s: float(s.obj)
 
 
 class LRUCache(object):
     """A simple LRU Cache implementation."""
-    # this is fast for small capacities (something around 200) but doesn't
-    # scale.  But as long as it's only used for the database connections in
-    # a non request fallback it's fine.
+
+    # this is fast for small capacities (something below 1000) but doesn't
+    # scale.  But as long as it's only used as storage for templates this
+    # won't do any harm.
 
     def __init__(self, capacity):
         self.capacity = capacity
         self._mapping = {}
         self._queue = deque()
+        self._postinit()
 
+    def _postinit(self):
         # alias all queue methods for faster lookup
         self._popleft = self._queue.popleft
         self._pop = self._queue.pop
         if hasattr(self._queue, 'remove'):
             self._remove = self._queue.remove
+        self._wlock = allocate_lock()
         self._append = self._queue.append
 
     def _remove(self, obj):
@@ -227,6 +514,20 @@ class LRUCache(object):
                 del self._queue[idx]
                 break
 
+    def __getstate__(self):
+        return {
+            'capacity':     self.capacity,
+            '_mapping':     self._mapping,
+            '_queue':       self._queue
+        }
+
+    def __setstate__(self, d):
+        self.__dict__.update(d)
+        self._postinit()
+
+    def __getnewargs__(self):
+        return (self.capacity,)
+
     def copy(self):
         """Return an shallow copy of the instance."""
         rv = self.__class__(self.capacity)
@@ -236,23 +537,29 @@ class LRUCache(object):
 
     def get(self, key, default=None):
         """Return an item from the cache dict or `default`"""
-        if key in self:
+        try:
             return self[key]
-        return default
+        except KeyError:
+            return default
 
     def setdefault(self, key, default=None):
         """Set `default` if the key is not in the cache otherwise
         leave unchanged. Return the value of this key.
         """
-        if key in self:
+        try:
             return self[key]
-        self[key] = default
-        return default
+        except KeyError:
+            self[key] = default
+            return default
 
     def clear(self):
         """Clear the cache."""
-        self._mapping.clear()
-        self._queue.clear()
+        self._wlock.acquire()
+        try:
+            self._mapping.clear()
+            self._queue.clear()
+        finally:
+            self._wlock.release()
 
     def __contains__(self, key):
         """Check if a key exists in this cache."""
@@ -284,52 +591,118 @@ class LRUCache(object):
         """Sets the value for an item. Moves the item up so that it
         has the highest priority then.
         """
-        if key in self._mapping:
-            self._remove(key)
-        elif len(self._mapping) == self.capacity:
-            del self._mapping[self._popleft()]
-        self._append(key)
-        self._mapping[key] = value
+        self._wlock.acquire()
+        try:
+            if key in self._mapping:
+                self._remove(key)
+            elif len(self._mapping) == self.capacity:
+                del self._mapping[self._popleft()]
+            self._append(key)
+            self._mapping[key] = value
+        finally:
+            self._wlock.release()
 
     def __delitem__(self, key):
         """Remove an item from the cache dict.
         Raise an `KeyError` if it does not exist.
         """
-        del self._mapping[key]
-        self._remove(key)
+        self._wlock.acquire()
+        try:
+            del self._mapping[key]
+            self._remove(key)
+        finally:
+            self._wlock.release()
+
+    def items(self):
+        """Return a list of items."""
+        result = [(key, self._mapping[key]) for key in list(self._queue)]
+        result.reverse()
+        return result
+
+    def iteritems(self):
+        """Iterate over all items."""
+        return iter(self.items())
+
+    def values(self):
+        """Return a list of all values."""
+        return [x[1] for x in self.items()]
+
+    def itervalue(self):
+        """Iterate over all values."""
+        return iter(self.values())
 
-    def __iter__(self):
-        """Iterate over all values in the cache dict, ordered by
+    def keys(self):
+        """Return a list of all keys ordered by most recent usage."""
+        return list(self)
+
+    def iterkeys(self):
+        """Iterate over all keys in the cache dict, ordered by
         the most recent usage.
         """
-        return reversed(self._queue)
+        return reversed(tuple(self._queue))
+
+    __iter__ = iterkeys
 
     def __reversed__(self):
         """Iterate over the values in the cache dict, oldest items
         coming first.
         """
-        return iter(self._queue)
+        return iter(tuple(self._queue))
 
     __copy__ = copy
 
 
+# register the LRU cache as mutable mapping if possible
+try:
+    from collections import MutableMapping
+    MutableMapping.register(LRUCache)
+except ImportError:
+    pass
+
+
+class Cycler(object):
+    """A cycle helper for templates."""
+
+    def __init__(self, *items):
+        if not items:
+            raise RuntimeError('at least one item has to be provided')
+        self.items = items
+        self.reset()
+
+    def reset(self):
+        """Resets the cycle."""
+        self.pos = 0
+
+    @property
+    def current(self):
+        """Returns the current item."""
+        return self.items[self.pos]
+
+    def next(self):
+        """Goes one item ahead and returns it."""
+        rv = self.current
+        self.pos = (self.pos + 1) % len(self.items)
+        return rv
+
+
 # we have to import it down here as the speedups module imports the
 # markup type which is define above.
 try:
     from jinja2._speedups import escape, soft_unicode
 except ImportError:
-    def escape(obj):
-        """Convert the characters &, <, >, and " in string s to HTML-safe
-        sequences. Use this if you need to display text that might contain
-        such characters in HTML.
+    def escape(s):
+        """Convert the characters &, <, >, and " in string s to HTML-safe
+        sequences.  Use this if you need to display text that might contain
+        such characters in HTML.  Marks return value as markup string.
         """
-        if hasattr(obj, '__html__'):
-            return obj.__html__()
-        return Markup(unicode(obj)
+        if hasattr(s, '__html__'):
+            return s.__html__()
+        return Markup(unicode(s)
             .replace('&', '&amp;')
             .replace('>', '&gt;')
             .replace('<', '&lt;')
-            .replace('"', '&quot;')
+            .replace("'", '&#39;')
+            .replace('"', '&#34;')
         )
 
     def soft_unicode(s):
@@ -339,3 +712,17 @@ except ImportError:
         if not isinstance(s, unicode):
             s = unicode(s)
         return s
+
+
+# partials
+try:
+    from functools import partial
+except ImportError:
+    class partial(object):
+        def __init__(self, _func, *args, **kwargs):
+            self._func = _func
+            self._args = args
+            self._kwargs = kwargs
+        def __call__(self, *args, **kwargs):
+            kwargs.update(self._kwargs)
+            return self._func(*(self._args + args), **kwargs)