Update to the latest version of jinja2 (#6790)

This commit is contained in:
Adam J. Stewart 2018-01-10 10:06:04 -06:00 committed by GitHub
parent 4fdf08e51c
commit d17a10c6ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 728 additions and 365 deletions

View File

@ -1,33 +0,0 @@
Jinja is written and maintained by the Jinja Team and various
contributors:
Lead Developer:
- Armin Ronacher <armin.ronacher@active-4.com>
Developers:
- Christoph Hack
- Georg Brandl
Contributors:
- Bryan McLemore
- Mickaël Guérin <kael@crocobox.org>
- Cameron Knight
- Lawrence Journal-World.
- David Cramer
Patches and suggestions:
- Ronny Pfannschmidt
- Axel Böhm
- Alexey Melchakov
- Bryan McLemore
- Clovis Fabricio (nosklo)
- Cameron Knight
- Peter van Dijk (Habbie)
- Stefan Ebner
- Rene Leonhardt
- Thomas Waldmann
- Cory Benfield (Lukasa)

View File

@ -1,31 +0,0 @@
Copyright (c) 2009 by the Jinja Team, see AUTHORS for more details.
Some rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,51 +0,0 @@
Jinja2
~~~~~~
Jinja2 is a template engine written in pure Python. It provides a
`Django`_ inspired non-XML syntax but supports inline expressions and
an optional `sandboxed`_ environment.
Nutshell
--------
Here a small example of a Jinja template:
.. code-block:: jinja
{% extends 'base.html' %}
{% block title %}Memberlist{% endblock %}
{% block content %}
<ul>
{% for user in users %}
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
{% endfor %}
</ul>
{% endblock %}
Philosophy
----------
Application logic is for the controller, but don't make the template designer's
life difficult by restricting functionality too much.
For more information visit the new `Jinja2 webpage`_ and `documentation`_.
The `Jinja2 tip`_ is installable via ``pip`` with ``pip install
https://github.com/pallets/jinja/zipball/master``.
.. _sandboxed: http://en.wikipedia.org/wiki/Sandbox_(computer_security)
.. _Django: http://www.djangoproject.com/
.. _Jinja2 webpage: http://jinja.pocoo.org/
.. _documentation: http://jinja.pocoo.org/docs/
.. _Jinja2 tip: http://jinja.pocoo.org/docs/intro/#as-a-python-egg-via-easy-install
Builds
------
+---------------------+------------------------------------------------------------------------------+
| ``master`` | .. image:: https://travis-ci.org/pallets/jinja.svg?branch=master |
| | :target: https://travis-ci.org/pallets/jinja |
+---------------------+------------------------------------------------------------------------------+
| ``2.9-maintenance`` | .. image:: https://travis-ci.org/pallets/jinja.svg?branch=2.9-maintenance |
| | :target: https://travis-ci.org/pallets/jinja |
+---------------------+------------------------------------------------------------------------------+

View File

@ -27,7 +27,7 @@
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__version__ = '2.9.6' __version__ = '2.10'
# high level interface # high level interface
from jinja2.environment import Environment, Template from jinja2.environment import Environment, Template
@ -48,7 +48,7 @@
# exceptions # exceptions
from jinja2.exceptions import TemplateError, UndefinedError, \ from jinja2.exceptions import TemplateError, UndefinedError, \
TemplateNotFound, TemplatesNotFound, TemplateSyntaxError, \ TemplateNotFound, TemplatesNotFound, TemplateSyntaxError, \
TemplateAssertionError TemplateAssertionError, TemplateRuntimeError
# decorators and public utilities # decorators and public utilities
from jinja2.filters import environmentfilter, contextfilter, \ from jinja2.filters import environmentfilter, contextfilter, \
@ -64,6 +64,7 @@
'MemcachedBytecodeCache', 'Undefined', 'DebugUndefined', 'MemcachedBytecodeCache', 'Undefined', 'DebugUndefined',
'StrictUndefined', 'TemplateError', 'UndefinedError', 'TemplateNotFound', 'StrictUndefined', 'TemplateError', 'UndefinedError', 'TemplateNotFound',
'TemplatesNotFound', 'TemplateSyntaxError', 'TemplateAssertionError', 'TemplatesNotFound', 'TemplateSyntaxError', 'TemplateAssertionError',
'TemplateRuntimeError',
'ModuleLoader', 'environmentfilter', 'contextfilter', 'Markup', 'escape', 'ModuleLoader', 'environmentfilter', 'contextfilter', 'Markup', 'escape',
'environmentfunction', 'contextfunction', 'clear_caches', 'is_undefined', 'environmentfunction', 'contextfunction', 'clear_caches', 'is_undefined',
'evalcontextfilter', 'evalcontextfunction', 'make_logging_undefined', 'evalcontextfilter', 'evalcontextfunction', 'make_logging_undefined',

View File

@ -0,0 +1,2 @@
# generated by scripts/generate_identifier_pattern.py
pattern = '·̀-ͯ·҃-֑҇-ׇֽֿׁׂׅׄؐ-ًؚ-ٰٟۖ-ۜ۟-۪ۤۧۨ-ܑۭܰ-݊ަ-ް߫-߳ࠖ-࠙ࠛ-ࠣࠥ-ࠧࠩ-࡙࠭-࡛ࣔ-ࣣ࣡-ःऺ-़ा-ॏ॑-ॗॢॣঁ-ঃ়া-ৄেৈো-্ৗৢৣਁ-ਃ਼ਾ-ੂੇੈੋ-੍ੑੰੱੵઁ-ઃ઼ા-ૅે-ૉો-્ૢૣଁ-ଃ଼ା-ୄେୈୋ-୍ୖୗୢୣஂா-ூெ-ைொ-்ௗఀ-ఃా-ౄె-ైొ-్ౕౖౢౣಁ-ಃ಼ಾ-ೄೆ-ೈೊ-್ೕೖೢೣഁ-ഃാ-ൄെ-ൈൊ-്ൗൢൣංඃ්ා-ුූෘ-ෟෲෳัิ-ฺ็-๎ັິ-ູົຼ່-ໍ༹༘༙༵༷༾༿ཱ-྄྆྇ྍ-ྗྙ-ྼ࿆ါ-ှၖ-ၙၞ-ၠၢ-ၤၧ-ၭၱ-ၴႂ-ႍႏႚ-ႝ፝-፟ᜒ-᜔ᜲ-᜴ᝒᝓᝲᝳ឴-៓៝᠋-᠍ᢅᢆᢩᤠ-ᤫᤰ-᤻ᨗ-ᨛᩕ-ᩞ᩠-᩿᩼᪰-᪽ᬀ-ᬄ᬴-᭄᭫-᭳ᮀ-ᮂᮡ-ᮭ᯦-᯳ᰤ-᰷᳐-᳔᳒-᳨᳭ᳲ-᳴᳸᳹᷀-᷵᷻-᷿‿⁀⁔⃐-⃥⃜⃡-⃰℘℮⳯-⵿⳱ⷠ-〪ⷿ-゙゚〯꙯ꙴ-꙽ꚞꚟ꛰꛱ꠂ꠆ꠋꠣ-ꠧꢀꢁꢴ-ꣅ꣠-꣱ꤦ-꤭ꥇ-꥓ꦀ-ꦃ꦳-꧀ꧥꨩ-ꨶꩃꩌꩍꩻ-ꩽꪰꪲ-ꪴꪷꪸꪾ꪿꫁ꫫ-ꫯꫵ꫶ꯣ-ꯪ꯬꯭ﬞ︀-️︠-︯︳︴﹍-﹏_𐇽𐋠𐍶-𐍺𐨁-𐨃𐨅𐨆𐨌-𐨏𐨸-𐨿𐨺𐫦𐫥𑀀-𑀂𑀸-𑁆𑁿-𑂂𑂰-𑂺𑄀-𑄂𑄧-𑅳𑄴𑆀-𑆂𑆳-𑇊𑇀-𑇌𑈬-𑈷𑈾𑋟-𑋪𑌀-𑌃𑌼𑌾-𑍄𑍇𑍈𑍋-𑍍𑍗𑍢𑍣𑍦-𑍬𑍰-𑍴𑐵-𑑆𑒰-𑓃𑖯-𑖵𑖸-𑗀𑗜𑗝𑘰-𑙀𑚫-𑚷𑜝-𑜫𑰯-𑰶𑰸-𑰿𑲒-𑲧𑲩-𑲶𖫰-𖫴𖬰-𖬶𖽑-𖽾𖾏-𖾒𛲝𛲞𝅥-𝅩𝅭-𝅲𝅻-𝆂𝆅-𝆋𝆪-𝆭𝉂-𝉄𝨀-𝨶𝨻-𝩬𝩵𝪄𝪛-𝪟𝪡-𝪯𞀀-𞀆𞀈-𞀘𞀛-𞀡𞀣𞀤𞀦-𞣐𞀪-𞣖𞥄-𞥊󠄀-󠇯'

File diff suppressed because one or more lines are too long

View File

@ -189,9 +189,9 @@ async def auto_aiter(iterable):
class AsyncLoopContext(LoopContextBase): class AsyncLoopContext(LoopContextBase):
def __init__(self, async_iterator, after, length, recurse=None, def __init__(self, async_iterator, undefined, after, length, recurse=None,
depth0=0): depth0=0):
LoopContextBase.__init__(self, recurse, depth0) LoopContextBase.__init__(self, undefined, recurse, depth0)
self._async_iterator = async_iterator self._async_iterator = async_iterator
self._after = after self._after = after
self._length = length self._length = length
@ -221,15 +221,16 @@ async def __anext__(self):
ctx.index0 += 1 ctx.index0 += 1
if ctx._after is _last_iteration: if ctx._after is _last_iteration:
raise StopAsyncIteration() raise StopAsyncIteration()
next_elem = ctx._after ctx._before = ctx._current
ctx._current = ctx._after
try: try:
ctx._after = await ctx._async_iterator.__anext__() ctx._after = await ctx._async_iterator.__anext__()
except StopAsyncIteration: except StopAsyncIteration:
ctx._after = _last_iteration ctx._after = _last_iteration
return next_elem, ctx return ctx._current, ctx
async def make_async_loop_context(iterable, recurse=None, depth0=0): async def make_async_loop_context(iterable, undefined, recurse=None, depth0=0):
# Length is more complicated and less efficient in async mode. The # Length is more complicated and less efficient in async mode. The
# reason for this is that we cannot know if length will be used # reason for this is that we cannot know if length will be used
# upfront but because length is a property we cannot lazily execute it # upfront but because length is a property we cannot lazily execute it
@ -251,4 +252,5 @@ async def make_async_loop_context(iterable, recurse=None, depth0=0):
after = await async_iterator.__anext__() after = await async_iterator.__anext__()
except StopAsyncIteration: except StopAsyncIteration:
after = _last_iteration after = _last_iteration
return AsyncLoopContext(async_iterator, after, length, recurse, depth0) return AsyncLoopContext(async_iterator, undefined, after, length, recurse,
depth0)

View File

@ -297,7 +297,7 @@ class MemcachedBytecodeCache(BytecodeCache):
Libraries compatible with this class: Libraries compatible with this class:
- `werkzeug <http://werkzeug.pocoo.org/>`_.contrib.cache - `werkzeug <http://werkzeug.pocoo.org/>`_.contrib.cache
- `python-memcached <http://www.tummy.com/Community/software/python-memcached/>`_ - `python-memcached <https://www.tummy.com/Community/software/python-memcached/>`_
- `cmemcache <http://gijsbert.org/cmemcache/>`_ - `cmemcache <http://gijsbert.org/cmemcache/>`_
(Unfortunately the django cache interface is not compatible because it (Unfortunately the django cache interface is not compatible because it

View File

@ -130,9 +130,10 @@ def __init__(self, node):
class Frame(object): class Frame(object):
"""Holds compile time information for us.""" """Holds compile time information for us."""
def __init__(self, eval_ctx, parent=None): def __init__(self, eval_ctx, parent=None, level=None):
self.eval_ctx = eval_ctx self.eval_ctx = eval_ctx
self.symbols = Symbols(parent and parent.symbols or None) self.symbols = Symbols(parent and parent.symbols or None,
level=level)
# a toplevel frame is the root + soft frames such as if conditions. # a toplevel frame is the root + soft frames such as if conditions.
self.toplevel = False self.toplevel = False
@ -168,8 +169,10 @@ def copy(self):
rv.symbols = self.symbols.copy() rv.symbols = self.symbols.copy()
return rv return rv
def inner(self): def inner(self, isolated=False):
"""Return an inner frame.""" """Return an inner frame."""
if isolated:
return Frame(self.eval_ctx, level=self.symbols.level + 1)
return Frame(self.eval_ctx, self) return Frame(self.eval_ctx, self)
def soft(self): def soft(self):
@ -302,6 +305,9 @@ def __init__(self, environment, name, filename, stream=None,
# Tracks parameter definition blocks # Tracks parameter definition blocks
self._param_def_block = [] self._param_def_block = []
# Tracks the current context.
self._context_reference_stack = ['context']
# -- Various compilation helpers # -- Various compilation helpers
def fail(self, msg, lineno): def fail(self, msg, lineno):
@ -472,8 +478,8 @@ def enter_frame(self, frame):
if action == VAR_LOAD_PARAMETER: if action == VAR_LOAD_PARAMETER:
pass pass
elif action == VAR_LOAD_RESOLVE: elif action == VAR_LOAD_RESOLVE:
self.writeline('%s = resolve(%r)' % self.writeline('%s = %s(%r)' %
(target, param)) (target, self.get_resolve_func(), param))
elif action == VAR_LOAD_ALIAS: elif action == VAR_LOAD_ALIAS:
self.writeline('%s = %s' % (target, param)) self.writeline('%s = %s' % (target, param))
elif action == VAR_LOAD_UNDEFINED: elif action == VAR_LOAD_UNDEFINED:
@ -625,6 +631,27 @@ def mark_parameter_stored(self, target):
if self._param_def_block: if self._param_def_block:
self._param_def_block[-1].discard(target) self._param_def_block[-1].discard(target)
def push_context_reference(self, target):
self._context_reference_stack.append(target)
def pop_context_reference(self):
self._context_reference_stack.pop()
def get_context_ref(self):
return self._context_reference_stack[-1]
def get_resolve_func(self):
target = self._context_reference_stack[-1]
if target == 'context':
return 'resolve'
return '%s.resolve' % target
def derive_context(self, frame):
return '%s.derived(%s)' % (
self.get_context_ref(),
self.dump_local_context(frame),
)
def parameter_is_undeclared(self, target): def parameter_is_undeclared(self, target):
"""Checks if a given target is an undeclared parameter.""" """Checks if a given target is an undeclared parameter."""
if not self._param_def_block: if not self._param_def_block:
@ -793,8 +820,11 @@ def visit_Block(self, node, frame):
self.writeline('if parent_template is None:') self.writeline('if parent_template is None:')
self.indent() self.indent()
level += 1 level += 1
context = node.scoped and (
'context.derived(%s)' % self.dump_local_context(frame)) or 'context' if node.scoped:
context = self.derive_context(frame)
else:
context = self.get_context_ref()
if supports_yield_from and not self.environment.is_async and \ if supports_yield_from and not self.environment.is_async and \
frame.buffer is None: frame.buffer is None:
@ -1082,9 +1112,9 @@ def visit_For(self, node, frame):
self.write(')') self.write(')')
if node.recursive: if node.recursive:
self.write(', loop_render_func, depth):') self.write(', undefined, loop_render_func, depth):')
else: else:
self.write(extended_loop and '):' or ':') self.write(extended_loop and ', undefined):' or ':')
self.indent() self.indent()
self.enter_frame(loop_frame) self.enter_frame(loop_frame)
@ -1129,6 +1159,13 @@ def visit_If(self, node, frame):
self.indent() self.indent()
self.blockvisit(node.body, if_frame) self.blockvisit(node.body, if_frame)
self.outdent() self.outdent()
for elif_ in node.elif_:
self.writeline('elif ', elif_)
self.visit(elif_.test, if_frame)
self.write(':')
self.indent()
self.blockvisit(elif_.body, if_frame)
self.outdent()
if node.else_: if node.else_:
self.writeline('else:') self.writeline('else:')
self.indent() self.indent()
@ -1348,7 +1385,12 @@ def visit_AssignBlock(self, node, frame):
self.newline(node) self.newline(node)
self.visit(node.target, frame) self.visit(node.target, frame)
self.write(' = (Markup if context.eval_ctx.autoescape ' self.write(' = (Markup if context.eval_ctx.autoescape '
'else identity)(concat(%s))' % block_frame.buffer) 'else identity)(')
if node.filter is not None:
self.visit_Filter(node.filter, block_frame)
else:
self.write('concat(%s)' % block_frame.buffer)
self.write(')')
self.pop_assign_tracking(frame) self.pop_assign_tracking(frame)
self.leave_frame(block_frame) self.leave_frame(block_frame)
@ -1373,6 +1415,18 @@ def visit_Name(self, node, frame):
self.write(ref) self.write(ref)
def visit_NSRef(self, node, frame):
# NSRefs can only be used to store values; since they use the normal
# `foo.bar` notation they will be parsed as a normal attribute access
# when used anywhere but in a `set` context
ref = frame.symbols.ref(node.name)
self.writeline('if not isinstance(%s, Namespace):' % ref)
self.indent()
self.writeline('raise TemplateRuntimeError(%r)' %
'cannot assign attribute on non-namespace object')
self.outdent()
self.writeline('%s[%r]' % (ref, node.attr))
def visit_Const(self, node, frame): def visit_Const(self, node, frame):
val = node.as_const(frame.eval_ctx) val = node.as_const(frame.eval_ctx)
if isinstance(val, float): if isinstance(val, float):
@ -1631,6 +1685,20 @@ def visit_Scope(self, node, frame):
self.blockvisit(node.body, scope_frame) self.blockvisit(node.body, scope_frame)
self.leave_frame(scope_frame) self.leave_frame(scope_frame)
def visit_OverlayScope(self, node, frame):
ctx = self.temporary_identifier()
self.writeline('%s = %s' % (ctx, self.derive_context(frame)))
self.writeline('%s.vars = ' % ctx)
self.visit(node.context, frame)
self.push_context_reference(ctx)
scope_frame = frame.inner(isolated=True)
scope_frame.symbols.analyze_node(node)
self.enter_frame(scope_frame)
self.blockvisit(node.body, scope_frame)
self.leave_frame(scope_frame)
self.pop_context_reference()
def visit_EvalContextModifier(self, node, frame): def visit_EvalContextModifier(self, node, frame):
for keyword in node.options: for keyword in node.options:
self.writeline('context.eval_ctx.%s = ' % keyword.key) self.writeline('context.eval_ctx.%s = ' % keyword.key)

View File

@ -198,7 +198,7 @@ def translate_exception(exc_info, initial_skip=0):
def get_jinja_locals(real_locals): def get_jinja_locals(real_locals):
ctx = real_locals.get('context') ctx = real_locals.get('context')
if ctx: if ctx:
locals = ctx.get_all() locals = ctx.get_all().copy()
else: else:
locals = {} locals = {}

View File

@ -9,7 +9,7 @@
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
from jinja2._compat import range_type from jinja2._compat import range_type
from jinja2.utils import generate_lorem_ipsum, Cycler, Joiner from jinja2.utils import generate_lorem_ipsum, Cycler, Joiner, Namespace
# defaults for the parser / lexer # defaults for the parser / lexer
@ -35,7 +35,8 @@
'dict': dict, 'dict': dict,
'lipsum': generate_lorem_ipsum, 'lipsum': generate_lorem_ipsum,
'cycler': Cycler, 'cycler': Cycler,
'joiner': Joiner 'joiner': Joiner,
'namespace': Namespace
} }
@ -47,6 +48,7 @@
'truncate.leeway': 5, 'truncate.leeway': 5,
'json.dumps_function': None, 'json.dumps_function': None,
'json.dumps_kwargs': {'sort_keys': True}, 'json.dumps_kwargs': {'sort_keys': True},
'ext.i18n.trimmed': False,
} }

