Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Fixed #3453: introduced a new template variable resolution system by …

…Brian Harring (thanks!). The upshot is that variable resolution is about 25% faster, and you should see a measurable performance increase any time you've got long or deeply nested loops.

Variable resolution has changed behind the scenes -- see the note in templates_python.txt -- but template.resolve_variable() still exists. This should be fully backwards-compatible.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@6399 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 2570954a9aa8ae8ad1edb6927f80b5952dfe5674 1 parent 75e4620
@jacobian jacobian authored
View
8 django/contrib/admin/templatetags/admin_modify.py
@@ -72,7 +72,7 @@ class FieldWidgetNode(template.Node):
default = None
def __init__(self, bound_field_var):
- self.bound_field_var = bound_field_var
+ self.bound_field_var = template.Variable(bound_field_var)
def get_nodelist(cls, klass):
if klass not in cls.nodelists:
@@ -96,7 +96,7 @@ def get_nodelist(cls, klass):
get_nodelist = classmethod(get_nodelist)
def render(self, context):
- bound_field = template.resolve_variable(self.bound_field_var, context)
+ bound_field = self.bound_field_var.resolve(context)
context.push()
context['bound_field'] = bound_field
@@ -156,10 +156,10 @@ def template_name(self):
class EditInlineNode(template.Node):
def __init__(self, rel_var):
- self.rel_var = rel_var
+ self.rel_var = template.Variable(rel_var)
def render(self, context):
- relation = template.resolve_variable(self.rel_var, context)
+ relation = self.rel_var.resolve(context)
context.push()
if relation.field.rel.edit_inline == models.TABULAR:
bound_related_object_class = TabularBoundRelatedObject
View
12 django/contrib/comments/templatetags/comments.py
@@ -19,6 +19,8 @@ def __init__(self, content_type, obj_id_lookup_var, obj_id, free,
ratings_optional=False, ratings_required=False, rating_options='',
is_public=True):
self.content_type = content_type
+ if obj_id_lookup_var is not None:
+ obj_id_lookup_var = template.Variable(obj_id_lookup_var)
self.obj_id_lookup_var, self.obj_id, self.free = obj_id_lookup_var, obj_id, free
self.photos_optional, self.photos_required = photos_optional, photos_required
self.ratings_optional, self.ratings_required = ratings_optional, ratings_required
@@ -32,7 +34,7 @@ def render(self, context):
context.push()
if self.obj_id_lookup_var is not None:
try:
- self.obj_id = template.resolve_variable(self.obj_id_lookup_var, context)
+ self.obj_id = self.obj_id_lookup_var.resolve(context)
except template.VariableDoesNotExist:
return ''
# Validate that this object ID is valid for this content-type.
@@ -75,6 +77,8 @@ def render(self, context):
class CommentCountNode(template.Node):
def __init__(self, package, module, context_var_name, obj_id, var_name, free):
self.package, self.module = package, module
+ if context_var_name is not None:
+ context_var_name = template.Variable(context_var_name)
self.context_var_name, self.obj_id = context_var_name, obj_id
self.var_name, self.free = var_name, free
@@ -82,7 +86,7 @@ def render(self, context):
from django.conf import settings
manager = self.free and FreeComment.objects or Comment.objects
if self.context_var_name is not None:
- self.obj_id = template.resolve_variable(self.context_var_name, context)
+ self.obj_id = self.context_var_name.resolve(context)
comment_count = manager.filter(object_id__exact=self.obj_id,
content_type__app_label__exact=self.package,
content_type__model__exact=self.module, site__id__exact=settings.SITE_ID).count()
@@ -92,6 +96,8 @@ def render(self, context):
class CommentListNode(template.Node):
def __init__(self, package, module, context_var_name, obj_id, var_name, free, ordering, extra_kwargs=None):
self.package, self.module = package, module
+ if context_var_name is not None:
+ context_var_name = template.Variable(context_var_name)
self.context_var_name, self.obj_id = context_var_name, obj_id
self.var_name, self.free = var_name, free
self.ordering = ordering
@@ -102,7 +108,7 @@ def render(self, context):
get_list_function = self.free and FreeComment.objects.filter or Comment.objects.get_list_with_karma
if self.context_var_name is not None:
try:
- self.obj_id = template.resolve_variable(self.context_var_name, context)
+ self.obj_id = self.context_var_name.resolve(context)
except template.VariableDoesNotExist:
return ''
kwargs = {
View
152 django/template/__init__.py
@@ -88,8 +88,6 @@
tag_re = re.compile('(%s.*?%s|%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END),
re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END),
re.escape(COMMENT_TAG_START), re.escape(COMMENT_TAG_END)))
-# matches if the string is valid number
-number_re = re.compile(r'[-+]?(\d+|\d*\.\d+)$')
# global dictionary of libraries that have been loaded using get_library
libraries = {}
@@ -564,18 +562,19 @@ def __init__(self, token, parser):
elif constant_arg is not None:
args.append((False, constant_arg.replace(r'\"', '"')))
elif var_arg:
- args.append((True, var_arg))
+ args.append((True, Variable(var_arg)))
filter_func = parser.find_filter(filter_name)
self.args_check(filter_name,filter_func, args)
filters.append( (filter_func,args))
upto = match.end()
if upto != len(token):
raise TemplateSyntaxError, "Could not parse the remainder: '%s' from '%s'" % (token[upto:], token)
- self.var, self.filters = var, filters
+ self.filters = filters
+ self.var = Variable(var)
def resolve(self, context, ignore_failures=False):
try:
- obj = resolve_variable(self.var, context)
+ obj = self.var.resolve(context)
except VariableDoesNotExist:
if ignore_failures:
obj = None
@@ -595,7 +594,7 @@ def resolve(self, context, ignore_failures=False):
if not lookup:
arg_vals.append(arg)
else:
- arg_vals.append(resolve_variable(arg, context))
+ arg_vals.append(arg.resolve(context))
obj = func(obj, *arg_vals)
return obj
@@ -637,37 +636,98 @@ def __str__(self):
def resolve_variable(path, context):
"""
Returns the resolved variable, which may contain attribute syntax, within
- the given context. The variable may be a hard-coded string (if it begins
- and ends with single or double quote marks).
-
- >>> c = {'article': {'section':'News'}}
- >>> resolve_variable('article.section', c)
- u'News'
- >>> resolve_variable('article', c)
- {'section': 'News'}
- >>> class AClass: pass
- >>> c = AClass()
- >>> c.article = AClass()
- >>> c.article.section = 'News'
- >>> resolve_variable('article.section', c)
- u'News'
+ the given context.
+
+ Deprecated; use the Variable class instead.
+ """
+ return Variable(path).resolve(context)
+
+class Variable(object):
+ """
+ A template variable, resolvable against a given context. The variable may be
+ a hard-coded string (if it begins and ends with single or double quote
+ marks)::
+
+ >>> c = {'article': {'section':'News'}}
+ >>> Variable('article.section').resolve(c)
+ u'News'
+ >>> Variable('article').resolve(c)
+ {'section': 'News'}
+ >>> class AClass: pass
+ >>> c = AClass()
+ >>> c.article = AClass()
+ >>> c.article.section = 'News'
+ >>> Variable('article.section').resolve(c)
+ u'News'
(The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.')
"""
- if number_re.match(path):
- number_type = '.' in path and float or int
- current = number_type(path)
- elif path[0] in ('"', "'") and path[0] == path[-1]:
- current = path[1:-1]
- else:
+
+ def __init__(self, var):
+ self.var = var
+ self.literal = None
+ self.lookups = None
+
+ try:
+ # First try to treat this variable as a number.
+ #
+ # Note that this could cause an OverflowError here that we're not
+ # catching. Since this should only happen at compile time, that's
+ # probably OK.
+ self.literal = float(var)
+
+ # So it's a float... is it an int? If the original value contained a
+ # dot or an "e" then it was a float, not an int.
+ if '.' not in var and 'e' not in var.lower():
+ self.literal = int(self.literal)
+
+ # "2." is invalid
+ if var.endswith('.'):
+ raise ValueError
+
+ except ValueError:
+ # A ValueError means that the variable isn't a number.
+ # If it's wrapped with quotes (single or double), then
+ # we're also dealing with a literal.
+ if var[0] in "\"'" and var[0] == var[-1]:
+ self.literal = var[1:-1]
+
+ else:
+ # Otherwise we'll set self.lookups so that resolve() knows we're
+ # dealing with a bonafide variable
+ self.lookups = tuple(var.split(VARIABLE_ATTRIBUTE_SEPARATOR))
+
+ def resolve(self, context):
+ """Resolve this variable against a given context."""
+ if self.lookups is not None:
+ # We're dealing with a variable that needs to be resolved
+ return self._resolve_lookup(context)
+ else:
+ # We're dealing with a literal, so it's already been "resolved"
+ return self.literal
+
+ def __repr__(self):
+ return "<%s: %r>" % (self.__class__.__name__, self.var)
+
+ def __str__(self):
+ return self.var
+
+ def _resolve_lookup(self, context):
+ """
+ Performs resolution of a real variable (i.e. not a literal) against the
+ given context.
+
+ As indicated by the method's name, this method is an implementation
+ detail and shouldn't be called by external code. Use Variable.resolve()
+ instead.
+ """
current = context
- bits = path.split(VARIABLE_ATTRIBUTE_SEPARATOR)
- while bits:
+ for bit in self.lookups:
try: # dictionary lookup
- current = current[bits[0]]
+ current = current[bit]
except (TypeError, AttributeError, KeyError):
try: # attribute lookup
- current = getattr(current, bits[0])
+ current = getattr(current, bit)
if callable(current):
if getattr(current, 'alters_data', False):
current = settings.TEMPLATE_STRING_IF_INVALID
@@ -685,27 +745,27 @@ def resolve_variable(path, context):
raise
except (TypeError, AttributeError):
try: # list-index lookup
- current = current[int(bits[0])]
+ current = current[int(bit)]
except (IndexError, # list index out of range
ValueError, # invalid literal for int()
- KeyError, # current is a dict without `int(bits[0])` key
+ KeyError, # current is a dict without `int(bit)` key
TypeError, # unsubscriptable object
):
- raise VariableDoesNotExist("Failed lookup for key [%s] in %r", (bits[0], current)) # missing attribute
+ raise VariableDoesNotExist("Failed lookup for key [%s] in %r", (bit, current)) # missing attribute
except Exception, e:
if getattr(e, 'silent_variable_failure', False):
current = settings.TEMPLATE_STRING_IF_INVALID
else:
raise
- del bits[0]
- if isinstance(current, (basestring, Promise)):
- try:
- current = force_unicode(current)
- except UnicodeDecodeError:
- # Failing to convert to unicode can happen sometimes (e.g. debug
- # tracebacks). So we allow it in this particular instance.
- pass
- return current
+
+ if isinstance(current, (basestring, Promise)):
+ try:
+ current = force_unicode(current)
+ except UnicodeDecodeError:
+ # Failing to convert to unicode can happen sometimes (e.g. debug
+ # tracebacks). So we allow it in this particular instance.
+ pass
+ return current
class Node(object):
def render(self, context):
@@ -861,10 +921,10 @@ def simple_tag(self,func):
class SimpleNode(Node):
def __init__(self, vars_to_resolve):
- self.vars_to_resolve = vars_to_resolve
+ self.vars_to_resolve = map(Variable, vars_to_resolve)
def render(self, context):
- resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve]
+ resolved_vars = [var.resolve(context) for var in self.vars_to_resolve]
return func(*resolved_vars)
compile_func = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, SimpleNode)
@@ -883,10 +943,10 @@ def dec(func):
class InclusionNode(Node):
def __init__(self, vars_to_resolve):
- self.vars_to_resolve = vars_to_resolve
+ self.vars_to_resolve = map(Variable, vars_to_resolve)
def render(self, context):
- resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve]
+ resolved_vars = [var.resolve(context) for var in self.vars_to_resolve]
if takes_context:
args = [context] + resolved_vars
else:
View
8 django/template/defaultfilters.py
@@ -1,6 +1,6 @@
"Default variable filters"
-from django.template import resolve_variable, Library
+from django.template import Variable, Library
from django.conf import settings
from django.utils.translation import ugettext, ungettext
from django.utils.encoding import force_unicode, smart_str, iri_to_uri
@@ -297,7 +297,8 @@ def dictsort(value, arg):
Takes a list of dicts, returns that list sorted by the property given in
the argument.
"""
- decorated = [(resolve_variable(u'var.' + arg, {u'var' : item}), item) for item in value]
+ var_resolve = Variable(arg).resolve
+ decorated = [(var_resolve(item), item) for item in value]
decorated.sort()
return [item[1] for item in decorated]
@@ -306,7 +307,8 @@ def dictsortreversed(value, arg):
Takes a list of dicts, returns that list sorted in reverse order by the
property given in the argument.
"""
- decorated = [(resolve_variable(u'var.' + arg, {u'var' : item}), item) for item in value]
+ var_resolve = Variable(arg).resolve
+ decorated = [(var_resolve(item), item) for item in value]
decorated.sort()
decorated.reverse()
return [item[1] for item in decorated]
View
18 django/template/defaulttags.py
@@ -1,6 +1,6 @@
"Default tags used by the template system, available to all templates."
-from django.template import Node, NodeList, Template, Context, resolve_variable
+from django.template import Node, NodeList, Template, Context, Variable
from django.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END, SINGLE_BRACE_START, SINGLE_BRACE_END, COMMENT_TAG_START, COMMENT_TAG_END
from django.template import get_library, Library, InvalidTemplateLibrary
from django.conf import settings
@@ -30,7 +30,7 @@ def __init__(self, cyclevars, variable_name=None):
def render(self, context):
self.counter += 1
value = self.cyclevars[self.counter % self.cyclevars_len]
- value = resolve_variable(value, context)
+ value = Variable(value).resolve(context)
if self.variable_name:
context[self.variable_name] = value
return value
@@ -57,12 +57,12 @@ def render(self, context):
class FirstOfNode(Node):
def __init__(self, vars):
- self.vars = vars
+ self.vars = map(Variable, vars)
def render(self, context):
for var in self.vars:
try:
- value = resolve_variable(var, context)
+ value = var.resolve(context)
except VariableDoesNotExist:
continue
if value:
@@ -147,7 +147,7 @@ class IfChangedNode(Node):
def __init__(self, nodelist, *varlist):
self.nodelist = nodelist
self._last_seen = None
- self._varlist = varlist
+ self._varlist = map(Variable, varlist)
def render(self, context):
if 'forloop' in context and context['forloop']['first']:
@@ -156,7 +156,7 @@ def render(self, context):
if self._varlist:
# Consider multiple parameters.
# This automatically behaves like a OR evaluation of the multiple variables.
- compare_to = [resolve_variable(var, context) for var in self._varlist]
+ compare_to = [var.resolve(context) for var in self._varlist]
else:
compare_to = self.nodelist.render(context)
except VariableDoesNotExist:
@@ -175,7 +175,7 @@ def render(self, context):
class IfEqualNode(Node):
def __init__(self, var1, var2, nodelist_true, nodelist_false, negate):
- self.var1, self.var2 = var1, var2
+ self.var1, self.var2 = Variable(var1), Variable(var2)
self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
self.negate = negate
@@ -184,11 +184,11 @@ def __repr__(self):
def render(self, context):
try:
- val1 = resolve_variable(self.var1, context)
+ val1 = self.var1.resolve(context)
except VariableDoesNotExist:
val1 = None
try:
- val2 = resolve_variable(self.var2, context)
+ val2 = self.var2.resolve(context)
except VariableDoesNotExist:
val2 = None
if (self.negate and val1 != val2) or (not self.negate and val1 == val2):
View
6 django/template/loader_tags.py
@@ -1,4 +1,4 @@
-from django.template import TemplateSyntaxError, TemplateDoesNotExist, resolve_variable
+from django.template import TemplateSyntaxError, TemplateDoesNotExist, Variable
from django.template import Library, Node
from django.template.loader import get_template, get_template_from_string, find_template_source
from django.conf import settings
@@ -99,11 +99,11 @@ def render(self, context):
class IncludeNode(Node):
def __init__(self, template_name):
- self.template_name = template_name
+ self.template_name = Variable(template_name)
def render(self, context):
try:
- template_name = resolve_variable(self.template_name, context)
+ template_name = self.template_name.resolve(context)
t = get_template(template_name)
return t.render(context)
except TemplateSyntaxError, e:
View
6 django/templatetags/i18n.py
@@ -1,4 +1,4 @@
-from django.template import Node, resolve_variable
+from django.template import Node, Variable
from django.template import TemplateSyntaxError, TokenParser, Library
from django.template import TOKEN_TEXT, TOKEN_VAR
from django.utils import translation
@@ -32,11 +32,11 @@ def render(self, context):
class TranslateNode(Node):
def __init__(self, value, noop):
- self.value = value
+ self.value = Variable(value)
self.noop = noop
def render(self, context):
- value = resolve_variable(self.value, context)
+ value = self.value.resolve(context)
if self.noop:
return value
else:
View
34 docs/templates_python.txt
@@ -928,10 +928,36 @@ current context, available in the ``render`` method::
``resolve_variable`` will try to resolve ``blog_entry.date_updated`` and then
format it accordingly.
-.. note::
- The ``resolve_variable()`` function will throw a ``VariableDoesNotExist``
- exception if it cannot resolve the string passed to it in the current
- context of the page.
+.. admonition:: New in development version:
+
+ Variable resolution has changed in the development version of Django.
+ ``template.resolve_variable()`` is still available, but has been deprecated
+ in favor of a new ``template.Variable`` class. Using this class will usually
+ be more efficient than calling ``template.resolve_variable``
+
+ To use the ``Variable`` class, simply instantiate it with the name of the
+ variable to be resolved, and then call ``variable.resolve(context)``. So,
+ in the development version, the above example would be more correctly
+ written as:
+
+ .. parsed-literal::
+
+ class FormatTimeNode(template.Node):
+ def __init__(self, date_to_be_formatted, format_string):
+ self.date_to_be_formatted = **Variable(date_to_be_formatted)**
+ self.format_string = format_string
+
+ def render(self, context):
+ try:
+ actual_date = **self.date_to_be_formatted.resolve(context)**
+ return actual_date.strftime(self.format_string)
+ except template.VariableDoesNotExist:
+ return ''
+
+ Changes are highlighted in bold.
+
+Variable resolution will throw a ``VariableDoesNotExist`` exception if it cannot
+resolve the string passed to it in the current context of the page.
Shortcut for simple tags
~~~~~~~~~~~~~~~~~~~~~~~~
Please sign in to comment.
Something went wrong with that request. Please try again.