From: Armin Ronacher just a small \n '
- 'example link to a webpage{{ title }}
+
+ {%- for item in items %}
+
+ {%- endmacro %}
+
+ {% call makelist([1, 2, 3, 4, 5, 6]) -%}
+ [[{{ item }}]]
+ {%- endcall %}
+
+This will then produce this output:
+
+.. sourcecode:: html
+
+
+
+
+
Template Inclusion
==================
@@ -374,6 +436,10 @@ template.
This is intended because it makes it possible to include macros from other
templates.
+*new in Jinja 1.1* you can now render an included template to a string that is
+evaluated in an indepdendent environment by calling `rendertemplate`. See the
+documentation for this function in the `builtins`_ documentation.
+
Filtering Blocks
================
@@ -442,65 +508,6 @@ alternative names:
{% endfor %}
-Bleeding Edge
-=============
-
-Here are some features documented that are new in the SVN version and might
-change.
-
-``{% call %}``:
-
- A new tag that allows to pass a macro a block with template data:
-
- .. sourcecode:: html+jinja
-
- {% macro dialog title %}
- {{ title }}
-
- {%- for item in items %}
-
- {%- endmacro %}
-
- {% call makelist([1, 2, 3, 4, 5, 6]) -%}
- [[{{ item }}]]
- {%- endcall %}
-
- This will then produce this output:
-
- .. sourcecode:: html
-
-
-
-
.. _slicing chapter: http://diveintopython.org/native_data_types/lists.html#odbchelper.list.slice
.. _Scopes and Variable Behavior: scopes.txt
+.. _builtins: builtins.txt
diff --git a/docs/src/filters.txt b/docs/src/filters.txt
index b21781d..df23205 100644
--- a/docs/src/filters.txt
+++ b/docs/src/filters.txt
@@ -57,5 +57,17 @@ the value already converted into a string.
If you're using Jinja with django and want to use the django filters in Jinja
have a look at the `developer recipies`_ page.
+*new in Jinja 1.1* additionally to the `stringfilter` decorator there is now
+a similar decorator that works exactly the same but does not convert values
+to unicode:
+
+.. sourcecode:: python
+
+ from jinja.filters import simplefilter
+
+ @simplefilter
+ def do_add(value, to_add):
+ return value + to_add
+
.. _designer documentation: builtins.txt
.. _developer recipies: devrecipies.txt
diff --git a/jinja/environment.py b/jinja/environment.py
index c4a01ef..011e12a 100644
--- a/jinja/environment.py
+++ b/jinja/environment.py
@@ -22,6 +22,10 @@ from jinja.defaults import DEFAULT_FILTERS, DEFAULT_TESTS, DEFAULT_NAMESPACE
__all__ = ['Environment']
+#: minor speedup
+_getattr = getattr
+
+
class Environment(object):
"""
The Jinja environment.
@@ -293,7 +297,7 @@ class Environment(object):
except (AttributeError, SecurityException):
pass
if obj is self.undefined_singleton:
- return getattr(self.undefined_singleton, name)
+ return _getattr(obj, name)
return self.undefined_singleton
def get_attributes(self, obj, attributes):
@@ -315,10 +319,10 @@ class Environment(object):
args += tuple(dyn_args)
if dyn_kwargs is not None:
kwargs.update(dyn_kwargs)
- if getattr(f, 'jinja_unsafe_call', False) or \
- getattr(f, 'alters_data', False):
+ if _getattr(f, 'jinja_unsafe_call', False) or \
+ _getattr(f, 'alters_data', False):
return self.undefined_singleton
- if getattr(f, 'jinja_context_callable', False):
+ if _getattr(f, 'jinja_context_callable', False):
args = (self, context) + args
return f(*args, **kwargs)
@@ -327,10 +331,10 @@ class Environment(object):
Function call without arguments. Because of the smaller signature and
fewer logic here we have a bit of redundant code.
"""
- if getattr(f, 'jinja_unsafe_call', False) or \
- getattr(f, 'alters_data', False):
+ if _getattr(f, 'jinja_unsafe_call', False) or \
+ _getattr(f, 'alters_data', False):
return self.undefined_singleton
- if getattr(f, 'jinja_context_callable', False):
+ if _getattr(f, 'jinja_context_callable', False):
return f(self, context)
return f()
@@ -344,7 +348,7 @@ class Environment(object):
return u''
elif value is self.undefined_singleton:
return unicode(value)
- elif getattr(value, 'jinja_no_finalization', False):
+ elif _getattr(value, 'jinja_no_finalization', False):
return value
val = self.to_unicode(value)
if self.default_filters:
diff --git a/jinja/filters.py b/jinja/filters.py
index 1d45fc4..4cd360f 100644
--- a/jinja/filters.py
+++ b/jinja/filters.py
@@ -11,11 +11,14 @@
import re
from random import choice
from urllib import urlencode, quote
-from jinja.utils import urlize, escape, reversed
+from jinja.utils import urlize, escape, reversed, sorted
from jinja.datastructure import TemplateData
from jinja.exceptions import FilterArgumentError
+_striptags_re = re.compile(r'(|<[^>]+>)')
+
+
def stringfilter(f):
"""
Decorator for filters that just work on unicode objects.
@@ -36,6 +39,25 @@ def stringfilter(f):
return decorator
+def simplefilter(f):
+ """
+ Decorator for simplifying filters. Filter arguments are passed
+ to the decorated function without environment and context. The
+ source value is the first argument. (like stringfilter but
+ without unicode conversion)
+ """
+ def decorator(*args):
+ def wrapped(env, context, value):
+ return f(value, *args)
+ return wrapped
+ try:
+ decorator.__doc__ = f.__doc__
+ decorator.__name__ = f.__name__
+ except:
+ pass
+ return decorator
+
+
def do_replace(s, old, new, count=None):
"""
Return a copy of the value with all occurrences of a substring
@@ -128,7 +150,7 @@ def do_xmlattr(autospace=False):
...
- As you can see it automatically appends a space in front of the item
+ As you can see it automatically prepends a space in front of the item
if the filter returned something. You can disable this by passing
`false` as only argument to the filter.
@@ -668,13 +690,13 @@ def do_capture(name='captured', clean=False):
return wrapped
-def do_striptags(value, rex=re.compile(r'<[^>]+>')):
+def do_striptags(value):
"""
Strip SGML/XML tags and replace adjacent whitespace by one space.
*new in Jinja 1.1*
"""
- return ' '.join(rex.sub('', value).split())
+ return ' '.join(_striptags_re.sub('', value).split())
do_striptags = stringfilter(do_striptags)
@@ -818,6 +840,18 @@ def do_round(precision=0, method='common'):
return wrapped
+def do_sort(reverse=False):
+ """
+ Sort a sequence. Per default it sorts ascending, if you pass it
+ `True` as first argument it will reverse the sorting.
+
+ *new in Jinja 1.1*
+ """
+ def wrapped(env, context, value):
+ return sorted(value, reverse=reverse)
+ return wrapped
+
+
FILTERS = {
'replace': do_replace,
'upper': do_upper,
@@ -863,5 +897,6 @@ FILTERS = {
'batch': do_batch,
'sum': do_sum,
'abs': do_abs,
- 'round': do_round
+ 'round': do_round,
+ 'sort': do_sort
}
diff --git a/jinja/translators/python.py b/jinja/translators/python.py
index a26299a..ddd9e54 100644
--- a/jinja/translators/python.py
+++ b/jinja/translators/python.py
@@ -224,17 +224,17 @@ class PythonTranslator(Translator):
#: mapping of unsupported syntax elements.
#: the value represents the feature name that appears
#: in the exception.
- self.unsupported = {
- ast.ListComp: 'list comprehensions'
- }
+ self.unsupported = {ast.ListComp: 'list comprehension'}
#: because of python2.3 compatibility add generator
#: expressions only to the list of unused features
#: if it exists.
if hasattr(ast, 'GenExpr'):
- self.unsupported.update({
- ast.GenExpr: 'generator expressions'
- })
+ self.unsupported[ast.GenExpr] = 'generator expression'
+
+ #: if expressions are unsupported too (so far)
+ if hasattr(ast, 'IfExp'):
+ self.unsupported[ast.IfExp] = 'inline if expression'
# -- public methods
diff --git a/jinja/utils.py b/jinja/utils.py
index 4102115..1359226 100644
--- a/jinja/utils.py
+++ b/jinja/utils.py
@@ -39,7 +39,7 @@ except (ImportError, AttributeError):
def clear(self):
del self[:]
-# support for a working reversed()
+# support for a working reversed() in 2.3
try:
reversed = reversed
except NameError:
@@ -51,12 +51,25 @@ except NameError:
except TypeError:
return iter(tuple(iterable)[::-1])
-# support for python 2.3/2.4
+# set support for python 2.3
try:
set = set
except NameError:
from sets import Set as set
+# sorted support (just a simplified version)
+try:
+ sorted = sorted
+except NameError:
+ def sorted(seq, reverse=False):
+ rv = list(seq)
+ rv.sort(reverse=reverse)
+ return rv
+
+#: function types
+callable_types = (FunctionType, MethodType)
+
+
#: number of maximal range items
MAX_RANGE = 1000000
@@ -141,6 +154,9 @@ def from_string(source):
return _from_string_env.from_string(source)
+#: minor speedup
+_getattr = getattr
+
def get_attribute(obj, name):
"""
Return the attribute from name. Raise either `AttributeError`
@@ -148,15 +164,15 @@ def get_attribute(obj, name):
"""
if not isinstance(name, basestring):
raise AttributeError(name)
- if name[:2] == name[-2:] == '__' or name[:2] == '::':
+ if name[:2] == name[-2:] == '__':
raise SecurityException('not allowed to access internal attributes')
- if (obj.__class__ is FunctionType and name.startswith('func_') or
- obj.__class__ is MethodType and name.startswith('im_')):
+ if obj.__class__ in callable_types and name.startswith('func_') or \
+ name.startswith('im_'):
raise SecurityException('not allowed to access function attributes')
- r = getattr(obj, 'jinja_allowed_attributes', None)
+ r = _getattr(obj, 'jinja_allowed_attributes', None)
if r is not None and name not in r:
- raise SecurityException('not allowed attribute accessed')
- return getattr(obj, name)
+ raise SecurityException('disallowed attribute accessed')
+ return _getattr(obj, name)
def safe_range(start, stop=None, step=None):
diff --git a/tests/test_filters.py b/tests/test_filters.py
index ba3bfba..2755224 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -61,6 +61,8 @@ ROUND = '''{{ 2.7|round }}|{{ 2.1|round }}|\
{{ 2.1234|round(2, 'floor') }}|{{ 2.1|round(0, 'ceil') }}'''
XMLATTR = '''{{ {'foo': 42, 'bar': 23, 'fish': none,
'spam': missing, 'blub:blub': '>'}|xmlattr }}'''
+SORT = '''{{ [2, 3, 1]|sort }}|{{ [2, 3, 1]|sort(true) }}'''
+
def test_capitalize(env):
@@ -114,7 +116,8 @@ def test_escape(env):
def test_striptags(env):
tmpl = env.from_string(STRIPTAGS)
out = tmpl.render(foo='
to a webpage
' + '') assert out == 'just a small example link to a webpage' @@ -274,3 +277,8 @@ def test_xmlattr(env): assert 'foo="42"' in out assert 'bar="23"' in out assert 'blub:blub="<?>"' in out + + +def test_sort(env): + tmpl = env.from_string(SORT) + assert tmpl.render() == '[1, 2, 3]|[3, 2, 1]' diff --git a/tests/test_forloop.py b/tests/test_forloop.py index ca50020..ed1cc2e 100644 --- a/tests/test_forloop.py +++ b/tests/test_forloop.py @@ -18,6 +18,7 @@ CYCLING = '''{% for item in seq %}{% cycle '<1>', '<2>' %}{% endfor %}\ {% for item in seq %}{% cycle through %}{% endfor %}''' SCOPE = '''{% for item in seq %}{% endfor %}{{ item }}''' VARLEN = '''{% for item in iter %}{{ item }}{% endfor %}''' +NONITER = '''{% for item in none %}...{% endfor %}''' def test_simple(env): @@ -73,3 +74,8 @@ def test_varlen(env): tmpl = env.from_string(VARLEN) output = tmpl.render(iter=inner()) assert output == '01234' + + +def test_noniter(env): + tmpl = env.from_string(NONITER) + assert not tmpl.render() diff --git a/tests/test_macros.py b/tests/test_macros.py index 3059384..cf84f95 100644 --- a/tests/test_macros.py +++ b/tests/test_macros.py @@ -39,6 +39,22 @@ VARARGS = '''\ {{ test(1, 2, 3) }}\ ''' +SIMPLECALL = '''\ +{% macro test %}[[{{ caller() }}]]{% endmacro %}\ +{% call test() %}data{% endcall %}\ +''' + +COMPLEXCALL = '''\ +{% macro test %}[[{{ caller(data='data') }}]]{% endmacro %}\ +{% call test() %}{{ data }}{% endcall %}\ +''' + +CALLERUNDEFINED = '''\ +{% set caller = 42 %}\ +{% macro test() %}{{ caller is not defined }}{% endmacro %}\ +{{ test() }}\ +''' + def test_simple(env): tmpl = env.from_string(SIMPLE) @@ -74,3 +90,18 @@ def test_parentheses(env): def test_varargs(env): tmpl = env.from_string(VARARGS) assert tmpl.render() == '1|2|3' + + +def test_simple_call(env): + tmpl = env.from_string(SIMPLECALL) + assert tmpl.render() == '[[data]]' + + +def test_complex_call(env): + tmpl = env.from_string(COMPLEXCALL) + assert tmpl.render() == '[[data]]' + + +def test_caller_undefined(env): + tmpl = env.from_string(CALLERUNDEFINED) + assert tmpl.render() == 'True' diff --git a/tests/test_various.py b/tests/test_various.py index df08bd4..62b516c 100644 --- a/tests/test_various.py +++ b/tests/test_various.py @@ -7,7 +7,7 @@ :license: BSD, see LICENSE for more details. """ -KEYWORDS = ''' +KEYWORDS = '''\ {{ with }} {{ as }} {{ import }} @@ -27,13 +27,9 @@ KEYWORDS = ''' {{ yield }} {{ while }} {{ pass }} -{{ finally }} -''' - +{{ finally }}''' UNPACKING = '''{% for a, b, c in [[1, 2, 3]] %}{{ a }}|{{ b }}|{{ c }}{% endfor %}''' - RAW = '''{% raw %}{{ FOO }} and {% BAR %}{% endraw %}''' - CALL = '''{{ foo('a', c='d', e='f', *['b'], **{'g': 'h'}) }}''' @@ -69,3 +65,15 @@ def test_call(): env.globals['foo'] = lambda a, b, c, e, g: a + b + c + e + g tmpl = env.from_string(CALL) assert tmpl.render() == 'abdfh' + + +def test_stringfilter(env): + from jinja.filters import stringfilter + f = stringfilter(lambda f, x: f + x) + assert f('42')(env, None, 23) == '2342' + + +def test_simplefilter(env): + from jinja.filters import simplefilter + f = simplefilter(lambda f, x: f + x) + assert f(42)(env, None, 23) == 65