]> git.ipfire.org Git - thirdparty/jinja.git/commitdiff
Added support for map, select, reject, selectattr and rejectattr
authorArmin Ronacher <armin.ronacher@active-4.com>
Sun, 19 May 2013 11:25:32 +0000 (12:25 +0100)
committerArmin Ronacher <armin.ronacher@active-4.com>
Sun, 19 May 2013 11:25:32 +0000 (12:25 +0100)
filters.

This supercedes #66

CHANGES
jinja2/environment.py
jinja2/filters.py
jinja2/testsuite/filters.py

diff --git a/CHANGES b/CHANGES
index af99f10052e76a18ac70e6faf7ddaf985e5c75b8..d45f78cce483ca3678a8bc23906f72c535bdf52c 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -26,6 +26,8 @@ Version 2.7
 - Added support for keeping the trailing newline in templates.
 - Added finer grained support for stripping whitespace on the left side
   of blocks.
+- Added `map`, `select`, `reject`, `selectattr` and `rejectattr`
+  filters.
 
 Version 2.6
 -----------
index 9ffb5ee7b4487d6c06634bc152799703550ab622..450cac147ab33ab6e2c31c8c9aeb6ac3389ed7e9 100644 (file)
@@ -19,11 +19,12 @@ from jinja2.defaults import BLOCK_START_STRING, \
      KEEP_TRAILING_NEWLINE, LSTRIP_BLOCKS
 from jinja2.lexer import get_lexer, TokenStream
 from jinja2.parser import Parser
+from jinja2.nodes import EvalContext
 from jinja2.optimizer import optimize
 from jinja2.compiler import generate
 from jinja2.runtime import Undefined, new_context
 from jinja2.exceptions import TemplateSyntaxError, TemplateNotFound, \
-     TemplatesNotFound
+     TemplatesNotFound, TemplateRuntimeError
 from jinja2.utils import import_string, LRUCache, Markup, missing, \
      concat, consume, internalcode, _encode_filename
 import six
@@ -400,6 +401,42 @@ class Environment(object):
         except (TypeError, LookupError, AttributeError):
             return self.undefined(obj=obj, name=attribute)
 
