filters.
This supercedes #66
- 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
-----------
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
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
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,
'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,
'format': do_format,
'trim': do_trim,
'striptags': do_striptags,
+ 'select': do_select,
+ 'selectattr': do_selectattr,
'slice': do_slice,
'batch': do_batch,
'sum': do_sum,
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()