[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}]"
+ )