summaryrefslogtreecommitdiffstats
path: root/module/lib/jinja2/filters.py
diff options
context:
space:
mode:
authorGravatar Walter Purcaro <vuolter@gmail.com> 2014-07-19 02:01:13 +0200
committerGravatar Walter Purcaro <vuolter@gmail.com> 2014-07-19 02:01:13 +0200
commitac6932aad1e0ccae18fe6332222731502fa119bc (patch)
tree350ceaf26712d4a476e19cfe83cbe14fe4ebb915 /module/lib/jinja2/filters.py
parentFix c1abc13d4dccb20f3845594c28952667573b7d0b (diff)
downloadpyload-ac6932aad1e0ccae18fe6332222731502fa119bc.tar.xz
[Lib] Updated jinja2 to v2.7.3 and markupsafe to v0.23
Diffstat (limited to 'module/lib/jinja2/filters.py')
-rw-r--r--module/lib/jinja2/filters.py366
1 files changed, 317 insertions, 49 deletions
diff --git a/module/lib/jinja2/filters.py b/module/lib/jinja2/filters.py
index d1848e434..fd0db04aa 100644
--- a/module/lib/jinja2/filters.py
+++ b/module/lib/jinja2/filters.py
@@ -10,12 +10,15 @@
"""
import re
import math
+
from random import choice
from operator import itemgetter
-from itertools import imap, groupby
-from jinja2.utils import Markup, escape, pformat, urlize, soft_unicode
+from itertools import groupby
+from jinja2.utils import Markup, escape, pformat, urlize, soft_unicode, \
+ unicode_urlencode
from jinja2.runtime import Undefined
-from jinja2.exceptions import FilterArgumentError, SecurityError
+from jinja2.exceptions import FilterArgumentError
+from jinja2._compat import next, imap, string_types, text_type, iteritems
_word_re = re.compile(r'\w+(?u)')
@@ -48,11 +51,50 @@ 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. Integer parts in paths are
+ looked up as integers.
+ """
+ if not isinstance(attribute, string_types) \
+ or ('.' not in attribute and not attribute.isdigit()):
+ return lambda x: environment.getitem(x, attribute)
+ attribute = attribute.split('.')
+ def attrgetter(item):
+ for part in attribute:
+ if part.isdigit():
+ part = int(part)
+ 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__'):
value = value.__html__()
- return escape(unicode(value))
+ return escape(text_type(value))
+
+
+def do_urlencode(value):
+ """Escape strings for use in URLs (uses UTF-8 encoding). It accepts both
+ dictionaries and regular strings as well as pairwise iterables.
+
+ .. versionadded:: 2.7
+ """
+ itemiter = None
+ if isinstance(value, dict):
+ itemiter = iteritems(value)
+ elif not isinstance(value, string_types):
+ try:
+ itemiter = iter(value)
+ except TypeError:
+ pass
+ if itemiter is None:
+ return unicode_urlencode(value)
+ return u'&'.join(unicode_urlencode(k) + '=' +
+ unicode_urlencode(v) for k, v in itemiter)
@evalcontextfilter
@@ -74,7 +116,7 @@ def do_replace(eval_ctx, s, old, new, count=None):
if count is None:
count = -1
if not eval_ctx.autoescape:
- return unicode(s).replace(unicode(old), unicode(new), count)
+ return text_type(s).replace(text_type(old), text_type(new), count)
if hasattr(old, '__html__') or hasattr(new, '__html__') and \
not hasattr(s, '__html__'):
s = escape(s)
@@ -119,7 +161,7 @@ def do_xmlattr(_eval_ctx, d, autospace=True):
"""
rv = u' '.join(
u'%s="%s"' % (escape(key), escape(value))
- for key, value in d.iteritems()
+ for key, value in iteritems(d)
if value is not None and not isinstance(value, Undefined)
)
if autospace and rv:
@@ -140,7 +182,12 @@ def do_title(s):
"""Return a titlecased version of the value. I.e. words will start with
uppercase letters, all remaining characters are lowercase.
"""
- return soft_unicode(s).title()
+ rv = []
+ for item in re.compile(r'([-\s]+)(?u)').split(s):
+ if not item:
+ continue
+ rv.append(item[0].upper() + item[1:].lower())
+ return ''.join(rv)
def do_dictsort(value, case_sensitive=False, by='key'):
@@ -153,7 +200,7 @@ def do_dictsort(value, case_sensitive=False, by='key'):
{% for item in mydict|dictsort %}
sort the dict by key, case insensitive
- {% for item in mydict|dicsort(true) %}
+ {% for item in mydict|dictsort(true) %}
sort the dict by key, case sensitive
{% for item in mydict|dictsort(false, 'value') %}
@@ -169,14 +216,16 @@ def do_dictsort(value, case_sensitive=False, by='key'):
'"key" or "value"')
def sort_func(item):
value = item[pos]
- if isinstance(value, basestring) and not case_sensitive:
+ if isinstance(value, string_types) and not case_sensitive:
value = value.lower()
return value
return sorted(value.items(), key=sort_func)
-def do_sort(value, reverse=False, case_sensitive=False):
+@environmentfilter
+def do_sort(environment, value, reverse=False, case_sensitive=False,
+ attribute=None):
"""Sort an iterable. Per default it sorts ascending, if you pass it
true as first argument it will reverse the sorting.
@@ -189,14 +238,30 @@ def do_sort(value, reverse=False, case_sensitive=False):
{% for item in iterable|sort %}
...
{% endfor %}
+
+ It is also possible to sort by an attribute (for example to sort
+ by the date of an object) by specifying the `attribute` parameter:
+
+ .. sourcecode:: jinja
+
+ {% for item in iterable|sort(attribute='date') %}
+ ...
+ {% endfor %}
+
+ .. versionchanged:: 2.6
+ The `attribute` parameter was added.
"""
if not case_sensitive:
def sort_func(item):
- if isinstance(item, basestring):
+ if isinstance(item, string_types):
item = item.lower()
return item
else:
sort_func = None
+ if attribute is not None:
+ getter = make_attrgetter(environment, attribute)
+ def sort_func(item, processor=sort_func or (lambda x: x)):
+ return processor(getter(item))
return sorted(value, key=sort_func, reverse=reverse)
@@ -217,13 +282,13 @@ def do_default(value, default_value=u'', boolean=False):
{{ ''|default('the string was empty', true) }}
"""
- if (boolean and not value) or isinstance(value, Undefined):
+ if isinstance(value, Undefined) or (boolean and not value):
return default_value
return value
@evalcontextfilter
-def do_join(eval_ctx, value, d=u''):
+def do_join(eval_ctx, value, d=u'', attribute=None):
"""Return a string which is the concatenation of the strings in the
sequence. The separator between elements is an empty string per
default, you can define it with the optional parameter:
@@ -235,10 +300,22 @@ def do_join(eval_ctx, value, d=u''):
{{ [1, 2, 3]|join }}
-> 123
+
+ It is also possible to join certain attributes of an object:
+
+ .. sourcecode:: jinja
+
+ {{ users|join(', ', attribute='username') }}
+
+ .. versionadded:: 2.6
+ The `attribute` parameter was added.
"""
+ if attribute is not None:
+ value = imap(make_attrgetter(eval_ctx.environment, attribute), value)
+
# no automatic escaping? joining is a lot eaiser then
if not eval_ctx.autoescape:
- return unicode(d).join(imap(unicode, value))
+ return text_type(d).join(imap(text_type, value))
# if the delimiter doesn't have an html representation we check
# if any of the items has. If yes we do a coercion to Markup
@@ -249,11 +326,11 @@ def do_join(eval_ctx, value, d=u''):
if hasattr(item, '__html__'):
do_escape = True
else:
- value[idx] = unicode(item)
+ value[idx] = text_type(item)
if do_escape:
d = escape(d)
else:
- d = unicode(d)
+ d = text_type(d)
return d.join(value)
# no html involved, to normal joining
@@ -262,14 +339,14 @@ def do_join(eval_ctx, value, d=u''):
def do_center(value, width=80):
"""Centers the value in a field of a given width."""
- return unicode(value).center(width)
+ return text_type(value).center(width)
@environmentfilter
def do_first(environment, seq):
"""Return the first item of a sequence."""
try:
- return iter(seq).next()
+ return next(iter(seq))
except StopIteration:
return environment.undefined('No first item, sequence was empty.')
@@ -278,7 +355,7 @@ def do_first(environment, seq):
def do_last(environment, seq):
"""Return the last item of a sequence."""
try:
- return iter(reversed(seq)).next()
+ return next(iter(reversed(seq)))
except StopIteration:
return environment.undefined('No last item, sequence was empty.')
@@ -293,21 +370,33 @@ def do_random(environment, seq):
def do_filesizeformat(value, binary=False):
- """Format the value like a 'human-readable' file size (i.e. 13 KB,
- 4.1 MB, 102 bytes, etc). Per default decimal prefixes are used (mega,
- giga, etc.), if the second parameter is set to `True` the binary
- prefixes are used (mebi, gibi).
+ """Format the value like a 'human-readable' file size (i.e. 13 kB,
+ 4.1 MB, 102 Bytes, etc). Per default decimal prefixes are used (Mega,
+ Giga, etc.), if the second parameter is set to `True` the binary
+ prefixes are used (Mebi, Gibi).
"""
bytes = float(value)
base = binary and 1024 or 1000
- middle = binary and 'i' or ''
- if bytes < base:
- return "%d Byte%s" % (bytes, bytes != 1 and 's' or '')
- elif bytes < base * base:
- return "%.1f K%sB" % (bytes / base, middle)
- elif bytes < base * base * base:
- return "%.1f M%sB" % (bytes / (base * base), middle)
- return "%.1f G%sB" % (bytes / (base * base * base), middle)
+ prefixes = [
+ (binary and 'KiB' or 'kB'),
+ (binary and 'MiB' or 'MB'),
+ (binary and 'GiB' or 'GB'),
+ (binary and 'TiB' or 'TB'),
+ (binary and 'PiB' or 'PB'),
+ (binary and 'EiB' or 'EB'),
+ (binary and 'ZiB' or 'ZB'),
+ (binary and 'YiB' or 'YB')
+ ]
+ if bytes == 1:
+ return '1 Byte'
+ elif bytes < base:
+ return '%d Bytes' % bytes
+ else:
+ for i, prefix in enumerate(prefixes):
+ unit = base ** (i + 2)
+ if bytes < unit:
+ return '%.1f %s' % ((base * bytes / unit), prefix)
+ return '%.1f %s' % ((base * bytes / unit), prefix)
def do_pprint(value, verbose=False):
@@ -360,16 +449,17 @@ def do_truncate(s, length=255, killwords=False, end='...'):
"""Return a truncated copy of the string. The length is specified
with the first parameter which defaults to ``255``. If the second
parameter is ``true`` the filter will cut the text at length. Otherwise
- it will try to save the last word. If the text was in fact
+ it will discard the last word. If the text was in fact
truncated it will append an ellipsis sign (``"..."``). If you want a
different ellipsis sign than ``"..."`` you can specify it using the
third parameter.
- .. sourcecode jinja::
+ .. sourcecode:: jinja
- {{ mytext|truncate(300, false, '&raquo;') }}
- truncate mytext to 300 chars, don't split up words, use a
- right pointing double arrow as ellipsis sign.
+ {{ "foo bar"|truncate(5) }}
+ -> "foo ..."
+ {{ "foo bar"|truncate(5, True) }}
+ -> "foo b..."
"""
if len(s) <= length:
return s
@@ -386,16 +476,24 @@ def do_truncate(s, length=255, killwords=False, end='...'):
result.append(end)
return u' '.join(result)
-
-def do_wordwrap(s, width=79, break_long_words=True):
+@environmentfilter
+def do_wordwrap(environment, s, width=79, break_long_words=True,
+ wrapstring=None):
"""
Return a copy of the string passed to the filter wrapped after
``79`` characters. You can override this default using the first
parameter. If you set the second parameter to `false` Jinja will not
- split words apart if they are longer than `width`.
+ split words apart if they are longer than `width`. By default, the newlines
+ will be the default newlines for the environment, but this can be changed
+ using the wrapstring keyword argument.
+
+ .. versionadded:: 2.7
+ Added support for the `wrapstring` parameter.
"""
+ if not wrapstring:
+ wrapstring = environment.newline_sequence
import textwrap
- return u'\n'.join(textwrap.wrap(s, width=width, expand_tabs=False,
+ return wrapstring.join(textwrap.wrap(s, width=width, expand_tabs=False,
replace_whitespace=False,
break_long_words=break_long_words))
@@ -456,7 +554,7 @@ def do_striptags(value):
"""
if hasattr(value, '__html__'):
value = value.__html__()
- return Markup(unicode(value)).striptags()
+ return Markup(text_type(value)).striptags()
def do_slice(value, slices, fill_with=None):
@@ -484,7 +582,7 @@ def do_slice(value, slices, fill_with=None):
items_per_slice = length // slices
slices_with_extra = length % slices
offset = 0
- for slice_number in xrange(slices):
+ for slice_number in range(slices):
start = offset + slice_number * items_per_slice
if slice_number < slices_with_extra:
offset += 1
@@ -500,7 +598,7 @@ def do_batch(value, linecount, fill_with=None):
A filter that batches items. It works pretty much like `slice`
just the other way round. It returns a list of lists with the
given number of items. If you provide a second parameter this
- is used to fill missing items. See this example:
+ is used to fill up missing items. See this example:
.. sourcecode:: html+jinja
@@ -595,8 +693,12 @@ def do_groupby(environment, value, attribute):
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.
+
+ .. versionchanged:: 2.6
+ It's now possible to use dotted notation to group by the child
+ attribute of another attribute.
"""
- expr = lambda x: environment.getitem(x, attribute)
+ expr = make_attrgetter(environment, attribute)
return sorted(map(_GroupTuple, groupby(sorted(value, key=expr), expr)))
@@ -605,10 +707,32 @@ class _GroupTuple(tuple):
grouper = property(itemgetter(0))
list = property(itemgetter(1))
- def __new__(cls, (key, value)):
+ def __new__(cls, xxx_todo_changeme):
+ (key, value) = xxx_todo_changeme
return tuple.__new__(cls, (key, list(value)))
+@environmentfilter
+def do_sum(environment, iterable, attribute=None, start=0):
+ """Returns the sum of a sequence of numbers plus the value of parameter
+ 'start' (which defaults to 0). When the sequence is empty it returns
+ start.
+
+ It is also possible to sum up only certain attributes:
+
+ .. sourcecode:: jinja
+
+ Total: {{ items|sum(attribute='price') }}
+
+ .. versionchanged:: 2.6
+ The `attribute` parameter was added to allow suming up over
+ attributes. Also the `start` parameter was moved on to the right.
+ """
+ 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.
@@ -625,14 +749,14 @@ def do_mark_safe(value):
def do_mark_unsafe(value):
"""Mark a value as unsafe. This is the reverse operation for :func:`safe`."""
- return unicode(value)
+ return text_type(value)
def do_reverse(value):
"""Reverse the object or return an iterator the iterates over it the other
way round.
"""
- if isinstance(value, basestring):
+ if isinstance(value, string_types):
return value[::-1]
try:
return reversed(value)
@@ -670,6 +794,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' %
+ next(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,
@@ -694,7 +956,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,
@@ -708,12 +973,15 @@ FILTERS = {
'format': do_format,
'trim': do_trim,
'striptags': do_striptags,
+ 'select': do_select,
+ 'selectattr': do_selectattr,
'slice': do_slice,
'batch': do_batch,
- 'sum': sum,
+ 'sum': do_sum,
'abs': abs,
'round': do_round,
'groupby': do_groupby,
'safe': do_mark_safe,
- 'xmlattr': do_xmlattr
+ 'xmlattr': do_xmlattr,
+ 'urlencode': do_urlencode
}