environment's ``undefined`` class. Omitting the ``else`` clause is a
valid shortcut and should not raise an error when using
:class:`StrictUndefined`). :issue:`710`, :pr:`1079`
+- Fix behavior of ``loop`` control variables such as ``length`` and
+ ``revindex0`` when looping over a generator. :issue:`459, 751, 794`,
+ :pr:`993`
Version 2.10.3
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
- # here because there are some broken iterators around where there
- # __len__ is the number of iterations left (i'm looking at your
- # listreverseiterator!).
- try:
- self._length = len(iterable)
- except (TypeError, AttributeError):
- self._length = None
+ self._iterations_done_count = 0
+ self._length = None
self._after = self._safe_next()
@property
def length(self):
+ """
+ Getting length of an iterator is a costly operation which requires extra memory
+ and traversing in linear time. So make it an on demand param that iterates from
+ the point onwards of the iterator and accounts for iterated elements.
+ """
if self._length is None:
# if was not possible to get the length of the iterator when
# the loop context was created (ie: iterating over a generator)
# length of that + the number of iterations so far.
iterable = tuple(self._iterator)
self._iterator = iter(iterable)
- iterations_done = self.index0 + 2
- self._length = len(iterable) + iterations_done
+ self._length = len(iterable) + self._iterations_done_count
return self._length
def __iter__(self):
def _safe_next(self):
try:
- return next(self._iterator)
+ tmp = next(self._iterator)
+ self._iterations_done_count += 1
+ return tmp
except StopIteration:
return _last_iteration
--- /dev/null
+from jinja2 import Template
+from jinja2.runtime import LoopContext
+
+
+TEST_IDX_TEMPLATE_STR_1 = (
+ "[{% for i in lst|reverse %}"
+ + "(len={{ loop.length }}, revindex={{ loop.revindex }}, index={{ loop.index }}, val={{ i }})"
+ + "{% endfor %}]"
+)
+
+
+TEST_IDX0_TEMPLATE_STR_1 = (
+ "[{% for i in lst|reverse %}"
+ + "(len={{ loop.length }}, revindex0={{ loop.revindex0 }}, index0={{ loop.index0 }}, val={{ i }})"
+ + "{% endfor %}]"
+)
+
+
+def test_loop_idx():
+ t = Template(TEST_IDX_TEMPLATE_STR_1)
+ lst = [10]
+ excepted_render = "[(len=1, revindex=1, index=1, val=10)]"
+ assert excepted_render == t.render(lst=lst)
+
+
+def test_loop_idx0():
+ t = Template(TEST_IDX0_TEMPLATE_STR_1)
+ lst = [10]
+ excepted_render = "[(len=1, revindex0=0, index0=0, val=10)]"
+ assert excepted_render == t.render(lst=lst)
+
+
+def test_loopcontext0():
+ in_lst = []
+ l = LoopContext(reversed(in_lst), None)
+ assert l.length == len(in_lst)
+
+
+def test_loopcontext1():
+ in_lst = [10]
+ l = LoopContext(reversed(in_lst), None)
+ assert l.length == len(in_lst)
+
+
+def test_loopcontext2():
+ in_lst = [10, 11]
+ l = LoopContext(reversed(in_lst), None)
+ assert l.length == len(in_lst)