From 2570954a9aa8ae8ad1edb6927f80b5952dfe5674 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Fri, 21 Sep 2007 04:00:32 +0000 Subject: [PATCH] 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 --- .../admin/templatetags/admin_modify.py | 8 +- .../contrib/comments/templatetags/comments.py | 12 +- django/template/__init__.py | 150 ++++++++++++------ django/template/defaultfilters.py | 8 +- django/template/defaulttags.py | 18 +-- django/template/loader_tags.py | 6 +- django/templatetags/i18n.py | 6 +- docs/templates_python.txt | 34 +++- 8 files changed, 168 insertions(+), 74 deletions(-) diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index 5f29782903..9ce6aa77ae 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/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 @@ class FieldWidgetNode(template.Node): 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 @@ class StackedBoundRelatedObject(BoundRelatedObject): 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 diff --git a/django/contrib/comments/templatetags/comments.py b/django/contrib/comments/templatetags/comments.py index 1d4628978d..959cec4c7f 100644 --- a/django/contrib/comments/templatetags/comments.py +++ b/django/contrib/comments/templatetags/comments.py @@ -19,6 +19,8 @@ class CommentFormNode(template.Node): 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 @@ class CommentFormNode(template.Node): 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 @@ class CommentFormNode(template.Node): 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 @@ class CommentCountNode(template.Node): 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 @@ class CommentCountNode(template.Node): 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 @@ class CommentListNode(template.Node): 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 = { diff --git a/django/template/__init__.py b/django/template/__init__.py index 449e0d0c28..1cfd85be06 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -88,8 +88,6 @@ UNKNOWN_SOURCE="<unknown source>" 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 @@ class FilterExpression(object): 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 @@ class FilterExpression(object): 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 @@ class FilterExpression(object): 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). + the given context. + + Deprecated; use the Variable class instead. + """ + return Variable(path).resolve(context) - >>> 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' +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 @@ class Library(object): 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 @@ class Library(object): 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: diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index 1fd6d02c70..f1fcf5fe90 100644 --- a/django/template/defaultfilters.py +++ b/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] diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index e23295f732..151985bfdd 100644 --- a/django/template/defaulttags.py +++ b/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 @@ class CycleNode(Node): 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 @@ class FilterNode(Node): 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 @@ class IfChangedNode(Node): 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 @@ class IfChangedNode(Node): 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 @@ class IfEqualNode(Node): 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): diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py index 19f368711c..652fda11ce 100644 --- a/django/template/loader_tags.py +++ b/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 @@ class ConstantIncludeNode(Node): 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: diff --git a/django/templatetags/i18n.py b/django/templatetags/i18n.py index 1e85c6b5d1..d5b0741a61 100644 --- a/django/templatetags/i18n.py +++ b/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 @@ class GetCurrentLanguageBidiNode(Node): 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: diff --git a/docs/templates_python.txt b/docs/templates_python.txt index 232f54061f..b6173ad39a 100644 --- a/docs/templates_python.txt +++ b/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 ~~~~~~~~~~~~~~~~~~~~~~~~