+    def call_filter(self, name, value, args=None, kwargs=None,
+                    context=None, eval_ctx=None):
+        """Invokes a filter on a value the same way the compiler does it.
+
+        .. versionadded:: 2.7
+        """
+        func = self.filters.get(name)
+        if func is None:
+            raise TemplateRuntimeError('no filter named %r' % name)
+        args = list(args or ())
+        if getattr(func, 'contextfilter', False):
+            if context is None:
+                raise TemplateRuntimeError('Attempted to invoke context '
+                                           'filter without context')
+            args.insert(0, context)
+        elif getattr(func, 'evalcontextfilter', False):
+            if eval_ctx is None:
+                if context is not None:
+                    eval_ctx = context.eval_ctx
+                else:
+                    eval_ctx = EvalContext(self)
+            args.insert(0, eval_ctx)
+        elif getattr(func, 'environmentfilter', False):
+            args.insert(0, self)
+        return func(value, *args, **(kwargs or {}))
+
+    def call_test(self, name, value, args=None, kwargs=None):
+        """Invokes a test on a value the same way the compiler does it.
+
+        .. versionadded:: 2.7
+        """
+        func = self.tests.get(name)
+        if func is None:
+            raise TemplateRuntimeError('no test named %r' % name)
+        return func(value, *(args or ()), **(kwargs or {}))
+
     @internalcode
     def parse(self, source, name=None, filename=None):
         """Parse the sourcecode and return the abstract syntax tree.  This
index 9dd693d537ba3a2760be8511007439d60146abed..adc513e17e10568fd466d4be879f19eead9004e2 100644 (file)
@@ -790,6 +790,144 @@ def do_attr(environment, obj, name):
     return environment.undefined(obj=obj, name=name)
 
 
+@contextfilter
+def do_map(*args, **kwargs):
+    """Applies a filter on a sequence of objects or looks up an attribute.
+    This is useful when dealing with lists of objects but you are really
+    only interested in a certain value of it.
+
+    The basic usage is mapping on an attribute.  Imagine you have a list
+    of users but you are only interested in a list of usernames:
+
+    .. sourcecode:: jinja
+
+        Users on this page: {{ users|map(attribute='username')|join(', ') }}
+
+    Alternatively you can let it invoke a filter by passing the name of the
+    filter and the arguments afterwards.  A good example would be applying a
+    text conversion filter on a sequence:
+
+    .. sourcecode:: jinja
+
+        Users on this page: {{ titles|map('lower')|join(', ') }}
+
+    .. versionadded:: 2.7
+    """
+    context = args[0]
+    seq = args[1]
+
+    if len(args) == 2 and 'attribute' in kwargs:
+        attribute = kwargs.pop('attribute')
+        if kwargs:
+            raise FilterArgumentError('Unexpected keyword argument %r' %
+                six.advance_iterator(iter(kwargs)))
+        func = make_attrgetter(context.environment, attribute)
+    else:
+        try:
+            name = args[2]
+            args = args[3:]
+        except LookupError:
+            raise FilterArgumentError('map requires a filter argument')
+        func = lambda item: context.environment.call_filter(
+            name, item, args, kwargs, context=context)
+
+    if seq:
+        for item in seq:
+            yield func(item)
+
+
+@contextfilter
+def do_select(*args, **kwargs):
+    """Filters a sequence of objects by appying a test to either the object
+    or the attribute and only selecting the ones with the test succeeding.
+
+    Example usage:
+
+    .. sourcecode:: jinja
+
+        {{ numbers|select("odd") }}
+
+    .. versionadded:: 2.7
+    """
+    return _select_or_reject(args, kwargs, lambda x: x, False)
+
+
+@contextfilter
+def do_reject(*args, **kwargs):
+    """Filters a sequence of objects by appying a test to either the object
+    or the attribute and rejecting the ones with the test succeeding.
+
+    Example usage:
+
+    .. sourcecode:: jinja
+
+        {{ numbers|reject("odd") }}
+
+    .. versionadded:: 2.7
+    """
+    return _select_or_reject(args, kwargs, lambda x: not x, False)
+
+
+@contextfilter
+def do_selectattr(*args, **kwargs):
+    """Filters a sequence of objects by appying a test to either the object
+    or the attribute and only selecting the ones with the test succeeding.
+
+    Example usage:
+
+    .. sourcecode:: jinja
+
+        {{ users|selectattr("is_active") }}
+        {{ users|selectattr("email", "none") }}
+
+    .. versionadded:: 2.7
+    """
+    return _select_or_reject(args, kwargs, lambda x: x, True)
+
+
+@contextfilter
+def do_rejectattr(*args, **kwargs):
+    """Filters a sequence of objects by appying a test to either the object
+    or the attribute and rejecting the ones with the test succeeding.
+
+    .. sourcecode:: jinja
+
+        {{ users|rejectattr("is_active") }}
+        {{ users|rejectattr("email", "none") }}
+
+    .. versionadded:: 2.7
+    """
+    return _select_or_reject(args, kwargs, lambda x: not x, True)
+
+
+def _select_or_reject(args, kwargs, modfunc, lookup_attr):
+    context = args[0]
+    seq = args[1]
+    if lookup_attr:
+        try:
+            attr = args[2]
+        except LookupError:
+            raise FilterArgumentError('Missing parameter for attribute name')
+        transfunc = make_attrgetter(context.environment, attr)
+        off = 1
+    else:
+        off = 0
+        transfunc = lambda x: x
+
+    try:
+        name = args[2 + off]
+        args = args[3 + off:]
+        func = lambda item: context.environment.call_test(
+            name, item, args, kwargs)
+    except LookupError:
+        func = bool
+
+    if seq:
+        for item in seq:
+            if modfunc(func(transfunc(item))):
+                yield item
+
+
 FILTERS = {
     'attr':                 do_attr,
     'replace':              do_replace,
@@ -814,7 +952,10 @@ FILTERS = {
     'capitalize':           do_capitalize,
     'first':                do_first,
     'last':                 do_last,
+    'map':                  do_map,
     'random':               do_random,
+    'reject':               do_reject,
+    'rejectattr':           do_rejectattr,
     'filesizeformat':       do_filesizeformat,
     'pprint':               do_pprint,
     'truncate':             do_truncate,
@@ -828,6 +969,8 @@ FILTERS = {
     'format':               do_format,
     'trim':                 do_trim,
     'striptags':            do_striptags,
+    'select':               do_select,
+    'selectattr':           do_selectattr,
     'slice':                do_slice,
     'batch':                do_batch,
     'sum':                  do_sum,
index 7f6b3141a1ca0f76095866a4014dd954ea021d73..88f93f97e007bb944e7d4d65f74bffab95dfa2a5 100644 (file)
@@ -392,6 +392,109 @@ class FilterTestCase(JinjaTestCase):
         assert tmpl.render(o={u"\u203d": 1}) == "%E2%80%BD=1"
         assert tmpl.render(o={0: 1}) == "0=1"
 
+    def test_simple_map(self):
+        env = Environment()
+        tmpl = env.from_string('{{ ["1", "2", "3"]|map("int")|sum }}')
+        self.assertEqual(tmpl.render(), '6')
+
+    def test_attribute_map(self):
+        class User(object):
+            def __init__(self, name):
+                self.name = name
+        env = Environment()
+        users = [
+            User('john'),
+            User('jane'),
+            User('mike'),
+        ]
+        tmpl = env.from_string('{{ users|map(attribute="name")|join("|") }}')
+        self.assertEqual(tmpl.render(users=users), 'john|jane|mike')
+
+    def test_empty_map(self):
+        env = Environment()
+        tmpl = env.from_string('{{ none|map("upper")|list }}')
+        self.assertEqual(tmpl.render(), '[]')
+
+    def test_simple_select(self):
+        env = Environment()
+        tmpl = env.from_string('{{ [1, 2, 3, 4, 5]|select("odd")|join("|") }}')
+        self.assertEqual(tmpl.render(), '1|3|5')
+
+    def test_bool_select(self):
+        env = Environment()
+        tmpl = env.from_string('{{ [none, false, 0, 1, 2, 3, 4, 5]|select|join("|") }}')
+        self.assertEqual(tmpl.render(), '1|2|3|4|5')
+
+    def test_simple_reject(self):
+        env = Environment()
+        tmpl = env.from_string('{{ [1, 2, 3, 4, 5]|reject("odd")|join("|") }}')
+        self.assertEqual(tmpl.render(), '2|4')
+
+    def test_bool_reject(self):
+        env = Environment()
+        tmpl = env.from_string('{{ [none, false, 0, 1, 2, 3, 4, 5]|reject|join("|") }}')
+        self.assertEqual(tmpl.render(), 'None|False|0')
+
+    def test_simple_select_attr(self):
+        class User(object):
+            def __init__(self, name, is_active):
+                self.name = name
+                self.is_active = is_active
+        env = Environment()
+        users = [
+            User('john', True),
+            User('jane', True),
+            User('mike', False),
+        ]
+        tmpl = env.from_string('{{ users|selectattr("is_active")|'
+            'map(attribute="name")|join("|") }}')
+        self.assertEqual(tmpl.render(users=users), 'john|jane')
+
+    def test_simple_reject_attr(self):
+        class User(object):
+            def __init__(self, name, is_active):
+                self.name = name
+                self.is_active = is_active
+        env = Environment()
+        users = [
+            User('john', True),
+            User('jane', True),
+            User('mike', False),
+        ]
+        tmpl = env.from_string('{{ users|rejectattr("is_active")|'
+            'map(attribute="name")|join("|") }}')
+        self.assertEqual(tmpl.render(users=users), 'mike')
+
+    def test_func_select_attr(self):
+        class User(object):
+            def __init__(self, id, name):
+                self.id = id
+                self.name = name
+        env = Environment()
+        users = [
+            User(1, 'john'),
+            User(2, 'jane'),
+            User(3, 'mike'),
+        ]
+        tmpl = env.from_string('{{ users|selectattr("id", "odd")|'
+            'map(attribute="name")|join("|") }}')
+        self.assertEqual(tmpl.render(users=users), 'john|mike')
+
+    def test_func_reject_attr(self):
+        class User(object):
+            def __init__(self, id, name):
+                self.id = id
+                self.name = name
+        env = Environment()
+        users = [
+            User(1, 'john'),
+            User(2, 'jane'),
+            User(3, 'mike'),
+        ]
+        tmpl = env.from_string('{{ users|rejectattr("id", "odd")|'
+            'map(attribute="name")|join("|") }}')
+        self.assertEqual(tmpl.render(users=users), 'jane')
+
 
 def suite():
     suite = unittest.TestSuite()