View File

@ -809,7 +809,7 @@ def _load_template(self, name, globals):
@internalcode @internalcode
def get_template(self, name, parent=None, globals=None): def get_template(self, name, parent=None, globals=None):
"""Load a template from the loader. If a loader is configured this """Load a template from the loader. If a loader is configured this
method ask the loader for the template and returns a :class:`Template`. method asks the loader for the template and returns a :class:`Template`.
If the `parent` parameter is not `None`, :meth:`join_path` is called If the `parent` parameter is not `None`, :meth:`join_path` is called
to get the real template name before loading. to get the real template name before loading.

View File

@ -10,6 +10,8 @@
:copyright: (c) 2017 by the Jinja Team. :copyright: (c) 2017 by the Jinja Team.
:license: BSD. :license: BSD.
""" """
import re
from jinja2 import nodes from jinja2 import nodes
from jinja2.defaults import BLOCK_START_STRING, \ from jinja2.defaults import BLOCK_START_STRING, \
BLOCK_END_STRING, VARIABLE_START_STRING, VARIABLE_END_STRING, \ BLOCK_END_STRING, VARIABLE_START_STRING, VARIABLE_END_STRING, \
@ -223,6 +225,7 @@ def parse(self, parser):
plural_expr = None plural_expr = None
plural_expr_assignment = None plural_expr_assignment = None
variables = {} variables = {}
trimmed = None
while parser.stream.current.type != 'block_end': while parser.stream.current.type != 'block_end':
if variables: if variables:
parser.stream.expect('comma') parser.stream.expect('comma')
@ -241,6 +244,9 @@ def parse(self, parser):
if parser.stream.current.type == 'assign': if parser.stream.current.type == 'assign':
next(parser.stream) next(parser.stream)
variables[name.value] = var = parser.parse_expression() variables[name.value] = var = parser.parse_expression()
elif trimmed is None and name.value in ('trimmed', 'notrimmed'):
trimmed = name.value == 'trimmed'
continue
else: else:
variables[name.value] = var = nodes.Name(name.value, 'load') variables[name.value] = var = nodes.Name(name.value, 'load')
@ -256,7 +262,7 @@ def parse(self, parser):
parser.stream.expect('block_end') parser.stream.expect('block_end')
plural = plural_names = None plural = None
have_plural = False have_plural = False
referenced = set() referenced = set()
@ -297,6 +303,13 @@ def parse(self, parser):
elif plural_expr is None: elif plural_expr is None:
parser.fail('pluralize without variables', lineno) parser.fail('pluralize without variables', lineno)
if trimmed is None:
trimmed = self.environment.policies['ext.i18n.trimmed']
if trimmed:
singular = self._trim_whitespace(singular)
if plural:
plural = self._trim_whitespace(plural)
node = self._make_node(singular, plural, variables, plural_expr, node = self._make_node(singular, plural, variables, plural_expr,
bool(referenced), bool(referenced),
num_called_num and have_plural) num_called_num and have_plural)
@ -306,6 +319,9 @@ def parse(self, parser):
else: else:
return node return node
def _trim_whitespace(self, string, _ws_re=re.compile(r'\s*\n\s*')):
return _ws_re.sub(' ', string.strip())
def _parse_block(self, parser, allow_pluralize): def _parse_block(self, parser, allow_pluralize):
"""Parse until the next block tag with a given name.""" """Parse until the next block tag with a given name."""
referenced = [] referenced = []
@ -583,6 +599,8 @@ def getbool(options, key, default=False):
auto_reload=False auto_reload=False
) )
if getbool(options, 'trimmed'):
environment.policies['ext.i18n.trimmed'] = True
if getbool(options, 'newstyle_gettext'): if getbool(options, 'newstyle_gettext'):
environment.newstyle_gettext = True environment.newstyle_gettext = True

