diff options
-rw-r--r-- | CHANGES | 3 | ||||
-rw-r--r-- | docs/templates.rst | 20 | ||||
-rw-r--r-- | jinja2/asyncsupport.py | 14 | ||||
-rw-r--r-- | jinja2/compiler.py | 4 | ||||
-rw-r--r-- | jinja2/runtime.py | 27 | ||||
-rw-r--r-- | tests/test_async.py | 20 | ||||
-rw-r--r-- | tests/test_core_tags.py | 20 |
7 files changed, 95 insertions, 13 deletions
@@ -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 %}] |