Fixed #13373 -- Ensured that {% if %} statements will short circuit template logic and not evaluate clauses that don't require evaluation. Thanks to Jerry Stratton for the report.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@13001 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
ebfe9383bf
commit
fef0d25bdc
|
@ -56,7 +56,7 @@ def infix(bp, func):
|
||||||
|
|
||||||
def eval(self, context):
|
def eval(self, context):
|
||||||
try:
|
try:
|
||||||
return func(self.first.eval(context), self.second.eval(context))
|
return func(context, self.first, self.second)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Templates shouldn't throw exceptions when rendering. We are
|
# Templates shouldn't throw exceptions when rendering. We are
|
||||||
# most likely to get exceptions for things like {% if foo in bar
|
# most likely to get exceptions for things like {% if foo in bar
|
||||||
|
@ -81,7 +81,7 @@ def prefix(bp, func):
|
||||||
|
|
||||||
def eval(self, context):
|
def eval(self, context):
|
||||||
try:
|
try:
|
||||||
return func(self.first.eval(context))
|
return func(context, self.first)
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -91,20 +91,21 @@ def prefix(bp, func):
|
||||||
# Operator precedence follows Python.
|
# Operator precedence follows Python.
|
||||||
# NB - we can get slightly more accurate syntax error messages by not using the
|
# NB - we can get slightly more accurate syntax error messages by not using the
|
||||||
# same object for '==' and '='.
|
# same object for '==' and '='.
|
||||||
|
# We defer variable evaluation to the lambda to ensure that terms are
|
||||||
|
# lazily evaluated using Python's boolean parsing logic.
|
||||||
OPERATORS = {
|
OPERATORS = {
|
||||||
'or': infix(6, lambda x, y: x or y),
|
'or': infix(6, lambda context, x, y: x.eval(context) or y.eval(context)),
|
||||||
'and': infix(7, lambda x, y: x and y),
|
'and': infix(7, lambda context, x, y: x.eval(context) and y.eval(context)),
|
||||||
'not': prefix(8, operator.not_),
|
'not': prefix(8, lambda context, x: not x.eval(context)),
|
||||||
'in': infix(9, lambda x, y: x in y),
|
'in': infix(9, lambda context, x, y: x.eval(context) in y.eval(context)),
|
||||||
'not in': infix(9, lambda x, y: x not in y),
|
'not in': infix(9, lambda context, x, y: x.eval(context) not in y.eval(context)),
|
||||||
'=': infix(10, operator.eq),
|
'=': infix(10, lambda context, x, y: x.eval(context) == y.eval(context)),
|
||||||
'==': infix(10, operator.eq),
|
'==': infix(10, lambda context, x, y: x.eval(context) == y.eval(context)),
|
||||||
'!=': infix(10, operator.ne),
|
'!=': infix(10, lambda context, x, y: x.eval(context) != y.eval(context)),
|
||||||
'>': infix(10, operator.gt),
|
'>': infix(10, lambda context, x, y: x.eval(context) > y.eval(context)),
|
||||||
'>=': infix(10, operator.ge),
|
'>=': infix(10, lambda context, x, y: x.eval(context) >= y.eval(context)),
|
||||||
'<': infix(10, operator.lt),
|
'<': infix(10, lambda context, x, y: x.eval(context) < y.eval(context)),
|
||||||
'<=': infix(10, operator.le),
|
'<=': infix(10, lambda context, x, y: x.eval(context) <= y.eval(context)),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Assign 'id' to each:
|
# Assign 'id' to each:
|
||||||
|
|
|
@ -7,6 +7,7 @@ if __name__ == '__main__':
|
||||||
settings.configure()
|
settings.configure()
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import time
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
@ -97,6 +98,17 @@ class OtherClass:
|
||||||
def method(self):
|
def method(self):
|
||||||
return "OtherClass.method"
|
return "OtherClass.method"
|
||||||
|
|
||||||
|
class TestObj(object):
|
||||||
|
def is_true(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def is_false(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_bad(self):
|
||||||
|
time.sleep(0.3)
|
||||||
|
return True
|
||||||
|
|
||||||
class SilentGetItemClass(object):
|
class SilentGetItemClass(object):
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
raise SomeException
|
raise SomeException
|
||||||
|
@ -342,6 +354,11 @@ class Templates(unittest.TestCase):
|
||||||
old_invalid = settings.TEMPLATE_STRING_IF_INVALID
|
old_invalid = settings.TEMPLATE_STRING_IF_INVALID
|
||||||
expected_invalid_str = 'INVALID'
|
expected_invalid_str = 'INVALID'
|
||||||
|
|
||||||
|
# Warm the URL reversing cache. This ensures we don't pay the cost
|
||||||
|
# warming the cache during one of the tests.
|
||||||
|
urlresolvers.reverse('regressiontests.templates.views.client_action',
|
||||||
|
kwargs={'id':0,'action':"update"})
|
||||||
|
|
||||||
for name, vals in tests:
|
for name, vals in tests:
|
||||||
if isinstance(vals[2], tuple):
|
if isinstance(vals[2], tuple):
|
||||||
normal_string_result = vals[2][0]
|
normal_string_result = vals[2][0]
|
||||||
|
@ -367,9 +384,14 @@ class Templates(unittest.TestCase):
|
||||||
start = datetime.now()
|
start = datetime.now()
|
||||||
test_template = loader.get_template(name)
|
test_template = loader.get_template(name)
|
||||||
end = datetime.now()
|
end = datetime.now()
|
||||||
output = self.render(test_template, vals)
|
|
||||||
if end-start > timedelta(seconds=0.2):
|
if end-start > timedelta(seconds=0.2):
|
||||||
failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Took too long to parse test" % (is_cached, invalid_str, name))
|
failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Took too long to parse test" % (is_cached, invalid_str, name))
|
||||||
|
|
||||||
|
start = datetime.now()
|
||||||
|
output = self.render(test_template, vals)
|
||||||
|
end = datetime.now()
|
||||||
|
if end-start > timedelta(seconds=0.2):
|
||||||
|
failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Took too long to render test" % (is_cached, invalid_str, name))
|
||||||
except ContextStackException:
|
except ContextStackException:
|
||||||
failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Context stack was left imbalanced" % (is_cached, invalid_str, name))
|
failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Context stack was left imbalanced" % (is_cached, invalid_str, name))
|
||||||
continue
|
continue
|
||||||
|
@ -782,6 +804,13 @@ class Templates(unittest.TestCase):
|
||||||
'if-tag-error11': ("{% if 1 == %}yes{% endif %}", {}, template.TemplateSyntaxError),
|
'if-tag-error11': ("{% if 1 == %}yes{% endif %}", {}, template.TemplateSyntaxError),
|
||||||
'if-tag-error12': ("{% if a not b %}yes{% endif %}", {}, template.TemplateSyntaxError),
|
'if-tag-error12': ("{% if a not b %}yes{% endif %}", {}, template.TemplateSyntaxError),
|
||||||
|
|
||||||
|
# If evaluations are shortcircuited where possible
|
||||||
|
# These tests will fail by taking too long to run. When the if clause
|
||||||
|
# is shortcircuiting correctly, the is_bad() function shouldn't be
|
||||||
|
# evaluated, and the deliberate sleep won't happen.
|
||||||
|
'if-tag-shortcircuit01': ('{% if x.is_true or x.is_bad %}yes{% else %}no{% endif %}', {'x': TestObj()}, "yes"),
|
||||||
|
'if-tag-shortcircuit02': ('{% if x.is_false and x.is_bad %}yes{% else %}no{% endif %}', {'x': TestObj()}, "no"),
|
||||||
|
|
||||||
# Non-existent args
|
# Non-existent args
|
||||||
'if-tag-badarg01':("{% if x|default_if_none:y %}yes{% endif %}", {}, ''),
|
'if-tag-badarg01':("{% if x|default_if_none:y %}yes{% endif %}", {}, ''),
|
||||||
'if-tag-badarg02':("{% if x|default_if_none:y %}yes{% endif %}", {'y': 0}, ''),
|
'if-tag-badarg02':("{% if x|default_if_none:y %}yes{% endif %}", {'y': 0}, ''),
|
||||||
|
|
Loading…
Reference in New Issue