View File

@ -10,9 +10,10 @@
""" """
import re import re
import math import math
import random
import warnings
from random import choice from itertools import groupby, chain
from itertools import groupby
from collections import namedtuple from collections import namedtuple
from jinja2.utils import Markup, escape, pformat, urlize, soft_unicode, \ from jinja2.utils import Markup, escape, pformat, urlize, soft_unicode, \
unicode_urlencode, htmlsafe_json_dumps unicode_urlencode, htmlsafe_json_dumps
@ -52,22 +53,34 @@ def environmentfilter(f):
return f return f
def make_attrgetter(environment, attribute): def ignore_case(value):
"""For use as a postprocessor for :func:`make_attrgetter`. Converts strings
to lowercase and returns other types as-is."""
return value.lower() if isinstance(value, string_types) else value
def make_attrgetter(environment, attribute, postprocess=None):
"""Returns a callable that looks up the given attribute from a """Returns a callable that looks up the given attribute from a
passed object with the rules of the environment. Dots are allowed passed object with the rules of the environment. Dots are allowed
to access attributes of attributes. Integer parts in paths are to access attributes of attributes. Integer parts in paths are
looked up as integers. looked up as integers.
""" """
if not isinstance(attribute, string_types) \ if attribute is None:
or ('.' not in attribute and not attribute.isdigit()): attribute = []
return lambda x: environment.getitem(x, attribute) elif isinstance(attribute, string_types):
attribute = attribute.split('.') attribute = [int(x) if x.isdigit() else x for x in attribute.split('.')]
else:
attribute = [attribute]
def attrgetter(item): def attrgetter(item):
for part in attribute: for part in attribute:
if part.isdigit():
part = int(part)
item = environment.getitem(item, part) item = environment.getitem(item, part)
if postprocess is not None:
item = postprocess(item)
return item return item
return attrgetter return attrgetter
@ -190,7 +203,7 @@ def do_title(s):
if item]) if item])
def do_dictsort(value, case_sensitive=False, by='key'): def do_dictsort(value, case_sensitive=False, by='key', reverse=False):
"""Sort a dict and yield (key, value) pairs. Because python dicts are """Sort a dict and yield (key, value) pairs. Because python dicts are
unsorted you may want to use this function to order them by either unsorted you may want to use this function to order them by either
key or value: key or value:
@ -200,6 +213,9 @@ def do_dictsort(value, case_sensitive=False, by='key'):
{% for item in mydict|dictsort %} {% for item in mydict|dictsort %}
sort the dict by key, case insensitive sort the dict by key, case insensitive
{% for item in mydict|dictsort(reverse=true) %}
sort the dict by key, case insensitive, reverse order
{% for item in mydict|dictsort(true) %} {% for item in mydict|dictsort(true) %}
sort the dict by key, case sensitive sort the dict by key, case sensitive
@ -211,20 +227,25 @@ def do_dictsort(value, case_sensitive=False, by='key'):
elif by == 'value': elif by == 'value':
pos = 1 pos = 1
else: else:
raise FilterArgumentError('You can only sort by either ' raise FilterArgumentError(
'"key" or "value"') 'You can only sort by either "key" or "value"'
)
def sort_func(item): def sort_func(item):
value = item[pos] value = item[pos]
if isinstance(value, string_types) and not case_sensitive:
value = value.lower() if not case_sensitive:
value = ignore_case(value)
return value return value
return sorted(value.items(), key=sort_func) return sorted(value.items(), key=sort_func, reverse=reverse)
@environmentfilter @environmentfilter
def do_sort(environment, value, reverse=False, case_sensitive=False, def do_sort(
attribute=None): environment, value, reverse=False, case_sensitive=False, attribute=None
):
"""Sort an iterable. Per default it sorts ascending, if you pass it """Sort an iterable. Per default it sorts ascending, if you pass it
true as first argument it will reverse the sorting. true as first argument it will reverse the sorting.
@ -250,18 +271,85 @@ def do_sort(environment, value, reverse=False, case_sensitive=False,
.. versionchanged:: 2.6 .. versionchanged:: 2.6
The `attribute` parameter was added. The `attribute` parameter was added.
""" """
if not case_sensitive: key_func = make_attrgetter(
def sort_func(item): environment, attribute,
if isinstance(item, string_types): postprocess=ignore_case if not case_sensitive else None
item = item.lower() )
return item return sorted(value, key=key_func, reverse=reverse)
else:
sort_func = None
if attribute is not None: @environmentfilter
getter = make_attrgetter(environment, attribute) def do_unique(environment, value, case_sensitive=False, attribute=None):
def sort_func(item, processor=sort_func or (lambda x: x)): """Returns a list of unique items from the the given iterable.
return processor(getter(item))
return sorted(value, key=sort_func, reverse=reverse) .. sourcecode:: jinja
{{ ['foo', 'bar', 'foobar', 'FooBar']|unique }}
-> ['foo', 'bar', 'foobar']
The unique items are yielded in the same order as their first occurrence in
the iterable passed to the filter.
:param case_sensitive: Treat upper and lower case strings as distinct.
:param attribute: Filter objects with unique values for this attribute.
"""
getter = make_attrgetter(
environment, attribute,
postprocess=ignore_case if not case_sensitive else None
)
seen = set()
for item in value:
key = getter(item)
if key not in seen:
seen.add(key)
yield item
def _min_or_max(environment, value, func, case_sensitive, attribute):
it = iter(value)
try:
first = next(it)
except StopIteration:
return environment.undefined('No aggregated item, sequence was empty.')
key_func = make_attrgetter(
environment, attribute,
ignore_case if not case_sensitive else None
)
return func(chain([first], it), key=key_func)
@environmentfilter
def do_min(environment, value, case_sensitive=False, attribute=None):
"""Return the smallest item from the sequence.
.. sourcecode:: jinja
{{ [1, 2, 3]|min }}
-> 1
:param case_sensitive: Treat upper and lower case strings as distinct.
:param attribute: Get the object with the max value of this attribute.
"""
return _min_or_max(environment, value, min, case_sensitive, attribute)
@environmentfilter
def do_max(environment, value, case_sensitive=False, attribute=None):
"""Return the largest item from the sequence.
.. sourcecode:: jinja
{{ [1, 2, 3]|max }}
-> 3
:param case_sensitive: Treat upper and lower case strings as distinct.
:param attribute: Get the object with the max value of this attribute.
"""
return _min_or_max(environment, value, max, case_sensitive, attribute)
def do_default(value, default_value=u'', boolean=False): def do_default(value, default_value=u'', boolean=False):
@ -359,13 +447,13 @@ def do_last(environment, seq):
return environment.undefined('No last item, sequence was empty.') return environment.undefined('No last item, sequence was empty.')
@environmentfilter @contextfilter
def do_random(environment, seq): def do_random(context, seq):
"""Return a random item from the sequence.""" """Return a random item from the sequence."""
try: try:
return choice(seq) return random.choice(seq)
except IndexError: except IndexError:
return environment.undefined('No random item, sequence was empty.') return context.environment.undefined('No random item, sequence was empty.')
def do_filesizeformat(value, binary=False): def do_filesizeformat(value, binary=False):
@ -445,21 +533,44 @@ def do_urlize(eval_ctx, value, trim_url_limit=None, nofollow=False,
return rv return rv
def do_indent(s, width=4, indentfirst=False): def do_indent(
"""Return a copy of the passed string, each line indented by s, width=4, first=False, blank=False, indentfirst=None
4 spaces. The first line is not indented. If you want to ):
change the number of spaces or indent the first line too """Return a copy of the string with each line indented by 4 spaces. The
you can pass additional parameters to the filter: first line and blank lines are not indented by default.
.. sourcecode:: jinja :param width: Number of spaces to indent by.
:param first: Don't skip indenting the first line.
:param blank: Don't skip indenting empty lines.
{{ mytext|indent(2, true) }} .. versionchanged:: 2.10
indent by two spaces and indent the first line too. Blank lines are not indented by default.
Rename the ``indentfirst`` argument to ``first``.
""" """
if indentfirst is not None:
warnings.warn(DeprecationWarning(
'The "indentfirst" argument is renamed to "first".'
), stacklevel=2)
first = indentfirst
s += u'\n' # this quirk is necessary for splitlines method
indention = u' ' * width indention = u' ' * width
rv = (u'\n' + indention).join(s.splitlines())
if indentfirst: if blank:
rv = (u'\n' + indention).join(s.splitlines())
else:
lines = s.splitlines()
rv = lines.pop(0)
if lines:
rv += u'\n' + u'\n'.join(
indention + line if line else line for line in lines
)
if first:
rv = indention + rv rv = indention + rv
return rv return rv
@ -865,6 +976,9 @@ def do_select(*args, **kwargs):
{{ numbers|select("odd") }} {{ numbers|select("odd") }}
{{ numbers|select("odd") }} {{ numbers|select("odd") }}
{{ numbers|select("divisibleby", 3) }}
{{ numbers|select("lessthan", 42) }}
{{ strings|select("equalto", "mystring") }}
.. versionadded:: 2.7 .. versionadded:: 2.7
""" """
@ -1045,6 +1159,8 @@ def select_or_reject(args, kwargs, modfunc, lookup_attr):
'list': do_list, 'list': do_list,
'lower': do_lower, 'lower': do_lower,
'map': do_map, 'map': do_map,
'min': do_min,
'max': do_max,
'pprint': do_pprint, 'pprint': do_pprint,
'random': do_random, 'random': do_random,
'reject': do_reject, 'reject': do_reject,
@ -1063,6 +1179,7 @@ def select_or_reject(args, kwargs, modfunc, lookup_attr):
'title': do_title, 'title': do_title,
'trim': do_trim, 'trim': do_trim,
'truncate': do_truncate, 'truncate': do_truncate,
'unique': do_unique,
'upper': do_upper, 'upper': do_upper,
'urlencode': do_urlencode, 'urlencode': do_urlencode,
'urlize': do_urlize, 'urlize': do_urlize,

View File

@ -24,11 +24,13 @@ def symbols_for_node(node, parent_symbols=None):
class Symbols(object): class Symbols(object):
def __init__(self, parent=None): def __init__(self, parent=None, level=None):
if parent is None: if level is None:
self.level = 0 if parent is None:
else: level = 0
self.level = parent.level + 1 else:
level = parent.level + 1
self.level = level
self.parent = parent self.parent = parent
self.refs = {} self.refs = {}
self.loads = {} self.loads = {}
@ -167,6 +169,10 @@ def visit_CallBlock(self, node, **kwargs):
for child in node.iter_child_nodes(exclude=('call',)): for child in node.iter_child_nodes(exclude=('call',)):
self.sym_visitor.visit(child) self.sym_visitor.visit(child)
def visit_OverlayScope(self, node, **kwargs):
for child in node.body:
self.sym_visitor.visit(child)
def visit_For(self, node, for_branch='body', **kwargs): def visit_For(self, node, for_branch='body', **kwargs):
if for_branch == 'body': if for_branch == 'body':
self.sym_visitor.visit(node.target, store_as_param=True) self.sym_visitor.visit(node.target, store_as_param=True)
@ -209,6 +215,9 @@ def visit_Name(self, node, store_as_param=False, **kwargs):
elif node.ctx == 'load': elif node.ctx == 'load':
self.symbols.load(node.name) self.symbols.load(node.name)
def visit_NSRef(self, node, **kwargs):
self.symbols.load(node.name)
def visit_If(self, node, **kwargs): def visit_If(self, node, **kwargs):
self.visit(node.test, **kwargs) self.visit(node.test, **kwargs)
@ -222,9 +231,10 @@ def inner_visit(nodes):
return rv return rv
body_symbols = inner_visit(node.body) body_symbols = inner_visit(node.body)
elif_symbols = inner_visit(node.elif_)
else_symbols = inner_visit(node.else_ or ()) else_symbols = inner_visit(node.else_ or ())
self.symbols.branch_update([body_symbols, else_symbols]) self.symbols.branch_update([body_symbols, elif_symbols, else_symbols])
def visit_Macro(self, node, **kwargs): def visit_Macro(self, node, **kwargs):
self.symbols.store(node.name) self.symbols.store(node.name)
@ -271,3 +281,6 @@ def visit_Scope(self, node, **kwargs):
def visit_Block(self, node, **kwargs): def visit_Block(self, node, **kwargs):
"""Stop visiting at blocks.""" """Stop visiting at blocks."""
def visit_OverlayScope(self, node, **kwargs):
"""Do not visit into overlay scopes."""

View File

@ -15,14 +15,12 @@
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
import re import re
import sys
from operator import itemgetter
from collections import deque from collections import deque
from operator import itemgetter
from jinja2._compat import implements_iterator, intern, iteritems, text_type
from jinja2.exceptions import TemplateSyntaxError from jinja2.exceptions import TemplateSyntaxError
from jinja2.utils import LRUCache from jinja2.utils import LRUCache
from jinja2._compat import iteritems, implements_iterator, text_type, intern
# cache for the lexers. Exists in order to be able to have multiple # cache for the lexers. Exists in order to be able to have multiple
# environments with the same lexer # environments with the same lexer
@ -34,28 +32,25 @@
r'|"([^"\\]*(?:\\.[^"\\]*)*)")', re.S) r'|"([^"\\]*(?:\\.[^"\\]*)*)")', re.S)
integer_re = re.compile(r'\d+') integer_re = re.compile(r'\d+')
def _make_name_re(): try:
try: # check if this Python supports Unicode identifiers
compile('föö', '<unknown>', 'eval') compile('föö', '<unknown>', 'eval')
except SyntaxError: except SyntaxError:
return re.compile(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b') # no Unicode support, use ASCII identifiers
name_re = re.compile(r'[a-zA-Z_][a-zA-Z0-9_]*')
check_ident = False
else:
# Unicode support, build a pattern to match valid characters, and set flag
# to use str.isidentifier to validate during lexing
from jinja2 import _identifier
name_re = re.compile(r'[\w{0}]+'.format(_identifier.pattern))
check_ident = True
# remove the pattern from memory after building the regex
import sys
del sys.modules['jinja2._identifier']
import jinja2 import jinja2
from jinja2 import _stringdefs del jinja2._identifier
name_re = re.compile(r'[%s][%s]*' % (_stringdefs.xid_start, del _identifier
_stringdefs.xid_continue))
# Save some memory here
sys.modules.pop('jinja2._stringdefs')
del _stringdefs
del jinja2._stringdefs
return name_re
# we use the unicode identifier rule if this python version is able
# to handle unicode identifiers, otherwise the standard ASCII one.
name_re = _make_name_re()
del _make_name_re
float_re = re.compile(r'(?<!\.)\d+\.\d+') float_re = re.compile(r'(?<!\.)\d+\.\d+')
newline_re = re.compile(r'(\r\n|\r|\n)') newline_re = re.compile(r'(\r\n|\r|\n)')
@ -352,7 +347,10 @@ def skip_if(self, expr):
return self.next_if(expr) is not None return self.next_if(expr) is not None
def __next__(self): def __next__(self):
"""Go one token ahead and return the old one""" """Go one token ahead and return the old one.
Use the built-in :func:`next` instead of calling this directly.
"""
rv = self.current rv = self.current
if self._pushed: if self._pushed:
self.current = self._pushed.popleft() self.current = self._pushed.popleft()
@ -577,6 +575,10 @@ def wrap(self, stream, name=None, filename=None):
token = value token = value
elif token == 'name': elif token == 'name':
value = str(value) value = str(value)
if check_ident and not value.isidentifier():
raise TemplateSyntaxError(
'Invalid character in identifier',
lineno, name, filename)
elif token == 'string': elif token == 'string':
# try to unescape string # try to unescape string
try: try:

220
lib/spack/external/jinja2/nativetypes.py vendored Normal file
View File

@ -0,0 +1,220 @@
import sys
from ast import literal_eval
from itertools import islice, chain
from jinja2 import nodes
from jinja2._compat import text_type
from jinja2.compiler import CodeGenerator, has_safe_repr
from jinja2.environment import Environment, Template
from jinja2.utils import concat, escape
def native_concat(nodes):
"""Return a native Python type from the list of compiled nodes. If the
result is a single node, its value is returned. Otherwise, the nodes are
concatenated as strings. If the result can be parsed with
:func:`ast.literal_eval`, the parsed value is returned. Otherwise, the
string is returned.
"""
head = list(islice(nodes, 2))
if not head:
return None
if len(head) == 1:
out = head[0]
else:
out = u''.join([text_type(v) for v in chain(head, nodes)])
try:
return literal_eval(out)
except (ValueError, SyntaxError, MemoryError):
return out
class NativeCodeGenerator(CodeGenerator):
"""A code generator which avoids injecting ``to_string()`` calls around the
internal code Jinja uses to render templates.
"""
def visit_Output(self, node, frame):
"""Same as :meth:`CodeGenerator.visit_Output`, but do not call
``to_string`` on output nodes in generated code.
"""
if self.has_known_extends and frame.require_output_check:
return
finalize = self.environment.finalize
finalize_context = getattr(finalize, 'contextfunction', False)
finalize_eval = getattr(finalize, 'evalcontextfunction', False)
finalize_env = getattr(finalize, 'environmentfunction', False)
if finalize is not None:
if finalize_context or finalize_eval:
const_finalize = None
elif finalize_env:
def const_finalize(x):
return finalize(self.environment, x)
else:
const_finalize = finalize
else:
def const_finalize(x):
return x
# If we are inside a frame that requires output checking, we do so.
outdent_later = False
if frame.require_output_check:
self.writeline('if parent_template is None:')
self.indent()
outdent_later = True
# Try to evaluate as many chunks as possible into a static string at
# compile time.
body = []
for child in node.nodes:
try:
if const_finalize is None:
raise nodes.Impossible()
const = child.as_const(frame.eval_ctx)
if not has_safe_repr(const):
raise nodes.Impossible()
except nodes.Impossible:
body.append(child)
continue
# the frame can't be volatile here, because otherwise the as_const
# function would raise an Impossible exception at that point
try:
if frame.eval_ctx.autoescape:
if hasattr(const, '__html__'):
const = const.__html__()
else:
const = escape(const)
const = const_finalize(const)
except Exception:
# if something goes wrong here we evaluate the node at runtime
# for easier debugging
body.append(child)
continue
if body and isinstance(body[-1], list):
body[-1].append(const)
else:
body.append([const])
# if we have less than 3 nodes or a buffer we yield or extend/append
if len(body) < 3 or frame.buffer is not None:
if frame.buffer is not None:
# for one item we append, for more we extend
if len(body) == 1:
self.writeline('%s.append(' % frame.buffer)
else:
self.writeline('%s.extend((' % frame.buffer)
self.indent()
for item in body:
if isinstance(item, list):
val = repr(native_concat(item))
if frame.buffer is None:
self.writeline('yield ' + val)
else:
self.writeline(val + ',')
else:
if frame.buffer is None:
self.writeline('yield ', item)
else:
self.newline(item)
close = 0
if finalize is not None:
self.write('environment.finalize(')
if finalize_context:
self.write('context, ')
close += 1
self.visit(item, frame)
if close > 0:
self.write(')' * close)
if frame.buffer is not None:
self.write(',')
if frame.buffer is not None:
# close the open parentheses
self.outdent()
self.writeline(len(body) == 1 and ')' or '))')
# otherwise we create a format string as this is faster in that case
else:
format = []
arguments = []
for item in body:
if isinstance(item, list):
format.append(native_concat(item).replace('%', '%%'))
else:
format.append('%s')
arguments.append(item)
self.writeline('yield ')
self.write(repr(concat(format)) + ' % (')
self.indent()
for argument in arguments:
self.newline(argument)
close = 0
if finalize is not None:
self.write('environment.finalize(')
if finalize_context:
self.write('context, ')
elif finalize_eval:
self.write('context.eval_ctx, ')
elif finalize_env:
self.write('environment, ')
close += 1
self.visit(argument, frame)
self.write(')' * close + ', ')
self.outdent()
self.writeline(')')
if outdent_later:
self.outdent()
class NativeTemplate(Template):
def render(self, *args, **kwargs):
"""Render the template to produce a native Python type. If the result
is a single node, its value is returned. Otherwise, the nodes are
concatenated as strings. If the result can be parsed with
:func:`ast.literal_eval`, the parsed value is returned. Otherwise, the
string is returned.
"""
vars = dict(*args, **kwargs)
try:
return native_concat(self.root_render_func(self.new_context(vars)))
except Exception:
exc_info = sys.exc_info()
return self.environment.handle_exception(exc_info, True)
class NativeEnvironment(Environment):
"""An environment that renders templates to native Python types."""
code_generator_class = NativeCodeGenerator
template_class = NativeTemplate

View File

@ -314,7 +314,7 @@ class For(Stmt):
class If(Stmt): class If(Stmt):
"""If `test` is true, `body` is rendered, else `else_`.""" """If `test` is true, `body` is rendered, else `else_`."""
fields = ('test', 'body', 'else_') fields = ('test', 'body', 'elif_', 'else_')
class Macro(Stmt): class Macro(Stmt):
@ -387,7 +387,7 @@ class Assign(Stmt):
class AssignBlock(Stmt): class AssignBlock(Stmt):
"""Assigns a block to a target.""" """Assigns a block to a target."""
fields = ('target', 'body') fields = ('target', 'filter', 'body')
class Expr(Node): class Expr(Node):
@ -465,6 +465,18 @@ def can_assign(self):
'True', 'False', 'None') 'True', 'False', 'None')
class NSRef(Expr):
"""Reference to a namespace value assignment"""
fields = ('name', 'attr')
def can_assign(self):
# We don't need any special checks here; NSRef assignments have a
# runtime check to ensure the target is a namespace object which will
# have been checked already as it is created using a normal assignment
# which goes through a `Name` node.
return True
class Literal(Expr): class Literal(Expr):
"""Baseclass for literals.""" """Baseclass for literals."""
abstract = True abstract = True
@ -587,6 +599,25 @@ def as_const(self, eval_ctx=None):
return self.expr2.as_const(eval_ctx) return self.expr2.as_const(eval_ctx)
def args_as_const(node, eval_ctx):
args = [x.as_const(eval_ctx) for x in node.args]
kwargs = dict(x.as_const(eval_ctx) for x in node.kwargs)
if node.dyn_args is not None:
try:
args.extend(node.dyn_args.as_const(eval_ctx))
except Exception:
raise Impossible()
if node.dyn_kwargs is not None:
try:
kwargs.update(node.dyn_kwargs.as_const(eval_ctx))
except Exception:
raise Impossible()
return args, kwargs
class Filter(Expr): class Filter(Expr):
"""This node applies a filter on an expression. `name` is the name of """This node applies a filter on an expression. `name` is the name of
the filter, the rest of the fields are the same as for :class:`Call`. the filter, the rest of the fields are the same as for :class:`Call`.
@ -594,44 +625,41 @@ class Filter(Expr):
If the `node` of a filter is `None` the contents of the last buffer are If the `node` of a filter is `None` the contents of the last buffer are
filtered. Buffers are created by macros and filter blocks. filtered. Buffers are created by macros and filter blocks.
""" """
fields = ('node', 'name', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs') fields = ('node', 'name', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs')
def as_const(self, eval_ctx=None): def as_const(self, eval_ctx=None):
eval_ctx = get_eval_context(self, eval_ctx) eval_ctx = get_eval_context(self, eval_ctx)
if eval_ctx.volatile or self.node is None: if eval_ctx.volatile or self.node is None:
raise Impossible() raise Impossible()
# we have to be careful here because we call filter_ below. # we have to be careful here because we call filter_ below.
# if this variable would be called filter, 2to3 would wrap the # if this variable would be called filter, 2to3 would wrap the
# call in a list beause it is assuming we are talking about the # call in a list beause it is assuming we are talking about the
# builtin filter function here which no longer returns a list in # builtin filter function here which no longer returns a list in
# python 3. because of that, do not rename filter_ to filter! # python 3. because of that, do not rename filter_ to filter!
filter_ = self.environment.filters.get(self.name) filter_ = self.environment.filters.get(self.name)
if filter_ is None or getattr(filter_, 'contextfilter', False): if filter_ is None or getattr(filter_, 'contextfilter', False):
raise Impossible() raise Impossible()
# We cannot constant handle async filters, so we need to make sure # We cannot constant handle async filters, so we need to make sure
# to not go down this path. # to not go down this path.
if eval_ctx.environment.is_async and \ if (
getattr(filter_, 'asyncfiltervariant', False): eval_ctx.environment.is_async
and getattr(filter_, 'asyncfiltervariant', False)
):
raise Impossible() raise Impossible()
obj = self.node.as_const(eval_ctx) args, kwargs = args_as_const(self, eval_ctx)
args = [obj] + [x.as_const(eval_ctx) for x in self.args] args.insert(0, self.node.as_const(eval_ctx))
if getattr(filter_, 'evalcontextfilter', False): if getattr(filter_, 'evalcontextfilter', False):
args.insert(0, eval_ctx) args.insert(0, eval_ctx)
elif getattr(filter_, 'environmentfilter', False): elif getattr(filter_, 'environmentfilter', False):
args.insert(0, self.environment) args.insert(0, self.environment)
kwargs = dict(x.as_const(eval_ctx) for x in self.kwargs)
if self.dyn_args is not None:
try:
args.extend(self.dyn_args.as_const(eval_ctx))
except Exception:
raise Impossible()
if self.dyn_kwargs is not None:
try:
kwargs.update(self.dyn_kwargs.as_const(eval_ctx))
except Exception:
raise Impossible()
try: try:
return filter_(*args, **kwargs) return filter_(*args, **kwargs)
except Exception: except Exception:
@ -642,8 +670,24 @@ class Test(Expr):
"""Applies a test on an expression. `name` is the name of the test, the """Applies a test on an expression. `name` is the name of the test, the
rest of the fields are the same as for :class:`Call`. rest of the fields are the same as for :class:`Call`.
""" """
fields = ('node', 'name', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs') fields = ('node', 'name', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs')
def as_const(self, eval_ctx=None):
test = self.environment.tests.get(self.name)
if test is None:
raise Impossible()
eval_ctx = get_eval_context(self, eval_ctx)
args, kwargs = args_as_const(self, eval_ctx)
args.insert(0, self.node.as_const(eval_ctx))
try:
return test(*args, **kwargs)
except Exception:
raise Impossible()
class Call(Expr): class Call(Expr):
"""Calls an expression. `args` is a list of arguments, `kwargs` a list """Calls an expression. `args` is a list of arguments, `kwargs` a list
@ -914,6 +958,22 @@ class Scope(Stmt):
fields = ('body',) fields = ('body',)
class OverlayScope(Stmt):
"""An overlay scope for extensions. This is a largely unoptimized scope
that however can be used to introduce completely arbitrary variables into
a sub scope from a dictionary or dictionary like object. The `context`
field has to evaluate to a dictionary object.
Example usage::
OverlayScope(context=self.call_method('get_context'),
body=[...])
.. versionadded:: 2.10
"""
fields = ('context', 'body')
class EvalContextModifier(Stmt): class EvalContextModifier(Stmt):
"""Modifies the eval context. For each option that should be modified, """Modifies the eval context. For each option that should be modified,
a :class:`Keyword` has to be added to the :attr:`options` list. a :class:`Keyword` has to be added to the :attr:`options` list.

