[svn] added groupby filter and fixed some small bugs
authorArmin Ronacher <armin.ronacher@active-4.com>
Sat, 23 Jun 2007 19:11:53 +0000 (21:11 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Sat, 23 Jun 2007 19:11:53 +0000 (21:11 +0200)
--HG--
branch : trunk

CHANGES
jinja/constants.py
jinja/debugger.py
jinja/filters.py
jinja/utils.py
tests/runtime/exception.py
tests/test_filters.py

diff --git a/CHANGES b/CHANGES
index fca9a188db3182a1d2c59327f55d58061efebdcf..6bf0278d39440bf5d40f92a7909da057a331019c 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -12,6 +12,8 @@ Version 1.2
 
 - once again improved debugger.
 
+- added `groupby` filter.
+
 
 Version 1.1
 -----------
index be66e0bc0a73f638de51bfbc06cf42c6c8a439ee..a0b4a6366b14ab950f61e01e86b3b65dee45f0db 100644 (file)
@@ -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
index bc6027268ebdb40a3dbd6ae3040aa0047368b36f..9078b090609a1c2d0b5472a96dd109115fa4ff32 100644 (file)
@@ -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 @@ def raise_syntax_error(exception, env, source=None):
 
 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 @@ class TracebackLoader(object):
         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:
index 619148b8aa7d92de02119397741dd66e8ebd3fef..3f621a7dabef7b8b049d9d9cf6c28c94dc1a66fa 100644 (file)
@@ -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 @@ def do_sort(reverse=False):
     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 @@ FILTERS = {
     'sum':                  do_sum,
     'abs':                  do_abs,
     'round':                do_round,
-    'sort':                 do_sort
+    'sort':                 do_sort,
+    'groupby':              do_groupby
 }
index eee55061ab2b4965e1b1d96a368669d8de5530e1..c4a2bb2cb92f7c8e25db6420b0e918ce902ccf7f 100644 (file)
@@ -75,6 +75,33 @@ try:
 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)
 
index 60437469830f0a98f008b8df5f480bfadfd0205f..0af7955a4b4e73db76a8ef8fc9c9cb6b81136239 100644 (file)
@@ -54,9 +54,8 @@ e = Environment(loader=DictLoader({
 {% 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
index 2755224ab2afe59a05f74c3b92c492f41f9b5399..a3581ea5ec37ada9476d8344a429c8fa80d8b7a6 100644 (file)
@@ -62,6 +62,10 @@ ROUND = '''{{ 2.7|round }}|{{ 2.1|round }}|\
 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_xmlattr(env):
 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}]"
+    )