From 9bcd41185ea4f7e4ac8e415032cdd392f36e6f98 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 29 May 2007 14:17:24 +0200 Subject: [PATCH] [svn] again some jinja updates, some minor performance and doc improvements --HG-- branch : trunk --- CHANGES | 7 +- docs/src/builtins.txt | 9 +++ docs/src/designerdoc.txt | 127 +++++++++++++++++++----------------- docs/src/filters.txt | 12 ++++ jinja/environment.py | 20 +++--- jinja/filters.py | 45 +++++++++++-- jinja/translators/python.py | 12 ++-- jinja/utils.py | 32 ++++++--- tests/test_filters.py | 10 ++- tests/test_forloop.py | 6 ++ tests/test_macros.py | 31 +++++++++ tests/test_various.py | 20 ++++-- 12 files changed, 236 insertions(+), 95 deletions(-) diff --git a/CHANGES b/CHANGES index dd45ed0..381415c 100644 --- 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 ----------- diff --git a/docs/src/builtins.txt b/docs/src/builtins.txt index 7f460a4..09d9da8 100644 --- a/docs/src/builtins.txt +++ b/docs/src/builtins.txt @@ -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 ================ diff --git a/docs/src/designerdoc.txt b/docs/src/designerdoc.txt index eb872ce..d7d4f6b 100644 --- a/docs/src/designerdoc.txt +++ b/docs/src/designerdoc.txt @@ -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 %} +
+

{{ title }}

+
+ {{ caller() }} +
+
+ {% 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 %} + + {%- 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 }}

-
- {{ caller() }} -
-
- {% 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 %} - - {%- 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='

just a small \n ' - 'example link

\n

to a webpage

') + 'example link

\n

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 -- 2.26.2