View File

@ -176,13 +176,14 @@ def parse_statements(self, end_tokens, drop_needle=False):
def parse_set(self): def parse_set(self):
"""Parse an assign statement.""" """Parse an assign statement."""
lineno = next(self.stream).lineno lineno = next(self.stream).lineno
target = self.parse_assign_target() target = self.parse_assign_target(with_namespace=True)
if self.stream.skip_if('assign'): if self.stream.skip_if('assign'):
expr = self.parse_tuple() expr = self.parse_tuple()
return nodes.Assign(target, expr, lineno=lineno) return nodes.Assign(target, expr, lineno=lineno)
filter_node = self.parse_filter(None)
body = self.parse_statements(('name:endset',), body = self.parse_statements(('name:endset',),
drop_needle=True) drop_needle=True)
return nodes.AssignBlock(target, body, lineno=lineno) return nodes.AssignBlock(target, filter_node, body, lineno=lineno)
def parse_for(self): def parse_for(self):
"""Parse a for loop.""" """Parse a for loop."""
@ -210,17 +211,16 @@ def parse_if(self):
node.test = self.parse_tuple(with_condexpr=False) node.test = self.parse_tuple(with_condexpr=False)
node.body = self.parse_statements(('name:elif', 'name:else', node.body = self.parse_statements(('name:elif', 'name:else',
'name:endif')) 'name:endif'))
node.elif_ = []
node.else_ = []
token = next(self.stream) token = next(self.stream)
if token.test('name:elif'): if token.test('name:elif'):
new_node = nodes.If(lineno=self.stream.current.lineno) node = nodes.If(lineno=self.stream.current.lineno)
node.else_ = [new_node] result.elif_.append(node)
node = new_node
continue continue
elif token.test('name:else'): elif token.test('name:else'):
node.else_ = self.parse_statements(('name:endif',), result.else_ = self.parse_statements(('name:endif',),
drop_needle=True) drop_needle=True)
else:
node.else_ = []
break break
return result return result
@ -334,10 +334,9 @@ def parse_context():
if parse_context() or self.stream.current.type != 'comma': if parse_context() or self.stream.current.type != 'comma':
break break
else: else:
break self.stream.expect('name')
if not hasattr(node, 'with_context'): if not hasattr(node, 'with_context'):
node.with_context = False node.with_context = False
self.stream.skip_if('comma')
return node return node
def parse_signature(self, node): def parse_signature(self, node):
@ -395,15 +394,21 @@ def parse_print(self):
return node return node
def parse_assign_target(self, with_tuple=True, name_only=False, def parse_assign_target(self, with_tuple=True, name_only=False,
extra_end_rules=None): extra_end_rules=None, with_namespace=False):
"""Parse an assignment target. As Jinja2 allows assignments to """Parse an assignment target. As Jinja2 allows assignments to
tuples, this function can parse all allowed assignment targets. Per tuples, this function can parse all allowed assignment targets. Per
default assignments to tuples are parsed, that can be disable however default assignments to tuples are parsed, that can be disable however
by setting `with_tuple` to `False`. If only assignments to names are by setting `with_tuple` to `False`. If only assignments to names are
wanted `name_only` can be set to `True`. The `extra_end_rules` wanted `name_only` can be set to `True`. The `extra_end_rules`
parameter is forwarded to the tuple parsing function. parameter is forwarded to the tuple parsing function. If
`with_namespace` is enabled, a namespace assignment may be parsed.
""" """
if name_only: if with_namespace and self.stream.look().type == 'dot':
token = self.stream.expect('name')
next(self.stream) # dot
attr = self.stream.expect('name')
target = nodes.NSRef(token.value, attr.value, lineno=token.lineno)
elif name_only:
token = self.stream.expect('name') token = self.stream.expect('name')
target = nodes.Name(token.value, 'store', lineno=token.lineno) target = nodes.Name(token.value, 'store', lineno=token.lineno)
else: else:

