From ef1a4c698c1b9f1630519730c8379c0d6987004e Mon Sep 17 00:00:00 2001 From: aayushuppal Date: Mon, 6 May 2019 17:17:43 -0400 Subject: [PATCH] fixing LoopContext, loops indexing and iterator length property --- CHANGES.rst | 3 +++ jinja2/runtime.py | 23 ++++++++++----------- tests/test_runtime.py | 48 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 tests/test_runtime.py diff --git a/CHANGES.rst b/CHANGES.rst index 93122a60..381f78e8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -44,6 +44,9 @@ Unreleased 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 diff --git a/jinja2/runtime.py b/jinja2/runtime.py index cc048cfa..ff12deda 100644 --- a/jinja2/runtime.py +++ b/jinja2/runtime.py @@ -418,19 +418,17 @@ class LoopContext(LoopContextBase): 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) @@ -438,8 +436,7 @@ class LoopContext(LoopContextBase): # 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): @@ -447,7 +444,9 @@ class LoopContext(LoopContextBase): 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 diff --git a/tests/test_runtime.py b/tests/test_runtime.py new file mode 100644 index 00000000..1b24b40d --- /dev/null +++ b/tests/test_runtime.py @@ -0,0 +1,48 @@ +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) -- 2.47.2