]> git.ipfire.org Git - thirdparty/jinja.git/commitdiff
Add previtem/nextitem to loop context
authorAdrian Moennich <adrian@planetcoding.net>
Wed, 1 Feb 2017 20:05:03 +0000 (21:05 +0100)
committerAdrian Moennich <adrian@planetcoding.net>
Sat, 24 Jun 2017 08:52:06 +0000 (10:52 +0200)
related: #641

CHANGES
docs/templates.rst
jinja2/asyncsupport.py
jinja2/compiler.py
jinja2/runtime.py
tests/test_async.py
tests/test_core_tags.py

diff --git a/CHANGES b/CHANGES
index 9550fa46b50d52acd322db9053cbdfa61eb1f732..afb26c18c3f232c44ab489b5b3dbce9432ba4ddc 100644 (file)
--- 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
 -------------
index e9e363911f0176735eeb021e571077692d2675a8..f259f83943d356d5e996a01a5cd8b8e562c7421b 100644 (file)
@@ -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
index f4f264ad8cf3e67256785decce76cbb8b857bd4e..b1e7b5ce9a27a6e069abfab9d36f80117ce6a74c 100644 (file)
@@ -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)
index 0a3f7962a349e0254cdded3e2c11720534c6319d..eb567c798a152183121c6ca6dfe62002642d7047 100644 (file)
@@ -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)
index 00d5f03ca610252a176872f7b1b0cbd431f0609e..2f91a8c8d877caacd3724a4bd6f3e7de37eedcc5 100644 (file)
@@ -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):
index 279a4bb3721624f65b60405592368add8950bf21..83eb93765c3ba0422ab3e6513a482745e3f8f3f8 100644 (file)
@@ -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 %}]
index f48d8b4483b947ad5269373725ee042b1a581d83..0fb4be8fb5059ea590102b0b6579633cd6dce5ca 100644 (file)
@@ -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 %}]