From e39a5d2da7dfd38152ff0c0712b918b7e058ebde Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 23 Jun 2007 21:11:53 +0200 Subject: [PATCH] [svn] added groupby filter and fixed some small bugs --HG-- branch : trunk --- CHANGES | 2 ++ jinja/constants.py | 1 + jinja/debugger.py | 8 ++++---- jinja/filters.py | 38 ++++++++++++++++++++++++++++++++++++-- jinja/utils.py | 27 +++++++++++++++++++++++++++ tests/runtime/exception.py | 5 ++--- tests/test_filters.py | 13 +++++++++++++ 7 files changed, 85 insertions(+), 9 deletions(-) diff --git a/CHANGES b/CHANGES index fca9a18..6bf0278 100644 --- a/CHANGES +++ b/CHANGES @@ -12,6 +12,8 @@ Version 1.2 - 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 @@ 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: 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 @@ 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 + + + + 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 } 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 @@ 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) 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 @@ 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 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 @@ 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}]" + ) -- 2.26.2