Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Fixed #13373 -- Ensured that {% if %} statements will short circuit t…

…emplate 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
  • Loading branch information...
commit fef0d25bdccbb8f01314cf3a94651d3cfe67a34e 1 parent ebfe938
@freakboy3742 freakboy3742 authored
Showing with 47 additions and 17 deletions.
  1. +17 −16 django/template/smartif.py
  2. +30 −1 tests/regressiontests/templates/tests.py
View
33 django/template/smartif.py
@@ -56,7 +56,7 @@ def led(self, left, parser):
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 nud(self, parser):
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 eval(self, context):
# 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
View
31 tests/regressiontests/templates/tests.py
@@ -7,6 +7,7 @@
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 @@ def test_template_loader(template_name, template_dirs=None):
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 @@ def test_template_loader(template_name, template_dirs=None):
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 @@ def get_template_tests(self):
'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}, ''),
Please sign in to comment.
Something went wrong with that request. Please try again.