Index: third_party/jinja2/filters.py |
diff --git a/third_party/jinja2/filters.py b/third_party/jinja2/filters.py |
index 1ef47f95161b7ef8f07dcd79bc86dc3582f6c985..fd0db04aa41026500910347cf401fe620c556188 100644 |
--- a/third_party/jinja2/filters.py |
+++ b/third_party/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)') |
@@ -51,13 +54,17 @@ def environmentfilter(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. |
+ to access attributes of attributes. Integer parts in paths are |
+ looked up as integers. |
""" |
- if not isinstance(attribute, basestring) or '.' not in attribute: |
+ 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 |
@@ -67,7 +74,27 @@ 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 |
@@ -89,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) |
@@ -134,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: |
@@ -155,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'): |
@@ -168,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') %} |
@@ -184,7 +216,7 @@ 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 |
@@ -221,7 +253,7 @@ def do_sort(environment, value, reverse=False, case_sensitive=False, |
""" |
if not case_sensitive: |
def sort_func(item): |
- if isinstance(item, basestring): |
+ if isinstance(item, string_types): |
item = item.lower() |
return item |
else: |
@@ -250,7 +282,7 @@ 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 |
@@ -283,7 +315,7 @@ def do_join(eval_ctx, value, d=u'', attribute=None): |
# 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 |
@@ -294,11 +326,11 @@ def do_join(eval_ctx, value, d=u'', attribute=None): |
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 |
@@ -307,14 +339,14 @@ def do_join(eval_ctx, value, d=u'', attribute=None): |
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.') |
@@ -323,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.') |
@@ -346,25 +378,25 @@ def do_filesizeformat(value, binary=False): |
bytes = float(value) |
base = binary and 1024 or 1000 |
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") |
+ (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" |
+ return '1 Byte' |
elif bytes < base: |
- return "%d Bytes" % bytes |
+ return '%d Bytes' % bytes |
else: |
for i, prefix in enumerate(prefixes): |
- unit = base * base ** (i + 1) |
+ unit = base ** (i + 2) |
if bytes < unit: |
- return "%.1f %s" % ((bytes / unit), prefix) |
- return "%.1f %s" % ((bytes / unit), prefix) |
+ return '%.1f %s' % ((base * bytes / unit), prefix) |
+ return '%.1f %s' % ((base * bytes / unit), prefix) |
def do_pprint(value, verbose=False): |
@@ -417,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, '»') }} |
- 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 |
@@ -444,15 +477,23 @@ def do_truncate(s, length=255, killwords=False, end='...'): |
return u' '.join(result) |
@environmentfilter |
-def do_wordwrap(environment, s, width=79, break_long_words=True): |
+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 environment.newline_sequence.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)) |
@@ -513,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): |
@@ -541,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 |
@@ -557,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 |
@@ -666,7 +707,8 @@ 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))) |
@@ -707,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) |
@@ -752,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, |
@@ -776,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, |
@@ -790,6 +973,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, |
@@ -797,5 +982,6 @@ FILTERS = { |
'round': do_round, |
'groupby': do_groupby, |
'safe': do_mark_safe, |
- 'xmlattr': do_xmlattr |
+ 'xmlattr': do_xmlattr, |
+ 'urlencode': do_urlencode |
} |