View File

@ -15,7 +15,7 @@
from jinja2.nodes import EvalContext, _context_function_types from jinja2.nodes import EvalContext, _context_function_types
from jinja2.utils import Markup, soft_unicode, escape, missing, concat, \ from jinja2.utils import Markup, soft_unicode, escape, missing, concat, \
internalcode, object_type_repr, evalcontextfunction internalcode, object_type_repr, evalcontextfunction, Namespace
from jinja2.exceptions import UndefinedError, TemplateRuntimeError, \ from jinja2.exceptions import UndefinedError, TemplateRuntimeError, \
TemplateNotFound TemplateNotFound
from jinja2._compat import imap, text_type, iteritems, \ from jinja2._compat import imap, text_type, iteritems, \
@ -27,7 +27,7 @@
__all__ = ['LoopContext', 'TemplateReference', 'Macro', 'Markup', __all__ = ['LoopContext', 'TemplateReference', 'Macro', 'Markup',
'TemplateRuntimeError', 'missing', 'concat', 'escape', 'TemplateRuntimeError', 'missing', 'concat', 'escape',
'markup_join', 'unicode_join', 'to_string', 'identity', 'markup_join', 'unicode_join', 'to_string', 'identity',
'TemplateNotFound'] 'TemplateNotFound', 'Namespace']
#: the name of the function that is used to convert something into #: the name of the function that is used to convert something into
#: a string. We can just use the text type here. #: a string. We can just use the text type here.
@ -36,6 +36,7 @@
#: the identity function. Useful for certain things in the environment #: the identity function. Useful for certain things in the environment
identity = lambda x: x identity = lambda x: x
_first_iteration = object()
_last_iteration = object() _last_iteration = object()
@ -241,13 +242,14 @@ def call(__self, __obj, *args, **kwargs):
__traceback_hide__ = True # noqa __traceback_hide__ = True # noqa
# Allow callable classes to take a context # Allow callable classes to take a context
fn = __obj.__call__ if hasattr(__obj, '__call__'):
for fn_type in ('contextfunction', fn = __obj.__call__
'evalcontextfunction', for fn_type in ('contextfunction',
'environmentfunction'): 'evalcontextfunction',
if hasattr(fn, fn_type): 'environmentfunction'):
__obj = fn if hasattr(fn, fn_type):
break __obj = fn
break
if isinstance(__obj, _context_function_types): if isinstance(__obj, _context_function_types):
if getattr(__obj, 'contextfunction', 0): if getattr(__obj, 'contextfunction', 0):
@ -349,13 +351,17 @@ def __call__(self):
class LoopContextBase(object): class LoopContextBase(object):
"""A loop context for dynamic iteration.""" """A loop context for dynamic iteration."""
_before = _first_iteration
_current = _first_iteration
_after = _last_iteration _after = _last_iteration
_length = None _length = None
def __init__(self, recurse=None, depth0=0): def __init__(self, undefined, recurse=None, depth0=0):
self._undefined = undefined
self._recurse = recurse self._recurse = recurse
self.index0 = -1 self.index0 = -1
self.depth0 = depth0 self.depth0 = depth0
self._last_checked_value = missing
def cycle(self, *args): def cycle(self, *args):
"""Cycles among the arguments with the current loop index.""" """Cycles among the arguments with the current loop index."""
@ -363,6 +369,13 @@ def cycle(self, *args):
raise TypeError('no items for cycling given') raise TypeError('no items for cycling given')
return args[self.index0 % len(args)] return args[self.index0 % len(args)]
def changed(self, *value):
"""Checks whether the value has changed since the last call."""
if self._last_checked_value != value:
self._last_checked_value = value
return True
return False
first = property(lambda x: x.index0 == 0) first = property(lambda x: x.index0 == 0)
last = property(lambda x: x._after is _last_iteration) last = property(lambda x: x._after is _last_iteration)
index = property(lambda x: x.index0 + 1) index = property(lambda x: x.index0 + 1)
@ -370,6 +383,18 @@ def cycle(self, *args):
revindex0 = property(lambda x: x.length - x.index) revindex0 = property(lambda x: x.length - x.index)
depth = property(lambda x: x.depth0 + 1) depth = property(lambda x: x.depth0 + 1)
@property
def previtem(self):
if self._before is _first_iteration:
return self._undefined('there is no previous item')
return self._before
@property
def nextitem(self):
if self._after is _last_iteration:
return self._undefined('there is no next item')
return self._after
def __len__(self): def __len__(self):
return self.length return self.length
@ -395,8 +420,8 @@ def __repr__(self):
class LoopContext(LoopContextBase): class LoopContext(LoopContextBase):
def __init__(self, iterable, recurse=None, depth0=0): def __init__(self, iterable, undefined, recurse=None, depth0=0):
LoopContextBase.__init__(self, recurse, depth0) LoopContextBase.__init__(self, undefined, recurse, depth0)
self._iterator = iter(iterable) self._iterator = iter(iterable)
# try to get the length of the iterable early. This must be done # try to get the length of the iterable early. This must be done
@ -448,9 +473,10 @@ def __next__(self):
ctx.index0 += 1 ctx.index0 += 1
if ctx._after is _last_iteration: if ctx._after is _last_iteration:
raise StopIteration() raise StopIteration()
next_elem = ctx._after ctx._before = ctx._current
ctx._current = ctx._after
ctx._after = ctx._safe_next() ctx._after = ctx._safe_next()
return next_elem, ctx return ctx._current, ctx
class Macro(object): class Macro(object):

