The sum filter can now sum up attributes
authorArmin Ronacher <armin.ronacher@active-4.com>
Sun, 23 Jan 2011 23:57:58 +0000 (00:57 +0100)
committerArmin Ronacher <armin.ronacher@active-4.com>
Sun, 23 Jan 2011 23:57:58 +0000 (00:57 +0100)
CHANGES
jinja2/filters.py
jinja2/testsuite/filters.py

diff --git a/CHANGES b/CHANGES
index 7815a6db634f58d2598b8d6643ce62a138ec0564..a410c9ed612c9899055e4903203d2da1ee79db12 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -24,6 +24,10 @@ Version 2.6
   earlier versions after an upgrade of the Python interpreter you don't have
   to upgrade, it's enough to flush the bytecode cache.  This just no longer
   makes this necessary, Jinja2 will automatically detect these cases now.
+- the sum filter can now sum up values by attribute.  This is a backwards
+  incompatible change.  The argument to the filter previously was the
+  optional starting index which defaultes to zero.  This now became the
+  second argument to the function because it's rarely used.
 
 Version 2.5.5
 -------------
index 3a81f70ba045b5f0bead3e6036c6a899a8c75ef8..82f0153da773287b1edf9941df3862b5001e38d1 100644 (file)
@@ -48,6 +48,21 @@ def environmentfilter(f):
     return f
 
 
+def make_attrgetter(environment, attribute):
+    """Returns a callable that looks up the given attribute from a
+    passed object with the rules of the environment.  Dots are allowed
+    to access attributes of attributes.
+    """
+    if '.' not in attribute:
+        return lambda x: environment.getitem(x, attribute)
+    attribute = attribute.split('.')
+    def attrgetter(item):
+        for part in attribute:
+            item = environment.getitem(item, part)
+        return item
+    return attrgetter
+
+
 def do_forceescape(value):
     """Enforce HTML escaping.  This will probably double escape variables."""
     if hasattr(value, '__html__'):
@@ -600,13 +615,7 @@ def do_groupby(environment, value, attribute):
        It's now possible to use dotted notation to group by the child
        attribute of another attribute.
     """
-    if '.' in attribute:
-        def expr(item):
-            for part in attribute.split('.'):
-                item = environment.getitem(item, part)
-            return item
-    else:
-        expr = lambda x: environment.getitem(x, attribute)
+    expr = make_attrgetter(environment, attribute)
     return sorted(map(_GroupTuple, groupby(sorted(value, key=expr), expr)))
 
 
@@ -619,6 +628,19 @@ class _GroupTuple(tuple):
         return tuple.__new__(cls, (key, list(value)))
 
 
+@environmentfilter
+def do_sum(environment, iterable, attribute=None, start=0):
+    """Sums up an iterable.
+
+    .. versionchanged:: 2.6
+       The `attribute` parameter was added to allow suming up over
+       attributes.
+    """
+    if attribute is not None:
+        iterable = imap(make_attrgetter(environment, attribute), iterable)
+    return sum(iterable, start)
+
+
 def do_list(value):
     """Convert the value into a list.  If it was a string the returned list
     will be a list of characters.
@@ -720,7 +742,7 @@ FILTERS = {
     'striptags':            do_striptags,
     'slice':                do_slice,
     'batch':                do_batch,
-    'sum':                  sum,
+    'sum':                  do_sum,
     'abs':                  abs,
     'round':                do_round,
     'groupby':              do_groupby,
index 80a6f805ffc5396d931c041171e36949594e5e88..6331d76e424dd8c5600c6b62c2fbaa619af700f7 100644 (file)
@@ -204,6 +204,22 @@ class FilterTestCase(JinjaTestCase):
         tmpl = env.from_string('''{{ [1, 2, 3, 4, 5, 6]|sum }}''')
         assert tmpl.render() == '21'
 
+    def test_sum_attributes(self):
+        tmpl = env.from_string('''{{ values|sum('value') }}''')
+        assert tmpl.render(values=[
+            {'value': 23},
+            {'value': 1},
+            {'value': 18},
+        ]) == '42'
+
+    def test_sum_attributes_nested(self):
+        tmpl = env.from_string('''{{ values|sum('real.value') }}''')
+        assert tmpl.render(values=[
+            {'real': {'value': 23}},
+            {'real': {'value': 1}},
+            {'real': {'value': 18}},
+        ]) == '42'
+
     def test_abs(self):
         tmpl = env.from_string('''{{ -1|abs }}|{{ 1|abs }}''')
         assert tmpl.render() == '1|1', tmpl.render()
@@ -234,9 +250,13 @@ class FilterTestCase(JinjaTestCase):
         assert tmpl.render() == '[1, 2, 3]|[3, 2, 1]'
 
     def test_sort2(self):
-        tmpl = env.from_string('{{ "".join(["c", "A", "b", "D"]|sort(false, true)) }}')
+        tmpl = env.from_string('{{ "".join(["c", "A", "b", "D"]|sort) }}')
         assert tmpl.render() == 'AbcD'
 
+    def test_sort3(self):
+        tmpl = env.from_string('''{{ ['foo', 'Bar', 'blah']|sort }}''')
+        assert tmpl.render() == "['Bar', 'blah', 'foo']"
+
     def test_groupby(self):
         tmpl = env.from_string('''
         {%- for grouper, list in [{'foo': 1, 'bar': 2},
@@ -306,10 +326,6 @@ class FilterTestCase(JinjaTestCase):
         tmpl = env.from_string('{{ "<div>foo</div>" }}')
         assert tmpl.render() == '&lt;div&gt;foo&lt;/div&gt;'
 
-    def test_sort2(self):
-        tmpl = env.from_string('''{{ ['foo', 'Bar', 'blah']|sort }}''')
-        assert tmpl.render() == "['Bar', 'blah', 'foo']"
-
 
 def suite():
     suite = unittest.TestSuite()