[svn] added groupby filter and fixed some small bugs

--HG--
branch : trunk
diff --git a/CHANGES b/CHANGES
index fca9a18..6bf0278 100644
--- a/CHANGES
+++ b/CHANGES
@@ -12,6 +12,8 @@
 
 - once again improved debugger.
 
+- added `groupby` filter.
+
 
 Version 1.1
 -----------
diff --git a/jinja/constants.py b/jinja/constants.py
index be66e0b..a0b4a63 100644
--- a/jinja/constants.py
+++ b/jinja/constants.py
@@ -10,6 +10,7 @@
 """
 
 
+#: list of lorem ipsum words used by the lipsum() helper function
 LOREM_IPSUM_WORDS = u'''\
 a ac accumsan ad adipiscing aenean aliquam aliquet amet ante aptent arcu at
 auctor augue bibendum blandit class commodo condimentum congue consectetuer
diff --git a/jinja/debugger.py b/jinja/debugger.py
index bc60272..9078b09 100644
--- a/jinja/debugger.py
+++ b/jinja/debugger.py
@@ -38,8 +38,6 @@
 
 import sys
 from random import randrange
-from opcode import opmap
-from types import CodeType
 
 # if we have extended debugger support we should really use it
 try:
@@ -154,7 +152,8 @@
 
 class TracebackLoader(object):
     """
-    Fake importer that just returns the source of a template.
+    Fake importer that just returns the source of a template. It's just used
+    by Jinja interally and you shouldn't use it on your own.
     """
 
     def __init__(self, environment, source, filename):
@@ -168,7 +167,8 @@
         Jinja template sourcecode. Very hackish indeed.
         """
         # check for linecache, not every implementation of python
-        # might have such an module.
+        # might have such an module (this check is pretty senseless
+        # because we depend on cpython anway)
         try:
             from linecache import cache
         except ImportError:
diff --git a/jinja/filters.py b/jinja/filters.py
index 619148b..3f621a7 100644
--- a/jinja/filters.py
+++ b/jinja/filters.py
@@ -11,7 +11,7 @@
 import re
 from random import choice
 from urllib import urlencode, quote
-from jinja.utils import urlize, escape, reversed, sorted
+from jinja.utils import urlize, escape, reversed, sorted, groupby
 from jinja.datastructure import TemplateData
 from jinja.exceptions import FilterArgumentError
 
@@ -849,6 +849,39 @@
     return wrapped
 
 
+def do_groupby(attribute):
+    """
+    Group a sequence of objects by a common attribute.
+
+    If you for example have a list of dicts or objects that represent persons
+    with `gender`, `first_name` and `last_name` attributes and you want to
+    group all users by genders you can do something like the following
+    snippet:
+
+    .. sourcecode:: html+jinja
+
+        <ul>
+        {% for group in persons|groupby('gender') %}
+            <li>{{ group.grouper }}<ul>
+            {% for person in group.list %}
+                <li>{{ person.first_name }} {{ person.last_name }}</li>
+            {% endfor %}</ul></li>
+        {% endfor %}
+        </ul>
+
+    As you can see the item we're grouping by is stored in the `grouper`
+    attribute and the `list` contains all the objects that have this grouper
+    in common.
+    """
+    def wrapped(env, context, value):
+        expr = lambda x: env.get_attribute(x, attribute)
+        return [{
+            'grouper':  a,
+            'list':     list(b)
+        } for a, b in groupby(sorted(value, key=expr), expr)]
+    return wrapped
+
+
 FILTERS = {
     'replace':              do_replace,
     'upper':                do_upper,
@@ -895,5 +928,6 @@
     'sum':                  do_sum,
     'abs':                  do_abs,
     'round':                do_round,
-    'sort':                 do_sort
+    'sort':                 do_sort,
+    'groupby':              do_groupby
 }
diff --git a/jinja/utils.py b/jinja/utils.py
index eee5506..c4a2bb2 100644
--- a/jinja/utils.py
+++ b/jinja/utils.py
@@ -75,6 +75,33 @@
 except ImportError:
     has_extended_debugger = False
 
+# group by support
+try:
+    from itertools import groupby
+except ImportError:
+    class groupby(object):
+
+        def __init__(self, iterable, key=lambda x: x):
+            self.keyfunc = key
+            self.it = iter(iterable)
+            self.tgtkey = self.currkey = self.currvalue = xrange(0)
+
+        def __iter__(self):
+            return self
+
+        def next(self):
+            while self.currkey == self.tgtkey:
+                self.currvalue = self.it.next()
+                self.currkey = self.keyfunc(self.currvalue)
+            self.tgtkey = self.currkey
+            return (self.currkey, self._grouper(self.tgtkey))
+
+        def _grouper(self, tgtkey):
+            while self.currkey == tgtkey:
+                yield self.currvalue
+                self.currvalue = self.it.next()
+                self.currkey = self.keyfunc(self.currvalue)
+
 #: function types
 callable_types = (FunctionType, MethodType)
 
diff --git a/tests/runtime/exception.py b/tests/runtime/exception.py
index 6043746..0af7955 100644
--- a/tests/runtime/exception.py
+++ b/tests/runtime/exception.py
@@ -54,9 +54,8 @@
 {% include 'syntax_broken' %}
     ''',
 
-    '/code_runtime_error': u'''
-{{ broken() }}
-''',
+    '/code_runtime_error': u'''We have a runtime error here:
+    {{ broken() }}''',
 
     'runtime_broken': '''\
 This is an included template
diff --git a/tests/test_filters.py b/tests/test_filters.py
index 2755224..a3581ea 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -62,6 +62,10 @@
 XMLATTR = '''{{ {'foo': 42, 'bar': 23, 'fish': none,
 'spam': missing, 'blub:blub': '<?>'}|xmlattr }}'''
 SORT = '''{{ [2, 3, 1]|sort }}|{{ [2, 3, 1]|sort(true) }}'''
+GROUPBY = '''{{ [{'foo': 1, 'bar': 2},
+                 {'foo': 2, 'bar': 3},
+                 {'foo': 1, 'bar': 1},
+                 {'foo': 3, 'bar': 4}]|groupby('foo') }}'''
 
 
 
@@ -282,3 +286,12 @@
 def test_sort(env):
     tmpl = env.from_string(SORT)
     assert tmpl.render() == '[1, 2, 3]|[3, 2, 1]'
+
+
+def test_groupby(env):
+    tmpl = env.from_string(GROUPBY)
+    assert tmpl.render() == (
+        "[{'list': [{'foo': 1, 'bar': 2}, {'foo': 1, 'bar': 1}], "
+        "'grouper': 1}, {'list': [{'foo': 2, 'bar': 3}], 'grouper': 2}, "
+        "{'list': [{'foo': 3, 'bar': 4}], 'grouper': 3}]"
+    )