View File

@ -107,7 +107,7 @@ class _MagicFormatMapping(Mapping):
"""This class implements a dummy wrapper to fix a bug in the Python """This class implements a dummy wrapper to fix a bug in the Python
standard library for string formatting. standard library for string formatting.
See http://bugs.python.org/issue13598 for information about why See https://bugs.python.org/issue13598 for information about why
this is necessary. this is necessary.
""" """

View File

@ -8,6 +8,7 @@
:copyright: (c) 2017 by the Jinja Team. :copyright: (c) 2017 by the Jinja Team.
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
import operator
import re import re
from collections import Mapping from collections import Mapping
from jinja2.runtime import Undefined from jinja2.runtime import Undefined
@ -103,28 +104,6 @@ def test_sequence(value):
return True return True
def test_equalto(value, other):
"""Check if an object has the same value as another object:
.. sourcecode:: jinja
{% if foo.expression is equalto 42 %}
the foo attribute evaluates to the constant 42
{% endif %}
This appears to be a useless test as it does exactly the same as the
``==`` operator, but it can be useful when used together with the
`selectattr` function:
.. sourcecode:: jinja
{{ users|selectattr("email", "equalto", "foo@bar.invalid") }}
.. versionadded:: 2.8
"""
return value == other
def test_sameas(value, other): def test_sameas(value, other):
"""Check if an object points to the same memory address than another """Check if an object points to the same memory address than another
object: object:
@ -152,14 +131,12 @@ def test_escaped(value):
return hasattr(value, '__html__') return hasattr(value, '__html__')
def test_greaterthan(value, other): def test_in(value, seq):
"""Check if value is greater than other.""" """Check if value is in seq.
return value > other
.. versionadded:: 2.10
def test_lessthan(value, other): """
"""Check if value is less than other.""" return value in seq
return value < other
TESTS = { TESTS = {
@ -178,8 +155,21 @@ def test_lessthan(value, other):
'iterable': test_iterable, 'iterable': test_iterable,
'callable': test_callable, 'callable': test_callable,
'sameas': test_sameas, 'sameas': test_sameas,
'equalto': test_equalto,
'escaped': test_escaped, 'escaped': test_escaped,
'greaterthan': test_greaterthan, 'in': test_in,
'lessthan': test_lessthan '==': operator.eq,
'eq': operator.eq,
'equalto': operator.eq,
'!=': operator.ne,
'ne': operator.ne,
'>': operator.gt,
'gt': operator.gt,
'greaterthan': operator.gt,
'ge': operator.ge,
'>=': operator.ge,
'<': operator.lt,
'lt': operator.lt,
'lessthan': operator.lt,
'<=': operator.le,
'le': operator.le,
} }

