[svn] again some jinja updates, some minor performance and doc improvements
authorArmin Ronacher <armin.ronacher@active-4.com>
Tue, 29 May 2007 12:17:24 +0000 (14:17 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Tue, 29 May 2007 12:17:24 +0000 (14:17 +0200)
--HG--
branch : trunk

12 files changed:
CHANGES
docs/src/builtins.txt
docs/src/designerdoc.txt
docs/src/filters.txt
jinja/environment.py
jinja/filters.py
jinja/translators/python.py
jinja/utils.py
tests/test_filters.py
tests/test_forloop.py
tests/test_macros.py
tests/test_various.py

diff --git a/CHANGES b/CHANGES
index dd45ed0b6a31d274ea479f19ed883a555b657fd4..381415cc9f3332f9c265c2a8e4233de0b2cffc0a 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -55,7 +55,7 @@ Version 1.1
 
 - added `batch` and `slice` filters for batching or slicing sequences
 
-- added `sum`, `abs` and `round` filters. This fixes #238
+- added `sum`, `abs`, `round` and `sort` filters. This fixes #238
 
 - added `striptags` and `xmlattr` filters for easier SGML/XML processing
 
@@ -103,6 +103,11 @@ Version 1.1
   If the iterator is inifite it will crash however, so makes sure you don't
   pass something like that to a template!
 
+- added `rendetemplate` to render included templates in an isolated
+  environment and get the outout back.
+
+- added `simplefilter` decorator.
+
 
 Version 1.0
 -----------
index 7f460a4e02af7e05bfa36e7cdb2b94ed7ab740c7..09d9da8ad1d107ad2edec3e69e31b0380051467d 100644 (file)
@@ -174,6 +174,15 @@ available per default:
 
     *new in Jinja 1.1*
 
+`rendertemplate`
+
+    Loads and renders a template with a copy of the current context. This works
+    in many situations like the ``{% include %}`` tag, just that it does not
+    include a template and merges it into the template structure but renders
+    it completely independent and returns the rendered data as string.
+
+    *new in Jinja 1.1*
+
 
 Global Constants
 ================
index eb872ce7ef2ad36455f3fedbdf0669ee0266818b..d7d4f6b417b13548ad3ddf786ab132e9a32852b7 100644 (file)
@@ -351,6 +351,68 @@ You can also specify more than one value:
 For information regarding the visibility of macros have a look at the
 `Scopes and Variable Behavior`_ section.
 
+Extended Macro Call
+===================
+
+*new in Jinja 1.1*
+
+Jinja 1.1 adds a new special tag that you can use to pass some evaluable
+template code to a macro. Here an example macro that uses the features of
+the ``{% call %}`` tag:
+
+.. sourcecode:: html+jinja
+
+    {% macro dialog title %}
+      <div class="dialog">
+        <h3>{{ title }}</h3>
+        <div class="text">
+          {{ caller() }}
+        </div>
+      </div>
+    {% endmacro %}
+
+Called the normal way `caller` will be undefined, but if you call it
+using the new `{% call %}` tag you can pass it some data:
+
+.. sourcecode:: html+jinja
+
+    {% call dialog('Hello World') %}
+        This is an example dialog
+    {% endcall %}
+
+Now the data wrapped will be inserted where you put the `caller` call.
+
+If you pass `caller()` some keyword arguments those are added to the
+namespace of the wrapped template data:
+
+.. sourcecode:: html+jinja
+
+    {% macro makelist items %}
+      <ul>
+      {%- for item in items %}
+        <li>{{ caller(item=item) }}</li>
+      {%- endfor %}
+      </ul>
+    {%- endmacro %}
+
+    {% call makelist([1, 2, 3, 4, 5, 6]) -%}
+      [[{{ item }}]]
+    {%- endcall %}
+
+This will then produce this output:
+
+.. sourcecode:: html
+
+    <ul>
+      <li>[[1]]</li>
+      <li>[[2]]</li>
+      <li>[[3]]</li>
+      <li>[[4]]</li>
+      <li>[[5]]</li>
+      <li>[[6]]</li>
+    </ul>
+
+
 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 %}
-          <div class="dialog">
-            <h3>{{ title }}</h3>
-            <div class="text">
-              {{ caller() }}
-            </div>
-          </div>
-        {% endmacro %}
-
-    Called the normal way `caller` will be undefined, but if you call it
-    using the new `{% call %}` tag you can pass it some data:
-
-    .. sourcecode:: html+jinja
-
-        {% call dialog('Hello World') %}
-            This is an example dialog
-        {% endcall %}
-
-    If you pass `caller()` some keyword arguments those are added to the
-    namespace of the wrapped template data:
-
-    .. sourcecode:: html+jinja
-
-        {% macro makelist items %}
-          <ul>
-          {%- for item in items %}
-            <li>{{ caller(item=item) }}</li>
-          {%- endfor %}
-          </ul>
-        {%- endmacro %}
-
-        {% call makelist([1, 2, 3, 4, 5, 6]) -%}
-          [[{{ item }}]]
-        {%- endcall %}
-
-    This will then produce this output:
-
-    .. sourcecode:: html
-
-        <ul>
-          <li>[[1]]</li>
-          <li>[[2]]</li>
-          <li>[[3]]</li>
-          <li>[[4]]</li>
-          <li>[[5]]</li>
-          <li>[[6]]</li>
-        </ul>
-
 .. _slicing chapter: http://diveintopython.org/native_data_types/lists.html#odbchelper.list.slice
 .. _Scopes and Variable Behavior: scopes.txt
+.. _builtins: builtins.txt
index b21781de03d0d6d560435a7bedcc086b71dfa85f..df2320554dd8dfe28a522af40f75866fb0e2ad17 100644 (file)
@@ -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
index c4a01efd1d7163934ced3d2239d96c37be9cf295..011e12a39366683f542ea8a8254a43edb39619e8 100644 (file)
@@ -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:
index 1d45fc489b344a8809c61f6eef586b729f08757e..4cd360ff33faee12315c9e77f0c35081a6a39c29 100644 (file)
 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):
         ...
         </ul>
 
-    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
 }
index a26299a8f51aa3f2900c6a9ee76a7e81cab5c343..ddd9e5441c8d416b00592c15530fbf88b888d458 100644 (file)
@@ -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
 
index 410211560004dd2f03f45a1531c25313e6fef1ee..135922677e519625bdee2336e2b3f91c03b1dd4a 100644 (file)
@@ -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):
index ba3bfbadb333d5b9003a1b41137cebee0db0cd66..2755224ab2afe59a05f74c3b92c492f41f9b5399 100644 (file)
@@ -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='  <p>just a small   \n <a href="#">'
-                      'example</a> link</p>\n<p>to a webpage</p>')
+                      'example</a> link</p>\n<p>to a webpage</p> '
+                      '<!-- <p>and some commented stuff</p> -->')
     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="&lt;?&gt;"' in out
+
+
+def test_sort(env):
+    tmpl = env.from_string(SORT)
+    assert tmpl.render() == '[1, 2, 3]|[3, 2, 1]'
index ca5002061f1a1f784422de414d4dc13e04babee7..ed1cc2ed0820b9a6698e68b6dad4ec0ea7933c6a 100644 (file)
@@ -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()
index 3059384fcb5f30a55839ca789f417251aaf59567..cf84f95163bc79c6fcf78d52c84eac020307b73e 100644 (file)
@@ -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'
index df08bd41a450f69f39db39ab9f788389c06a0a1e..62b516c9e640a89f6de1745e536829daf2a1c0bb 100644 (file)
@@ -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