diff --git a/django/template/smartif.py b/django/template/smartif.py index c4e3a06b7c..e835e0ff70 100644 --- a/django/template/smartif.py +++ b/django/template/smartif.py @@ -56,7 +56,7 @@ def infix(bp, func): def eval(self, context): try: - return func(self.first.eval(context), self.second.eval(context)) + return func(context, self.first, self.second) except Exception: # Templates shouldn't throw exceptions when rendering. We are # most likely to get exceptions for things like {% if foo in bar @@ -81,7 +81,7 @@ def prefix(bp, func): def eval(self, context): try: - return func(self.first.eval(context)) + return func(context, self.first) except Exception: return False @@ -91,20 +91,21 @@ def prefix(bp, func): # Operator precedence follows Python. # NB - we can get slightly more accurate syntax error messages by not using the # 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 = { - 'or': infix(6, lambda x, y: x or y), - 'and': infix(7, lambda x, y: x and y), - 'not': prefix(8, operator.not_), - 'in': infix(9, lambda x, y: x in y), - 'not in': infix(9, lambda x, y: x not in y), - '=': infix(10, operator.eq), - '==': infix(10, operator.eq), - '!=': infix(10, operator.ne), - '>': infix(10, operator.gt), - '>=': infix(10, operator.ge), - '<': infix(10, operator.lt), - '<=': infix(10, operator.le), + 'or': infix(6, lambda context, x, y: x.eval(context) or y.eval(context)), + 'and': infix(7, lambda context, x, y: x.eval(context) and y.eval(context)), + 'not': prefix(8, lambda context, x: not x.eval(context)), + 'in': infix(9, lambda context, x, y: x.eval(context) in y.eval(context)), + 'not in': infix(9, lambda context, x, y: x.eval(context) not in y.eval(context)), + '=': infix(10, lambda context, x, y: x.eval(context) == y.eval(context)), + '==': infix(10, lambda context, x, y: x.eval(context) == y.eval(context)), + '!=': infix(10, lambda context, x, y: x.eval(context) != y.eval(context)), + '>': infix(10, lambda context, x, y: x.eval(context) > y.eval(context)), + '>=': infix(10, lambda context, x, y: x.eval(context) >= y.eval(context)), + '<': infix(10, lambda context, x, y: x.eval(context) < y.eval(context)), + '<=': infix(10, lambda context, x, y: x.eval(context) <= y.eval(context)), } # Assign 'id' to each: @@ -151,7 +152,7 @@ class IfParser(object): error_class = ValueError def __init__(self, tokens): - # pre-pass necessary to turn 'not','in' into single token + # pre-pass necessary to turn 'not','in' into single token l = len(tokens) mapped_tokens = [] i = 0 diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index 33d0650c7c..5902e8d5e7 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -7,6 +7,7 @@ if __name__ == '__main__': settings.configure() from datetime import datetime, timedelta +import time import os import sys import traceback @@ -97,6 +98,17 @@ class OtherClass: def method(self): 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): def __getitem__(self, key): raise SomeException @@ -342,6 +354,11 @@ class Templates(unittest.TestCase): old_invalid = settings.TEMPLATE_STRING_IF_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: if isinstance(vals[2], tuple): normal_string_result = vals[2][0] @@ -367,9 +384,14 @@ class Templates(unittest.TestCase): start = datetime.now() test_template = loader.get_template(name) end = datetime.now() - output = self.render(test_template, vals) 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)) + + 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: failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Context stack was left imbalanced" % (is_cached, invalid_str, name)) continue @@ -782,6 +804,13 @@ class Templates(unittest.TestCase): 'if-tag-error11': ("{% if 1 == %}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 'if-tag-badarg01':("{% if x|default_if_none:y %}yes{% endif %}", {}, ''), 'if-tag-badarg02':("{% if x|default_if_none:y %}yes{% endif %}", {'y': 0}, ''),