DictLoader
from jinja2.runtime import Undefined, DebugUndefined, StrictUndefined
from jinja2.filters import environmentfilter, contextfilter
-from jinja2.utils import Markup, escape
+from jinja2.utils import Markup, escape, contextfunction
"""
from jinja2.filters import FILTERS as DEFAULT_FILTERS
from jinja2.tests import TESTS as DEFAULT_TESTS
+from jinja2.utils import generate_lorem_ipsum
DEFAULT_NAMESPACE = {
- 'range': xrange
+ 'range': xrange,
+ 'lipsum': generate_lorem_ipsum
}
def parse(self, parser):
lineno = parser.stream.next().lineno
args = [parser.parse_expression()]
- if self.stream.current.type is 'comma':
+ if parser.stream.current.type is 'comma':
+ parser.stream.next()
args.append(parser.parse_expression())
body = parser.parse_statements(('name:endcache',), drop_needle=True)
return nodes.CallBlock(
- nodes.Call(nodes.Name('cache_support'), args, [], None, None),
+ nodes.Call(nodes.Name('cache_support', 'load'), args, [], None, None),
[], [], body
)
* ``message`` is the string itself (a ``unicode`` object, or a tuple
of ``unicode`` objects for functions with multiple string arguments).
"""
- for call in node.find_all(nodes.Call):
+ for node in node.find_all(nodes.Call):
if not isinstance(node.node, nodes.Name) or \
node.node.name not in gettext_functions:
continue
for arg in node.args:
if isinstance(arg, nodes.Const) and \
isinstance(arg.value, basestring):
- strings.append(arg)
+ strings.append(arg.value)
else:
strings.append(None)
(comments will be empty currently)
"""
encoding = options.get('encoding', 'utf-8')
+ extensions = [x.strip() for x in options.get('extensions', '').split(',')]
environment = Environment(
options.get('block_start_string', '{%'),
options.get('block_end_string', '%}'),
options.get('comment_end_string', '#}'),
options.get('line_statement_prefix') or None,
options.get('trim_blocks', '').lower() in ('1', 'on', 'yes', 'true'),
- extensions=[x.strip() for x in options.get('extensions', '')
- .split(',')] + [TransExtension]
+ extensions=[x for x in extensions if x]
)
+
+ # add the i18n extension only if it's not yet in the list. Some people
+ # might use a script to sync the babel ini with the Jinja configuration
+ # so we want to avoid having the trans extension twice in the list.
+ for extension in environment.extensions:
+ if isinstance(extension, TransExtension):
+ break
+ else:
+ environment.extensions.append(TransExtension(environment))
+
node = environment.parse(fileobj.read().decode(encoding))
for lineno, func, message in extract_from_ast(node, keywords):
yield lineno, func, message, []
plural = plural.replace('%%', '%')
if not have_plural:
- if plural_expr is None:
- raise TemplateAssertionError('pluralize without variables',
- lineno, parser.filename)
plural_expr = None
+ elif plural_expr is None:
+ raise TemplateAssertionError('pluralize without variables',
+ lineno, parser.filename)
if variables:
variables = nodes.Dict([nodes.Pair(nodes.Const(x, lineno=lineno), y)
# set of used keywords
keywords = set(['and', 'block', 'elif', 'else', 'endblock', 'print',
'endfilter', 'endfor', 'endif', 'endmacro', 'endraw',
- 'extends', 'filter', 'for', 'if', 'in', 'include'
+ 'extends', 'filter', 'for', 'if', 'in', 'include',
'is', 'macro', 'not', 'or', 'raw', 'call', 'endcall'])
# bind operators to token types
('block', environment.block_start_string),
('variable', environment.variable_start_string)
]
- root_tag_rules.sort(key=lambda x: len(x[1]))
+ root_tag_rules.sort(key=lambda x: -len(x[1]))
# now escape the rules. This is done here so that the escape
# signs don't count for the lengths of the tags.
def tokenize(self, source, filename=None):
"""Works like `tokeniter` but returns a tokenstream of tokens and not
- a generator or token tuples. Additionally all token values are already
+ a generator or token tuples. Additionally all token values are already
converted into types and postprocessed. For example keywords are
already keyword tokens, not named tokens, comments are removed,
integers and floats converted, strings unescaped etc.
token = 'block_begin'
elif token == 'linestatement_end':
token = 'block_end'
+ # we are not interested in those tokens in the parser
+ elif token in ('raw_begin', 'raw_end'):
+ continue
elif token == 'data':
try:
value = str(value)
`get_nodes` used by the parser and translator in order to normalize
python and jinja nodes.
- :copyright: 2007 by Armin Ronacher.
+ :copyright: 2008 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
import operator
+from types import FunctionType
from itertools import chain, izip
from collections import deque
from copy import copy
'lt': operator.lt,
'lteq': operator.le,
'in': operator.contains,
- 'notin': lambda a, b: not operator.contains(a, b)
+ 'notin': lambda a, b: b not in a
}
def as_const(self):
obj = self.node.as_const()
+
+ # don't evaluate context functions
+ if type(obj) is FunctionType and \
+ getattr(obj, 'contextfunction', False):
+ raise Impossible()
+
args = [x.as_const() for x in self.args]
kwargs = dict(x.as_const() for x in self.kwargs)
if self.dyn_args is not None:
raise Impossible()
def can_assign(self):
- return True
+ return False
class Slice(Expr):
# - multiple Output() nodes should be concatenated into one node.
# for example the i18n system could output such nodes:
# "foo{% trans %}bar{% endtrans %}blah"
+# - when unrolling loops local sets become global sets :-/
+# see also failing test case `test_localset` in test_various
def optimize(node, environment, context_hint=None):
for item, loop in LoopContext(iterable, True):
context['loop'] = loop.make_static()
assign(node.target, item)
- result.extend(self.visit(n.copy(), context)
- for n in node.body)
+ for n in node.body:
+ result.extend(self.visit_list(n.copy(), context))
iterated = True
if not iterated and node.else_:
- result.extend(self.visit(n.copy(), context)
- for n in node.else_)
+ for n in node.else_:
+ result.extend(self.visit_list(n.copy(), context))
except nodes.Impossible:
return node
finally:
lineno = self.stream.expect('assign').lineno
if not target.can_assign():
raise TemplateSyntaxError("can't assign to '%s'" %
- target, target.lineno,
- self.filename)
+ target.__class__.__name__.lower(),
+ target.lineno, self.filename)
expr = self.parse_tuple()
target.set_ctx('store')
return nodes.Assign(target, expr, lineno=lineno)
target = self.parse_tuple(simplified=True)
if not target.can_assign():
raise TemplateSyntaxError("can't assign to '%s'" %
- target, target.lineno,
- self.filename)
+ target.__class__.__name__.lower(),
+ target.lineno, self.filename)
target.set_ctx('store')
self.stream.expect('in')
iter = self.parse_tuple(no_condexpr=True)
from collections import defaultdict
except ImportError:
defaultdict = None
-from jinja2.utils import Markup
+from types import FunctionType
+from jinja2.utils import Markup, partial
from jinja2.exceptions import UndefinedError
self.name = name
self.blocks = dict((k, [v]) for k, v in blocks.iteritems())
+ # give all context functions the context as first argument
+ for key, value in self.iteritems():
+ if type(value) is FunctionType and \
+ getattr(value, 'contextfunction', False):
+ dict.__setitem__(self, key, partial(value, self))
+
# if the template is in standalone mode we don't copy the blocks over.
# this is used for includes for example but otherwise, if the globals
# are a template context, this template is participating in a template
import string
from collections import deque
from copy import deepcopy
-from functools import update_wrapper
from itertools import imap
_simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$')
+def contextfunction(f):
+ """Mark a callable as context callable. A context callable is passed
+ the active context as first argument.
+ """
+ f.contextfunction = True
+ return f
+
+
def import_string(import_name, silent=False):
"""Imports an object based on a string. This use useful if you want to
use import paths as endpoints or something similar. An import path can
return u''.join(words)
+def generate_lorem_ipsum(n=5, html=True, min=20, max=100):
+ """Generate some lorem impsum for the template."""
+ from jinja2.constants import LOREM_IPSUM_WORDS
+ from random import choice, random, randrange
+ words = LOREM_IPSUM_WORDS.split()
+ result = []
+
+ for _ in xrange(n):
+ next_capitalized = True
+ last_comma = last_fullstop = 0
+ word = None
+ last = None
+ p = []
+
+ # each paragraph contains out of 20 to 100 words.
+ for idx, _ in enumerate(xrange(randrange(min, max))):
+ while True:
+ word = choice(words)
+ if word != last:
+ last = word
+ break
+ if next_capitalized:
+ word = word.capitalize()
+ next_capitalized = False
+ # add commas
+ if idx - randrange(3, 8) > last_comma:
+ last_comma = idx
+ last_fullstop += 2
+ word += ','
+ # add end of sentences
+ if idx - randrange(10, 20) > last_fullstop:
+ last_comma = last_fullstop = idx
+ word += '.'
+ next_capitalized = True
+ p.append(word)
+
+ # ensure that the paragraph ends with a dot.
+ p = u' '.join(p)
+ if p.endswith(','):
+ p = p[:-1] + '.'
+ elif not p.endswith('.'):
+ p += '.'
+ result.append(p)
+
+ if not html:
+ return u'\n\n'.join(result)
+ return Markup(u'\n'.join(u'<p>%s</p>' % escape(x) for x in result))
+
+
class Markup(unicode):
"""Marks a string as being safe for inclusion in HTML/XML output without
needing to be escaped. This implements the `__html__` interface a couple
if hasattr(arg, '__html__') or isinstance(arg, basestring):
kwargs[name] = escape(arg)
return self.__class__(orig(self, *args, **kwargs))
- return update_wrapper(func, orig, ('__name__', '__doc__'))
+ func.__name__ = orig.__name__
+ func.__doc__ = orig.__doc__
+ return func
for method in '__getitem__', '__getslice__', 'capitalize', \
'title', 'lower', 'upper', 'replace', 'ljust', \
'rjust', 'lstrip', 'rstrip', 'partition', 'center', \
if not isinstance(s, unicode):
s = unicode(s)
return s
+
+
+# partials
+try:
+ from functools import partial
+except ImportError:
+ class partial(object):
+ def __init__(self, _func, *args, **kwargs):
+ self._func = func
+ self._args = args
+ self._kwargs = kwargs
+ def __call__(self, *args, **kwargs):
+ kwargs.update(self._kwargs)
+ return self._func(*(self._args + args), **kwargs)
else:
setattr(node, field, new_node)
return node
+
+ def visit_list(self, node, *args, **kwargs):
+ """As transformers may return lists in some places this method
+ can be used to enforce a list as return value.
+ """
+ rv = self.visit(node, *args, **kwargs)
+ if not isinstance(rv, list):
+ rv = [rv]
+ return rv
:copyright: 2007 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
-from jinja2 import Environment, DictLoader
+from jinja2 import Environment, DictLoader, contextfunction
templates = {
'master.html': '<title>{{ page_title|default(_("missing")) }}</title>'
'{% block body %}{% endblock %}',
'child.html': '{% extends "master.html" %}{% block body %}'
- '{% trans "watch out" %}{% endblock %}',
+ '{% trans %}watch out{% endtrans %}{% endblock %}',
'plural.html': '{% trans user_count %}One user online{% pluralize %}'
'{{ user_count }} users online{% endtrans %}',
'stringformat.html': '{{ _("User: %d")|format(user_count) }}'
}
-class SimpleTranslator(object):
- """Yes i know it's only suitable for english and german but
- that's a stupid unittest..."""
+@contextfunction
+def gettext(context, string):
+ language = context.get('LANGUAGE', 'en')
+ return languages.get(language, {}).get(string, string)
- def __init__(self, language):
- self.strings = languages.get(language, {})
- def gettext(self, string):
- return self.strings.get(string, string)
+@contextfunction
+def ngettext(context, s, p, n):
+ language = context.get('LANGUAGE', 'en')
+ if n != 1:
+ return languages.get(language, {}).get(p, p)
+ return languages.get(language, {}).get(s, s)
- def ngettext(self, s, p, n):
- if n != 1:
- return self.strings.get(p, p)
- return self.strings.get(s, s)
-
-class I18NEnvironment(Environment):
-
- def __init__(self):
- super(I18NEnvironment, self).__init__(loader=DictLoader(templates))
-
- def get_translator(self, context):
- return SimpleTranslator(context['LANGUAGE'] or 'en')
-
-
-i18n_env = I18NEnvironment()
-
-
-def test_factory():
- def factory(context):
- return SimpleTranslator(context['LANGUAGE'] or 'en')
- env = Environment(translator_factory=factory)
- tmpl = env.from_string('{% trans "watch out" %}')
- assert tmpl.render(LANGUAGE='de') == 'pass auf'
-
-
-def test_get_translations():
- trans = list(i18n_env.get_translations('child.html'))
- assert len(trans) == 1
- assert trans[0] == (1, u'watch out', None)
-
-
-def test_get_translations_for_string():
- trans = list(i18n_env.get_translations('master.html'))
- assert len(trans) == 1
- assert trans[0] == (1, u'missing', None)
+i18n_env = Environment(
+ loader=DictLoader(templates),
+ extensions=['jinja2.i18n.TransExtension']
+)
+i18n_env.globals.update({
+ '_': gettext,
+ 'gettext': gettext,
+ 'ngettext': ngettext
+})
def test_trans():
def test_trans_stringformatting():
tmpl = i18n_env.get_template('stringformat.html')
assert tmpl.render(LANGUAGE='de', user_count=5) == 'Benutzer: 5'
+
+
+def test_extract():
+ from jinja2.i18n import babel_extract
+ from StringIO import StringIO
+ source = StringIO('''
+ {{ gettext('Hello World') }}
+ {% trans %}Hello World{% endtrans %}
+ {% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %}
+ ''')
+ assert list(babel_extract(source, ('gettext', 'ngettext', '_'), [], {})) == [
+ (2, 'gettext', 'Hello World', []),
+ (3, 'gettext', u'Hello World', []),
+ (4, 'ngettext', (u'%(users)s user', u'%(users)s users', None), [])
+ ]
:copyright: 2007 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
-
from jinja2 import Environment
-NO_VARIABLE_BLOCK = '''\
-{# i'm a freaking comment #}\
-{% if foo %}{% foo %}{% endif %}
-{% for item in seq %}{% item %}{% endfor %}
-{% trans foo %}foo is {% foo %}{% endtrans %}
-{% trans foo %}one foo{% pluralize %}{% foo %} foos{% endtrans %}'''
PHP_SYNTAX = '''\
<!-- I'm a comment, I'm not interesting -->\
${item}
<!--- endfor -->'''
-SMARTY_SYNTAX = '''\
-{* I'm a comment, I'm not interesting *}\
-{for item in seq-}
- {item}
-{-endfor}'''
+MAKO_SYNTAX = '''\
+% for item in seq:
+ ${item}
+% endfor'''
BALANCING = '''{{{'foo':'bar'}.foo}}'''
{{ blub() }}'''
-def test_no_variable_block():
- env = Environment('{%', '%}', None, None)
- tmpl = env.from_string(NO_VARIABLE_BLOCK)
- assert tmpl.render(foo=42, seq=range(2)).splitlines() == [
- '42',
- '01',
- 'foo is 42',
- '42 foos'
- ]
-
-
def test_php_syntax():
env = Environment('<?', '?>', '<?=', '?>', '<!--', '-->')
tmpl = env.from_string(PHP_SYNTAX)
assert tmpl.render(seq=range(5)) == '01234'
-def test_smarty_syntax():
- env = Environment('{', '}', '{', '}', '{*', '*}')
- tmpl = env.from_string(SMARTY_SYNTAX)
- assert tmpl.render(seq=range(5)) == '01234'
-
-
def test_balancing(env):
tmpl = env.from_string(BALANCING)
assert tmpl.render() == 'bar'
def test_start_comment(env):
tmpl = env.from_string(STARTCOMMENT)
assert tmpl.render().strip() == 'foo'
+
+
+def test_line_syntax():
+ env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%')
+ tmpl = env.from_string(MAKO_SYNTAX)
+ assert [int(x.strip()) for x in tmpl.render(seq=range(5)).split()] == \
+ range(5)
:copyright: 2007 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
-from jinja2 import Environment
+from jinja2.sandbox import SandboxedEnvironment, unsafe
-NONLOCALSET = '''\
-{% for item in range(10) %}
- {%- set outer = item! -%}
-{% endfor -%}
-{{ outer }}'''
+class PrivateStuff(object):
+ def bar(self):
+ return 23
-class PrivateStuff(object):
- bar = lambda self: 23
- foo = lambda self: 42
- foo.jinja_unsafe_call = True
+ @unsafe
+ def foo(self):
+ return 42
+
+ def __repr__(self):
+ return 'PrivateStuff'
class PublicStuff(object):
- jinja_allowed_attributes = ['bar']
bar = lambda self: 23
- foo = lambda self: 42
+ _foo = lambda self: 42
+
+ def __repr__(self):
+ return 'PublicStuff'
test_unsafe = '''
+>>> env = MODULE.SandboxedEnvironment()
>>> env.from_string("{{ foo.foo() }}").render(foo=MODULE.PrivateStuff())
-u''
+Traceback (most recent call last):
+ ...
+TypeError: <bound method PrivateStuff.foo of PrivateStuff> is not safely callable
>>> env.from_string("{{ foo.bar() }}").render(foo=MODULE.PrivateStuff())
u'23'
->>> env.from_string("{{ foo.foo() }}").render(foo=MODULE.PublicStuff())
-u''
+>>> env.from_string("{{ foo._foo() }}").render(foo=MODULE.PublicStuff())
+Traceback (most recent call last):
+ ...
+UndefinedError: access to attribute '_foo' of 'PublicStuff' object is unsafe.
>>> env.from_string("{{ foo.bar() }}").render(foo=MODULE.PublicStuff())
u'23'
>>> env.from_string("{{ foo.__class__ }}").render(foo=42)
u''
-
>>> env.from_string("{{ foo.func_code }}").render(foo=lambda:None)
u''
+>>> env.from_string("{{ foo.__class__.__subclasses__() }}").render(foo=42)
+Traceback (most recent call last):
+ ...
+UndefinedError: access to attribute '__class__' of 'int' object is unsafe.
'''
test_restricted = '''
+>>> env = MODULE.SandboxedEnvironment()
>>> env.from_string("{% for item.attribute in seq %}...{% endfor %}")
Traceback (most recent call last):
...
-TemplateSyntaxError: cannot assign to expression (line 1)
+TemplateSyntaxError: can't assign to 'subscript' (line 1)
>>> env.from_string("{% for foo, bar.baz in seq %}...{% endfor %}")
Traceback (most recent call last):
...
-TemplateSyntaxError: cannot assign to expression (line 1)
+TemplateSyntaxError: can't assign to 'tuple' (line 1)
'''
-
-
-def test_nonlocal_set():
- env = Environment()
- env.globals['outer'] = 42
- tmpl = env.from_string(NONLOCALSET)
- assert tmpl.render() == '9'
- assert env.globals['outer'] == 42
"""
from jinja2.exceptions import TemplateSyntaxError
-KEYWORDS = '''\
-{{ with }}
-{{ as }}
-{{ import }}
-{{ from }}
-{{ class }}
-{{ def }}
-{{ try }}
-{{ except }}
-{{ exec }}
-{{ global }}
-{{ assert }}
-{{ break }}
-{{ continue }}
-{{ lambda }}
-{{ return }}
-{{ raise }}
-{{ yield }}
-{{ while }}
-{{ pass }}
-{{ finally }}'''
+
UNPACKING = '''{% for a, b, c in [[1, 2, 3]] %}{{ a }}|{{ b }}|{{ c }}{% endfor %}'''
RAW = '''{% raw %}{{ FOO }} and {% BAR %}{% endraw %}'''
-CONST = '''{{ true }}|{{ false }}|{{ none }}|{{ undefined }}|\
-{{ none is defined }}|{{ undefined is defined }}'''
-LOCALSET = '''{% set foo = 0 %}\
-{% for item in [1, 2] %}{% set foo = 1 %}{% endfor %}\
-{{ foo }}'''
-NONLOCALSET = '''{% set foo = 0 %}\
-{% for item in [1, 2] %}{% set foo = 1! %}{% endfor %}\
+CONST = '''{{ true }}|{{ false }}|{{ none }}|\
+{{ none is defined }}|{{ missing is defined }}'''
+LOCALSET = '''{% foo = 0 %}\
+{% for item in [1, 2] %}{% foo = 1 %}{% endfor %}\
{{ foo }}'''
-CONSTASS1 = '''{% set true = 42 %}'''
-CONSTASS2 = '''{% for undefined in seq %}{% endfor %}'''
-
-
-def test_keywords(env):
- env.from_string(KEYWORDS)
+CONSTASS1 = '''{% true = 42 %}'''
+CONSTASS2 = '''{% for none in seq %}{% endfor %}'''
def test_unpacking(env):
assert tmpl.render() == '{{ FOO }} and {% BAR %}'
-def test_crazy_raw():
- from jinja2 import Environment
- env = Environment('{', '}', '{', '}')
- tmpl = env.from_string('{raw}{broken foo}{endraw}')
- assert tmpl.render() == '{broken foo}'
-
-
-def test_cache_dict():
- from jinja2.utils import CacheDict
- d = CacheDict(3)
+def test_lru_cache():
+ from jinja2.utils import LRUCache
+ d = LRUCache(3)
d["a"] = 1
d["b"] = 2
d["c"] = 3
assert 'a' in d and 'c' in d and 'd' in d and 'b' not in d
-def test_stringfilter(env):
- from jinja2.filters import stringfilter
- f = stringfilter(lambda f, x: f + x)
- assert f('42')(env, None, 23) == '2342'
-
-
-def test_simplefilter(env):
- from jinja2.filters import simplefilter
- f = simplefilter(lambda f, x: f + x)
- assert f(42)(env, None, 23) == 65
-
-
def test_const(env):
tmpl = env.from_string(CONST)
- assert tmpl.render() == 'True|False|||True|False'
+ assert tmpl.render() == 'True|False|None|True|False'
def test_const_assign(env):
def test_localset(env):
tmpl = env.from_string(LOCALSET)
assert tmpl.render() == '0'
-
-
-def test_nonlocalset(env):
- tmpl = env.from_string(NONLOCALSET)
- assert tmpl.render() == '1'