View File

@ -567,7 +567,7 @@ def htmlsafe_json_dumps(obj, dumper=None, **kwargs):
.replace(u'>', u'\\u003e') \ .replace(u'>', u'\\u003e') \
.replace(u'&', u'\\u0026') \ .replace(u'&', u'\\u0026') \
.replace(u"'", u'\\u0027') .replace(u"'", u'\\u0027')
return rv return Markup(rv)
@implements_iterator @implements_iterator
@ -612,6 +612,29 @@ def __call__(self):
return self.sep return self.sep
class Namespace(object):
"""A namespace object that can hold arbitrary attributes. It may be
initialized from a dictionary or with keyword argments."""
def __init__(*args, **kwargs):
self, args = args[0], args[1:]
self.__attrs = dict(*args, **kwargs)
def __getattribute__(self, name):
if name == '_Namespace__attrs':
return object.__getattribute__(self, name)
try:
return self.__attrs[name]
except KeyError:
raise AttributeError(name)
def __setitem__(self, name, value):
self.__attrs[name] = value
def __repr__(self):
return '<Namespace %r>' % self.__attrs
# does this python version support async for in and async generators? # does this python version support async for in and async generators?
try: try:
exec('async def _():\n async for _ in ():\n yield _') exec('async def _():\n async for _ in ():\n yield _')