Fixed #15849 -- Made IfChanged node thread safe.
Previously, the ifchanged node stored state on `self._last_seen`, thereby giving undesired results when the node is reused by another thread at the same time (e.g. globally caching a Template object). Thanks to akaihola for the report and Diederik van der Boor and Bas Peschier for the patch.
This commit is contained in:
parent
a5733fcd7b
commit
8503120c10
|
@ -215,32 +215,44 @@ class IfChangedNode(Node):
|
||||||
|
|
||||||
def __init__(self, nodelist_true, nodelist_false, *varlist):
|
def __init__(self, nodelist_true, nodelist_false, *varlist):
|
||||||
self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
|
self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
|
||||||
self._last_seen = None
|
|
||||||
self._varlist = varlist
|
self._varlist = varlist
|
||||||
self._id = str(id(self))
|
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
if 'forloop' in context and self._id not in context['forloop']:
|
# Init state storage
|
||||||
self._last_seen = None
|
state_frame = self._get_context_stack_frame(context)
|
||||||
context['forloop'][self._id] = 1
|
if self not in state_frame:
|
||||||
|
state_frame[self] = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self._varlist:
|
if self._varlist:
|
||||||
# Consider multiple parameters. This automatically behaves
|
# Consider multiple parameters. This automatically behaves
|
||||||
# like an OR evaluation of the multiple variables.
|
# like an OR evaluation of the multiple variables.
|
||||||
compare_to = [var.resolve(context, True) for var in self._varlist]
|
compare_to = [var.resolve(context, True) for var in self._varlist]
|
||||||
else:
|
else:
|
||||||
|
# The "{% ifchanged %}" syntax (without any variables) compares the rendered output.
|
||||||
compare_to = self.nodelist_true.render(context)
|
compare_to = self.nodelist_true.render(context)
|
||||||
except VariableDoesNotExist:
|
except VariableDoesNotExist:
|
||||||
compare_to = None
|
compare_to = None
|
||||||
|
|
||||||
if compare_to != self._last_seen:
|
if compare_to != state_frame[self]:
|
||||||
self._last_seen = compare_to
|
state_frame[self] = compare_to
|
||||||
content = self.nodelist_true.render(context)
|
return self.nodelist_true.render(context)
|
||||||
return content
|
|
||||||
elif self.nodelist_false:
|
elif self.nodelist_false:
|
||||||
return self.nodelist_false.render(context)
|
return self.nodelist_false.render(context)
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
def _get_context_stack_frame(self, context):
|
||||||
|
# The Context object behaves like a stack where each template tag can create a new scope.
|
||||||
|
# Find the place where to store the state to detect changes.
|
||||||
|
if 'forloop' in context:
|
||||||
|
# Ifchanged is bound to the local for loop.
|
||||||
|
# When there is a loop-in-loop, the state is bound to the inner loop,
|
||||||
|
# so it resets when the outer loop continues.
|
||||||
|
return context['forloop']
|
||||||
|
else:
|
||||||
|
# Using ifchanged outside loops. Effectively this is a no-op because the state is associated with 'self'.
|
||||||
|
return context.render_context
|
||||||
|
|
||||||
class IfEqualNode(Node):
|
class IfEqualNode(Node):
|
||||||
child_nodelists = ('nodelist_true', 'nodelist_false')
|
child_nodelists = ('nodelist_true', 'nodelist_false')
|
||||||
|
|
||||||
|
|
|
@ -420,6 +420,27 @@ class Templates(TestCase):
|
||||||
except TemplateSyntaxError as e:
|
except TemplateSyntaxError as e:
|
||||||
self.assertEqual(e.args[0], "Invalid block tag: 'endblock', expected 'elif', 'else' or 'endif'")
|
self.assertEqual(e.args[0], "Invalid block tag: 'endblock', expected 'elif', 'else' or 'endif'")
|
||||||
|
|
||||||
|
def test_ifchanged_concurrency(self):
|
||||||
|
# Tests for #15849
|
||||||
|
template = Template('[0{% for x in foo %},{% with var=get_value %}{% ifchanged %}{{ var }}{% endifchanged %}{% endwith %}{% endfor %}]')
|
||||||
|
|
||||||
|
# Using generator to mimic concurrency.
|
||||||
|
# The generator is not passed to the 'for' loop, because it does a list(values)
|
||||||
|
# instead, call gen.next() in the template to control the generator.
|
||||||
|
def gen():
|
||||||
|
yield 1
|
||||||
|
yield 2
|
||||||
|
# Simulate that another thread is now rendering.
|
||||||
|
# When the IfChangeNode stores state at 'self' it stays at '3' and skip the last yielded value below.
|
||||||
|
iter2 = iter([1, 2, 3])
|
||||||
|
output2 = template.render(Context({'foo': range(3), 'get_value': lambda: next(iter2)}))
|
||||||
|
self.assertEqual(output2, '[0,1,2,3]', 'Expected [0,1,2,3] in second parallel template, got {0}'.format(output2))
|
||||||
|
yield 3
|
||||||
|
|
||||||
|
gen1 = gen()
|
||||||
|
output1 = template.render(Context({'foo': range(3), 'get_value': lambda: next(gen1)}))
|
||||||
|
self.assertEqual(output1, '[0,1,2,3]', 'Expected [0,1,2,3] in first template, got {0}'.format(output1))
|
||||||
|
|
||||||
def test_templates(self):
|
def test_templates(self):
|
||||||
template_tests = self.get_template_tests()
|
template_tests = self.get_template_tests()
|
||||||
filter_tests = filters.get_filter_tests()
|
filter_tests = filters.get_filter_tests()
|
||||||
|
|
Loading…
Reference in New Issue