summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES3
-rw-r--r--docs/templates.rst20
-rw-r--r--jinja2/asyncsupport.py14
-rw-r--r--jinja2/compiler.py4
-rw-r--r--jinja2/runtime.py27
-rw-r--r--tests/test_async.py20
-rw-r--r--tests/test_core_tags.py20
7 files changed, 95 insertions, 13 deletions
diff --git a/CHANGES b/CHANGES
index 9550fa4..afb26c1 100644
--- a/CHANGES
+++ b/CHANGES
@@ -10,6 +10,9 @@ Version 2.10
derived context.
- Added an `in` test that works like the in operator. This can be used
in combination with `reject` and `select`.
+- Added `previtem` and `nextitem` to loop contexts, providing access to the
+ previous/next item in the loop. If such an item does not exist, the value is
+ undefined.
Version 2.9.6
-------------
diff --git a/docs/templates.rst b/docs/templates.rst
index e9e3639..f259f83 100644
--- a/docs/templates.rst
+++ b/docs/templates.rst
@@ -612,6 +612,12 @@ Inside of a for-loop block, you can access some special variables:
| `loop.depth0` | Indicates how deep in a recursive loop |
| | the rendering currently is. Starts at level 0 |
+-----------------------+---------------------------------------------------+
+| `loop.previtem` | The item from the previous iteration of the loop. |
+| | Undefined during the first iteration. |
++-----------------------+---------------------------------------------------+
+| `loop.nextitem` | The item from the following iteration of the loop.|
+| | Undefined during the last iteration. |
++-----------------------+---------------------------------------------------+
Within a for-loop, it's possible to cycle among a list of strings/variables
each time through the loop by using the special `loop.cycle` helper::
@@ -680,6 +686,20 @@ a bug where in some circumstances it appeared that assignments would work.
This is not supported. See :ref:`assignments` for more information about
how to deal with this.
+If all you want to do is check whether some value has changed since the
+last iteration or will change in the next iteration, you can use `previtem`
+and `nextitem`::
+
+ {% for value in values %}
+ {% if loop.previtem is defined and value > loop.previtem %}
+ The value just increased!
+ {% endif %}
+ {{ value }}
+ {% if loop.nextitem is defined and loop.nextitem > value %}
+ The value will increase even more!
+ {% endif %}
+ {% endfor %}
+
.. _if:
If
diff --git a/jinja2/asyncsupport.py b/jinja2/asyncsupport.py
index f4f264a..b1e7b5c 100644
--- a/jinja2/asyncsupport.py
+++ b/jinja2/asyncsupport.py
@@ -189,9 +189,9 @@ async def auto_aiter(iterable):
class AsyncLoopContext(LoopContextBase):
- def __init__(self, async_iterator, after, length, recurse=None,
+ def __init__(self, async_iterator, undefined, after, length, recurse=None,
depth0=0):
- LoopContextBase.__init__(self, recurse, depth0)
+ LoopContextBase.__init__(self, undefined, recurse, depth0)
self._async_iterator = async_iterator
self._after = after
self._length = length
@@ -221,15 +221,16 @@ class AsyncLoopContextIterator(object):
ctx.index0 += 1
if ctx._after is _last_iteration:
raise StopAsyncIteration()
- next_elem = ctx._after
+ ctx._before = ctx._current
+ ctx._current = ctx._after
try:
ctx._after = await ctx._async_iterator.__anext__()
except StopAsyncIteration:
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
# 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
@@ -251,4 +252,5 @@ async def make_async_loop_context(iterable, recurse=None, depth0=0):
after = await async_iterator.__anext__()
except StopAsyncIteration:
after = _last_iteration
- return AsyncLoopContext(async_iterator, after, length, recurse, depth0)
+ return AsyncLoopContext(async_iterator, undefined, after, length, recurse,
+ depth0)
diff --git a/jinja2/compiler.py b/jinja2/compiler.py
index 0a3f796..eb567c7 100644
--- a/jinja2/compiler.py
+++ b/jinja2/compiler.py
@@ -1112,9 +1112,9 @@ class CodeGenerator(NodeVisitor):
self.write(')')
if node.recursive:
- self.write(', loop_render_func, depth):')
+ self.write(', undefined, loop_render_func, depth):')
else:
- self.write(extended_loop and '):' or ':')
+ self.write(extended_loop and ', undefined):' or ':')
self.indent()
self.enter_frame(loop_frame)
diff --git a/jinja2/runtime.py b/jinja2/runtime.py
index 00d5f03..2f91a8c 100644
--- a/jinja2/runtime.py
+++ b/jinja2/runtime.py
@@ -36,6 +36,7 @@ to_string = text_type
#: the identity function. Useful for certain things in the environment
identity = lambda x: x
+_first_iteration = object()
_last_iteration = object()
@@ -349,10 +350,13 @@ class BlockReference(object):
class LoopContextBase(object):
"""A loop context for dynamic iteration."""
+ _before = _first_iteration
+ _current = _first_iteration
_after = _last_iteration
_length = None
- def __init__(self, recurse=None, depth0=0):
+ def __init__(self, undefined, recurse=None, depth0=0):
+ self._undefined = undefined
self._recurse = recurse
self.index0 = -1
self.depth0 = depth0
@@ -370,6 +374,18 @@ class LoopContextBase(object):
revindex0 = property(lambda x: x.length - x.index)
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):
return self.length
@@ -395,8 +411,8 @@ class LoopContextBase(object):
class LoopContext(LoopContextBase):
- def __init__(self, iterable, recurse=None, depth0=0):
- LoopContextBase.__init__(self, recurse, depth0)
+ def __init__(self, iterable, undefined, recurse=None, depth0=0):
+ LoopContextBase.__init__(self, undefined, recurse, depth0)
self._iterator = iter(iterable)
# try to get the length of the iterable early. This must be done
@@ -448,9 +464,10 @@ class LoopContextIterator(object):
ctx.index0 += 1
if ctx._after is _last_iteration:
raise StopIteration()
- next_elem = ctx._after
+ ctx._before = ctx._current
+ ctx._current = ctx._after
ctx._after = ctx._safe_next()
- return next_elem, ctx
+ return ctx._current, ctx
class Macro(object):
diff --git a/tests/test_async.py b/tests/test_async.py
index 279a4bb..83eb937 100644
--- a/tests/test_async.py
+++ b/tests/test_async.py
@@ -304,6 +304,14 @@ class TestAsyncForLoop(object):
output = tmpl.render(seq=list(range(4)), through=('<1>', '<2>'))
assert output == '<1><2>' * 4
+ def test_lookaround(self, test_env_async):
+ tmpl = test_env_async.from_string('''{% for item in seq -%}
+ {{ loop.previtem|default('x') }}-{{ item }}-{{
+ loop.nextitem|default('x') }}|
+ {%- endfor %}''')
+ output = tmpl.render(seq=list(range(4)))
+ assert output == 'x-0-1|0-1-2|1-2-3|2-3-x|'
+
def test_scope(self, test_env_async):
tmpl = test_env_async.from_string('{% for item in seq %}{% endfor %}{{ item }}')
output = tmpl.render(seq=list(range(10)))
@@ -331,6 +339,18 @@ class TestAsyncForLoop(object):
dict(a=3, b=[dict(a='a')])
]) == '[1<[1][2]>][2<[1][2]>][3<[a]>]'
+ def test_recursive_lookaround(self, test_env_async):
+ tmpl = test_env_async.from_string('''{% for item in seq recursive -%}
+ [{{ loop.previtem.a if loop.previtem is defined else 'x' }}.{{
+ item.a }}.{{ loop.nextitem.a if loop.nextitem is defined else 'x'
+ }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}]
+ {%- endfor %}''')
+ assert tmpl.render(seq=[
+ dict(a=1, b=[dict(a=1), dict(a=2)]),
+ dict(a=2, b=[dict(a=1), dict(a=2)]),
+ dict(a=3, b=[dict(a='a')])
+ ]) == '[x.1.2<[x.1.2][1.2.x]>][1.2.3<[x.1.2][1.2.x]>][2.3.x<[x.a.x]>]'
+
def test_recursive_depth0(self, test_env_async):
tmpl = test_env_async.from_string('''{% for item in seq recursive -%}
[{{ loop.depth0 }}:{{ item.a }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}]
diff --git a/tests/test_core_tags.py b/tests/test_core_tags.py
index f48d8b4..0fb4be8 100644
--- a/tests/test_core_tags.py
+++ b/tests/test_core_tags.py
@@ -68,6 +68,14 @@ class TestForLoop(object):
output = tmpl.render(seq=list(range(4)), through=('<1>', '<2>'))
assert output == '<1><2>' * 4
+ def test_lookaround(self, env):
+ tmpl = env.from_string('''{% for item in seq -%}
+ {{ loop.previtem|default('x') }}-{{ item }}-{{
+ loop.nextitem|default('x') }}|
+ {%- endfor %}''')
+ output = tmpl.render(seq=list(range(4)))
+ assert output == 'x-0-1|0-1-2|1-2-3|2-3-x|'
+
def test_scope(self, env):
tmpl = env.from_string('{% for item in seq %}{% endfor %}{{ item }}')
output = tmpl.render(seq=list(range(10)))
@@ -95,6 +103,18 @@ class TestForLoop(object):
dict(a=3, b=[dict(a='a')])
]) == '[1<[1][2]>][2<[1][2]>][3<[a]>]'
+ def test_recursive_lookaround(self, env):
+ tmpl = env.from_string('''{% for item in seq recursive -%}
+ [{{ loop.previtem.a if loop.previtem is defined else 'x' }}.{{
+ item.a }}.{{ loop.nextitem.a if loop.nextitem is defined else 'x'
+ }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}]
+ {%- endfor %}''')
+ assert tmpl.render(seq=[
+ dict(a=1, b=[dict(a=1), dict(a=2)]),
+ dict(a=2, b=[dict(a=1), dict(a=2)]),
+ dict(a=3, b=[dict(a='a')])
+ ]) == '[x.1.2<[x.1.2][1.2.x]>][1.2.3<[x.1.2][1.2.x]>][2.3.x<[x.a.x]>]'
+
def test_recursive_depth0(self, env):
tmpl = env.from_string('''{% for item in seq recursive -%}
[{{ loop.depth0 }}:{{ item.a }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}]