diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index f9cad005d5..b8836caa5a 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -94,15 +94,15 @@ class FieldWidgetNode(template.Node): return cls.nodelists[klass] get_nodelist = classmethod(get_nodelist) - def render(self, context): + def iter_render(self, context): bound_field = template.resolve_variable(self.bound_field_var, context) context.push() context['bound_field'] = bound_field - output = self.get_nodelist(bound_field.field.__class__).render(context) + for chunk in self.get_nodelist(bound_field.field.__class__).iter_render(context): + yield chunk context.pop() - return output class FieldWrapper(object): def __init__(self, field ): @@ -157,7 +157,7 @@ class EditInlineNode(template.Node): def __init__(self, rel_var): self.rel_var = rel_var - def render(self, context): + def iter_render(self, context): relation = template.resolve_variable(self.rel_var, context) context.push() if relation.field.rel.edit_inline == models.TABULAR: @@ -169,10 +169,9 @@ class EditInlineNode(template.Node): original = context.get('original', None) bound_related_object = relation.bind(context['form'], original, bound_related_object_class) context['bound_related_object'] = bound_related_object - t = loader.get_template(bound_related_object.template_name()) - output = t.render(context) + for chunk in loader.get_template(bound_related_object.template_name()).iter_render(context): + yield chunk context.pop() - return output def output_all(form_fields): return ''.join([str(f) for f in form_fields]) diff --git a/django/contrib/admin/templatetags/adminapplist.py b/django/contrib/admin/templatetags/adminapplist.py index 10e09ca0b6..53455d6c74 100644 --- a/django/contrib/admin/templatetags/adminapplist.py +++ b/django/contrib/admin/templatetags/adminapplist.py @@ -7,7 +7,7 @@ class AdminApplistNode(template.Node): def __init__(self, varname): self.varname = varname - def render(self, context): + def iter_render(self, context): from django.db import models from django.utils.text import capfirst app_list = [] @@ -54,7 +54,7 @@ class AdminApplistNode(template.Node): 'models': model_list, }) context[self.varname] = app_list - return '' + return () def get_admin_app_list(parser, token): """ diff --git a/django/contrib/admin/templatetags/log.py b/django/contrib/admin/templatetags/log.py index 8d52d2e944..96db2373b4 100644 --- a/django/contrib/admin/templatetags/log.py +++ b/django/contrib/admin/templatetags/log.py @@ -10,14 +10,14 @@ class AdminLogNode(template.Node): def __repr__(self): return "" - def render(self, context): + def iter_render(self, context): if self.user is None: context[self.varname] = LogEntry.objects.all().select_related()[:self.limit] else: if not self.user.isdigit(): self.user = context[self.user].id context[self.varname] = LogEntry.objects.filter(user__id__exact=self.user).select_related()[:self.limit] - return '' + return () class DoGetAdminLog: """ diff --git a/django/contrib/comments/templatetags/comments.py b/django/contrib/comments/templatetags/comments.py index 5c02c16f95..a43b11f452 100644 --- a/django/contrib/comments/templatetags/comments.py +++ b/django/contrib/comments/templatetags/comments.py @@ -24,7 +24,7 @@ class CommentFormNode(template.Node): self.photo_options, self.rating_options = photo_options, rating_options self.is_public = is_public - def render(self, context): + def iter_render(self, context): from django.conf import settings from django.utils.text import normalize_newlines import base64 @@ -33,7 +33,7 @@ class CommentFormNode(template.Node): try: self.obj_id = template.resolve_variable(self.obj_id_lookup_var, context) except template.VariableDoesNotExist: - return '' + return # Validate that this object ID is valid for this content-type. # We only have to do this validation if obj_id_lookup_var is provided, # because do_comment_form() validates hard-coded object IDs. @@ -67,9 +67,9 @@ class CommentFormNode(template.Node): context['hash'] = Comment.objects.get_security_hash(context['options'], context['photo_options'], context['rating_options'], context['target']) context['logout_url'] = settings.LOGOUT_URL default_form = loader.get_template(COMMENT_FORM) - output = default_form.render(context) + for chunk in default_form.iter_render(context): + yield chunk context.pop() - return output class CommentCountNode(template.Node): def __init__(self, package, module, context_var_name, obj_id, var_name, free): @@ -77,7 +77,7 @@ class CommentCountNode(template.Node): self.context_var_name, self.obj_id = context_var_name, obj_id self.var_name, self.free = var_name, free - def render(self, context): + def iter_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: @@ -86,7 +86,7 @@ class CommentCountNode(template.Node): content_type__app_label__exact=self.package, content_type__model__exact=self.module, site__id__exact=settings.SITE_ID).count() context[self.var_name] = comment_count - return '' + return () class CommentListNode(template.Node): def __init__(self, package, module, context_var_name, obj_id, var_name, free, ordering, extra_kwargs=None): @@ -96,14 +96,14 @@ class CommentListNode(template.Node): self.ordering = ordering self.extra_kwargs = extra_kwargs or {} - def render(self, context): + def iter_render(self, context): from django.conf import settings 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) except template.VariableDoesNotExist: - return '' + return () kwargs = { 'object_id__exact': self.obj_id, 'content_type__app_label__exact': self.package, @@ -127,7 +127,7 @@ class CommentListNode(template.Node): comment_list = [c for c in comment_list if not c.is_hidden or (user_id == c.user_id)] context[self.var_name] = comment_list - return '' + return () class DoCommentForm: """ diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py index 80a0bf6a91..9e603b42d4 100644 --- a/django/core/servers/basehttp.py +++ b/django/core/servers/basehttp.py @@ -309,7 +309,7 @@ class ServerHandler(object): """ if not self.result_is_file() and not self.sendfile(): for data in self.result: - self.write(data) + self.write(data, False) self.finish_content() self.close() @@ -377,7 +377,7 @@ class ServerHandler(object): else: self._write('Status: %s\r\n' % self.status) - def write(self, data): + def write(self, data, flush=True): """'write()' callable as specified by PEP 333""" assert type(data) is StringType,"write() argument must be string" @@ -394,7 +394,8 @@ class ServerHandler(object): # XXX check Content-Length and truncate if too many bytes written? self._write(data) - self._flush() + if flush: + self._flush() def sendfile(self): """Platform-specific file transmission @@ -421,8 +422,6 @@ class ServerHandler(object): if not self.headers_sent: self.headers['Content-Length'] = "0" self.send_headers() - else: - pass # XXX check if content-length was too short? def close(self): try: diff --git a/django/http/__init__.py b/django/http/__init__.py index ca3b5eab24..a8c8afe433 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -222,6 +222,12 @@ class HttpResponse(object): content = ''.join(self._container) if isinstance(content, unicode): content = content.encode(self._charset) + + # If self._container was an iterator, we have just exhausted it, so we + # need to save the results for anything else that needs access + if not self._is_string: + self._container = [content] + self._is_string = True return content def _set_content(self, value): @@ -231,14 +237,10 @@ class HttpResponse(object): content = property(_get_content, _set_content) def __iter__(self): - self._iterator = self._container.__iter__() - return self - - def next(self): - chunk = self._iterator.next() - if isinstance(chunk, unicode): - chunk = chunk.encode(self._charset) - return chunk + for chunk in self._container: + if isinstance(chunk, unicode): + chunk = chunk.encode(self._charset) + yield chunk def close(self): if hasattr(self._container, 'close'): diff --git a/django/oldforms/__init__.py b/django/oldforms/__init__.py index 5814eef7ff..ea1f425ad3 100644 --- a/django/oldforms/__init__.py +++ b/django/oldforms/__init__.py @@ -309,6 +309,10 @@ class FormField(object): return data html2python = staticmethod(html2python) + def iter_render(self, data): + # this even needed? + return (self.render(data),) + def render(self, data): raise NotImplementedError diff --git a/django/shortcuts/__init__.py b/django/shortcuts/__init__.py index 81381d08c1..3a0f6a0091 100644 --- a/django/shortcuts/__init__.py +++ b/django/shortcuts/__init__.py @@ -7,7 +7,7 @@ from django.http import HttpResponse, Http404 from django.db.models.manager import Manager def render_to_response(*args, **kwargs): - return HttpResponse(loader.render_to_string(*args, **kwargs)) + return HttpResponse(loader.render_to_iter(*args, **kwargs)) load_and_render = render_to_response # For backwards compatibility. def get_object_or_404(klass, *args, **kwargs): diff --git a/django/template/__init__.py b/django/template/__init__.py index 4f2ddfc8b3..0d1256c4dc 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -55,6 +55,7 @@ times with multiple contexts) '\n\n\n\n' """ import re +import types from inspect import getargspec from django.conf import settings from django.template.context import Context, RequestContext, ContextPopException @@ -167,9 +168,12 @@ class Template(object): for subnode in node: yield subnode - def render(self, context): + def iter_render(self, context): "Display stage -- can be called many times" - return self.nodelist.render(context) + return self.nodelist.iter_render(context) + + def render(self, context): + return ''.join(self.iter_render(context)) def compile_string(template_string, origin): "Compiles template_string into NodeList ready for rendering" @@ -698,10 +702,26 @@ def resolve_variable(path, context): del bits[0] return current +class NodeBase(type): + def __new__(cls, name, bases, attrs): + """ + Ensures that either a 'render' or 'render_iter' method is defined on + any Node sub-class. This avoids potential infinite loops at runtime. + """ + if not (isinstance(attrs.get('render'), types.FunctionType) or + isinstance(attrs.get('iter_render'), types.FunctionType)): + raise TypeError('Unable to create Node subclass without either "render" or "iter_render" method.') + return type.__new__(cls, name, bases, attrs) + class Node(object): + __metaclass__ = NodeBase + + def iter_render(self, context): + return (self.render(context),) + def render(self, context): "Return the node rendered as a string" - pass + return ''.join(self.iter_render(context)) def __iter__(self): yield self @@ -717,13 +737,12 @@ class Node(object): class NodeList(list): def render(self, context): - bits = [] + return ''.join(self.iter_render(context)) + + def iter_render(self, context): for node in self: - if isinstance(node, Node): - bits.append(self.render_node(node, context)) - else: - bits.append(node) - return ''.join(bits) + for chunk in node.iter_render(context): + yield chunk def get_nodes_by_type(self, nodetype): "Return a list of all nodes of the given type" @@ -732,24 +751,26 @@ class NodeList(list): nodes.extend(node.get_nodes_by_type(nodetype)) return nodes - def render_node(self, node, context): - return(node.render(context)) class DebugNodeList(NodeList): - def render_node(self, node, context): - try: - result = node.render(context) - except TemplateSyntaxError, e: - if not hasattr(e, 'source'): - e.source = node.source - raise - except Exception, e: - from sys import exc_info - wrapped = TemplateSyntaxError('Caught an exception while rendering: %s' % e) - wrapped.source = node.source - wrapped.exc_info = exc_info() - raise wrapped - return result + def iter_render(self, context): + for node in self: + if not isinstance(node, Node): + yield node + continue + try: + for chunk in node.iter_render(context): + yield chunk + except TemplateSyntaxError, e: + if not hasattr(e, 'source'): + e.source = node.source + raise + except Exception, e: + from sys import exc_info + wrapped = TemplateSyntaxError('Caught an exception while rendering: %s' % e) + wrapped.source = node.source + wrapped.exc_info = exc_info() + raise wrapped class TextNode(Node): def __init__(self, s): @@ -758,6 +779,9 @@ class TextNode(Node): def __repr__(self): return "" % self.s[:25] + def iter_render(self, context): + return (self.s,) + def render(self, context): return self.s @@ -781,6 +805,9 @@ class VariableNode(Node): else: return output + def iter_render(self, context): + return (self.render(context),) + def render(self, context): output = self.filter_expression.resolve(context) return self.encode_output(output) @@ -869,6 +896,9 @@ class Library(object): def __init__(self, vars_to_resolve): self.vars_to_resolve = vars_to_resolve + #def iter_render(self, context): + # return (self.render(context),) + def render(self, context): resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve] return func(*resolved_vars) @@ -891,7 +921,7 @@ class Library(object): def __init__(self, vars_to_resolve): self.vars_to_resolve = vars_to_resolve - def render(self, context): + def iter_render(self, context): resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve] if takes_context: args = [context] + resolved_vars @@ -907,7 +937,7 @@ class Library(object): else: t = get_template(file_name) self.nodelist = t.nodelist - return self.nodelist.render(context_class(dict)) + return self.nodelist.iter_render(context_class(dict)) compile_func = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, InclusionNode) compile_func.__doc__ = func.__doc__ diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 6a6665a445..45005fa988 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -14,12 +14,11 @@ if not hasattr(__builtins__, 'reversed'): for index in xrange(len(data)-1, -1, -1): yield data[index] - register = Library() class CommentNode(Node): - def render(self, context): - return '' + def iter_render(self, context): + return () class CycleNode(Node): def __init__(self, cyclevars, variable_name=None): @@ -28,6 +27,9 @@ class CycleNode(Node): self.counter = -1 self.variable_name = variable_name + def iter_render(self, context): + return (self.render(context),) + def render(self, context): self.counter += 1 value = self.cyclevars[self.counter % self.cyclevars_len] @@ -36,29 +38,32 @@ class CycleNode(Node): return value class DebugNode(Node): - def render(self, context): + def iter_render(self, context): from pprint import pformat - output = [pformat(val) for val in context] - output.append('\n\n') - output.append(pformat(sys.modules)) - return ''.join(output) + for val in context: + yield pformat(val) + yield "\n\n" + yield pformat(sys.modules) class FilterNode(Node): def __init__(self, filter_expr, nodelist): self.filter_expr, self.nodelist = filter_expr, nodelist - def render(self, context): + def iter_render(self, context): output = self.nodelist.render(context) # apply filters context.update({'var': output}) filtered = self.filter_expr.resolve(context) context.pop() - return filtered + return (filtered,) class FirstOfNode(Node): def __init__(self, vars): self.vars = vars + def iter_render(self, context): + return (self.render(context),) + def render(self, context): for var in self.vars: try: @@ -94,8 +99,7 @@ class ForNode(Node): nodes.extend(self.nodelist_loop.get_nodes_by_type(nodetype)) return nodes - def render(self, context): - nodelist = NodeList() + def iter_render(self, context): if 'forloop' in context: parentloop = context['forloop'] else: @@ -103,12 +107,12 @@ class ForNode(Node): context.push() try: values = self.sequence.resolve(context, True) + if values is None: + values = () + elif not hasattr(values, '__len__'): + values = list(values) except VariableDoesNotExist: - values = [] - if values is None: - values = [] - if not hasattr(values, '__len__'): - values = list(values) + values = () len_values = len(values) if self.reversed: values = reversed(values) @@ -127,12 +131,17 @@ class ForNode(Node): 'parentloop': parentloop, } if unpack: - # If there are multiple loop variables, unpack the item into them. + # If there are multiple loop variables, unpack the item into + # them. context.update(dict(zip(self.loopvars, item))) else: context[self.loopvars[0]] = item + + # We inline this to avoid the overhead since ForNode is pretty + # common. for node in self.nodelist_loop: - nodelist.append(node.render(context)) + for chunk in node.iter_render(context): + yield chunk if unpack: # The loop variables were pushed on to the context so pop them # off again. This is necessary because the tag lets the length @@ -141,7 +150,6 @@ class ForNode(Node): # context. context.pop() context.pop() - return nodelist.render(context) class IfChangedNode(Node): def __init__(self, nodelist, *varlist): @@ -149,7 +157,7 @@ class IfChangedNode(Node): self._last_seen = None self._varlist = varlist - def render(self, context): + def iter_render(self, context): if 'forloop' in context and context['forloop']['first']: self._last_seen = None try: @@ -167,11 +175,9 @@ class IfChangedNode(Node): self._last_seen = compare_to context.push() context['ifchanged'] = {'firstloop': firstloop} - content = self.nodelist.render(context) + for chunk in self.nodelist.iter_render(context): + yield chunk context.pop() - return content - else: - return '' class IfEqualNode(Node): def __init__(self, var1, var2, nodelist_true, nodelist_false, negate): @@ -182,7 +188,7 @@ class IfEqualNode(Node): def __repr__(self): return "" - def render(self, context): + def iter_render(self, context): try: val1 = resolve_variable(self.var1, context) except VariableDoesNotExist: @@ -192,8 +198,8 @@ class IfEqualNode(Node): except VariableDoesNotExist: val2 = None if (self.negate and val1 != val2) or (not self.negate and val1 == val2): - return self.nodelist_true.render(context) - return self.nodelist_false.render(context) + return self.nodelist_true.iter_render(context) + return self.nodelist_false.iter_render(context) class IfNode(Node): def __init__(self, bool_exprs, nodelist_true, nodelist_false, link_type): @@ -218,7 +224,7 @@ class IfNode(Node): nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype)) return nodes - def render(self, context): + def iter_render(self, context): if self.link_type == IfNode.LinkTypes.or_: for ifnot, bool_expr in self.bool_exprs: try: @@ -226,8 +232,8 @@ class IfNode(Node): except VariableDoesNotExist: value = None if (value and not ifnot) or (ifnot and not value): - return self.nodelist_true.render(context) - return self.nodelist_false.render(context) + return self.nodelist_true.iter_render(context) + return self.nodelist_false.iter_render(context) else: for ifnot, bool_expr in self.bool_exprs: try: @@ -235,8 +241,8 @@ class IfNode(Node): except VariableDoesNotExist: value = None if not ((value and not ifnot) or (ifnot and not value)): - return self.nodelist_false.render(context) - return self.nodelist_true.render(context) + return self.nodelist_false.iter_render(context) + return self.nodelist_true.iter_render(context) class LinkTypes: and_ = 0, @@ -247,11 +253,11 @@ class RegroupNode(Node): self.target, self.expression = target, expression self.var_name = var_name - def render(self, context): + def iter_render(self, context): obj_list = self.target.resolve(context, True) if obj_list == None: # target_var wasn't found in context; fail silently context[self.var_name] = [] - return '' + return () output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]} for obj in obj_list: grouper = self.expression.resolve(obj, True) @@ -261,7 +267,7 @@ class RegroupNode(Node): else: output.append({'grouper': grouper, 'list': [obj]}) context[self.var_name] = output - return '' + return () def include_is_allowed(filepath): for root in settings.ALLOWED_INCLUDE_ROOTS: @@ -273,10 +279,10 @@ class SsiNode(Node): def __init__(self, filepath, parsed): self.filepath, self.parsed = filepath, parsed - def render(self, context): + def iter_render(self, context): if not include_is_allowed(self.filepath): if settings.DEBUG: - return "[Didn't have permission to include file]" + return ("[Didn't have permission to include file]",) else: return '' # Fail silently for invalid includes. try: @@ -287,23 +293,25 @@ class SsiNode(Node): output = '' if self.parsed: try: - t = Template(output, name=self.filepath) - return t.render(context) + return Template(output, name=self.filepath).iter_render(context) except TemplateSyntaxError, e: if settings.DEBUG: return "[Included template had syntax error: %s]" % e else: return '' # Fail silently for invalid included templates. - return output + return (output,) class LoadNode(Node): - def render(self, context): - return '' + def iter_render(self, context): + return () class NowNode(Node): def __init__(self, format_string): self.format_string = format_string + def iter_render(self, context): + return (self.render(context),) + def render(self, context): from datetime import datetime from django.utils.dateformat import DateFormat @@ -332,6 +340,9 @@ class TemplateTagNode(Node): def __init__(self, tagtype): self.tagtype = tagtype + def iter_render(self, context): + return (self.render(context),) + def render(self, context): return self.mapping.get(self.tagtype, '') @@ -341,18 +352,18 @@ class URLNode(Node): self.args = args self.kwargs = kwargs - def render(self, context): + def iter_render(self, context): from django.core.urlresolvers import reverse, NoReverseMatch args = [arg.resolve(context) for arg in self.args] kwargs = dict([(k, v.resolve(context)) for k, v in self.kwargs.items()]) try: - return reverse(self.view_name, args=args, kwargs=kwargs) + return (reverse(self.view_name, args=args, kwargs=kwargs),) except NoReverseMatch: try: project_name = settings.SETTINGS_MODULE.split('.')[0] return reverse(project_name + '.' + self.view_name, args=args, kwargs=kwargs) except NoReverseMatch: - return '' + return () class WidthRatioNode(Node): def __init__(self, val_expr, max_expr, max_width): @@ -360,6 +371,9 @@ class WidthRatioNode(Node): self.max_expr = max_expr self.max_width = max_width + def iter_render(self, context): + return (self.render(context),) + def render(self, context): try: value = self.val_expr.resolve(context) @@ -383,13 +397,13 @@ class WithNode(Node): def __repr__(self): return "" - def render(self, context): + def iter_render(self, context): val = self.var.resolve(context) context.push() context[self.name] = val - output = self.nodelist.render(context) + for chunk in self.nodelist.iter_render(context): + yield chunk context.pop() - return output #@register.tag def comment(parser, token): diff --git a/django/template/loader.py b/django/template/loader.py index 03e6f8d49d..45cf5a9d7c 100644 --- a/django/template/loader.py +++ b/django/template/loader.py @@ -87,14 +87,12 @@ def get_template_from_string(source, origin=None, name=None): """ return Template(source, origin, name) -def render_to_string(template_name, dictionary=None, context_instance=None): +def _render_setup(template_name, dictionary=None, context_instance=None): """ - Loads the given template_name and renders it with the given dictionary as - context. The template_name may be a string to load a single template using - get_template, or it may be a tuple to use select_template to find one of - the templates in the list. Returns a string. + Common setup code for render_to_string and render_to_iter. """ - dictionary = dictionary or {} + if dictionary is None: + dictionary = {} if isinstance(template_name, (list, tuple)): t = select_template(template_name) else: @@ -103,7 +101,28 @@ def render_to_string(template_name, dictionary=None, context_instance=None): context_instance.update(dictionary) else: context_instance = Context(dictionary) - return t.render(context_instance) + return t, context_instance + +def render_to_string(template_name, dictionary=None, context_instance=None): + """ + Loads the given template_name and renders it with the given dictionary as + context. The template_name may be a string to load a single template using + get_template, or it may be a tuple to use select_template to find one of + the templates in the list. Returns a string. + """ + t, c = _render_setup(template_name, dictionary=dictionary, context_instance=context_instance) + return t.render(c) + +def render_to_iter(template_name, dictionary=None, context_instance=None): + """ + Loads the given template_name and renders it with the given dictionary as + context. The template_name may be a string to load a single template using + get_template, or it may be a tuple to use select_template to find one of + the templates in the list. Returns a string. + """ + t, c = _render_setup(template_name, dictionary=dictionary, context_instance=context_instance) + return t.iter_render(c) + def select_template(template_name_list): "Given a list of template names, returns the first that can be loaded." diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py index 4439e0b010..d12d0b55ad 100644 --- a/django/template/loader_tags.py +++ b/django/template/loader_tags.py @@ -15,14 +15,14 @@ class BlockNode(Node): def __repr__(self): return "" % (self.name, self.nodelist) - def render(self, context): + def iter_render(self, context): context.push() # Save context in case of block.super(). self.context = context context['block'] = self - result = self.nodelist.render(context) + for chunk in self.nodelist.iter_render(context): + yield chunk context.pop() - return result def super(self): if self.parent: @@ -59,7 +59,7 @@ class ExtendsNode(Node): else: return get_template_from_string(source, origin, parent) - def render(self, context): + def iter_render(self, context): compiled_parent = self.get_parent(context) parent_is_child = isinstance(compiled_parent.nodelist[0], ExtendsNode) parent_blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)]) @@ -79,7 +79,7 @@ class ExtendsNode(Node): parent_block.parent = block_node.parent parent_block.add_parent(parent_block.nodelist) parent_block.nodelist = block_node.nodelist - return compiled_parent.render(context) + return compiled_parent.iter_render(context) class ConstantIncludeNode(Node): def __init__(self, template_path): @@ -91,27 +91,26 @@ class ConstantIncludeNode(Node): raise self.template = None - def render(self, context): + def iter_render(self, context): if self.template: - return self.template.render(context) - else: - return '' + return self.template.iter_render(context) + return () class IncludeNode(Node): def __init__(self, template_name): self.template_name = template_name - def render(self, context): + def iter_render(self, context): try: template_name = resolve_variable(self.template_name, context) t = get_template(template_name) - return t.render(context) + return t.iter_render(context) except TemplateSyntaxError, e: if settings.TEMPLATE_DEBUG: raise - return '' + return () except: - return '' # Fail silently for invalid included templates. + return () # Fail silently for invalid included templates. def do_block(parser, token): """ diff --git a/django/test/utils.py b/django/test/utils.py index f5122fa96d..303a223183 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -11,12 +11,21 @@ from django.template import Template TEST_DATABASE_PREFIX = 'test_' def instrumented_test_render(self, context): - """An instrumented Template render method, providing a signal - that can be intercepted by the test system Client - + """ + An instrumented Template render method, providing a signal that can be + intercepted by the test system Client. """ dispatcher.send(signal=signals.template_rendered, sender=self, template=self, context=context) return self.nodelist.render(context) + +def instrumented_test_iter_render(self, context): + """ + An instrumented Template iter_render method, providing a signal that can be + intercepted by the test system Client. + """ + for chunk in self.nodelist.iter_render(context): + yield chunk + dispatcher.send(signal=signals.template_rendered, sender=self, template=self, context=context) class TestSMTPConnection(object): """A substitute SMTP connection for use during test sessions. @@ -44,7 +53,9 @@ def setup_test_environment(): """ Template.original_render = Template.render + Template.original_iter_render = Template.iter_render Template.render = instrumented_test_render + Template.iter_render = instrumented_test_render mail.original_SMTPConnection = mail.SMTPConnection mail.SMTPConnection = TestSMTPConnection @@ -59,7 +70,8 @@ def teardown_test_environment(): """ Template.render = Template.original_render - del Template.original_render + Template.iter_render = Template.original_iter_render + del Template.original_render, Template.original_iter_render mail.SMTPConnection = mail.original_SMTPConnection del mail.original_SMTPConnection diff --git a/django/views/debug.py b/django/views/debug.py index a534f17b33..75b1a26af9 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -137,7 +137,7 @@ def technical_500_response(request, exc_type, exc_value, tb): 'template_does_not_exist': template_does_not_exist, 'loader_debug_info': loader_debug_info, }) - return HttpResponseServerError(t.render(c), mimetype='text/html') + return HttpResponseServerError(t.iter_render(c), mimetype='text/html') def technical_404_response(request, exception): "Create a technical 404 error response. The exception should be the Http404." @@ -160,7 +160,7 @@ def technical_404_response(request, exception): 'request_protocol': request.is_secure() and "https" or "http", 'settings': get_safe_settings(), }) - return HttpResponseNotFound(t.render(c), mimetype='text/html') + return HttpResponseNotFound(t.iter_render(c), mimetype='text/html') def empty_urlconf(request): "Create an empty URLconf 404 error response." @@ -168,7 +168,7 @@ def empty_urlconf(request): c = Context({ 'project_name': settings.SETTINGS_MODULE.split('.')[0] }) - return HttpResponseNotFound(t.render(c), mimetype='text/html') + return HttpResponseNotFound(t.iter_render(c), mimetype='text/html') def _get_lines_from_file(filename, lineno, context_lines, loader=None, module_name=None): """ diff --git a/django/views/defaults.py b/django/views/defaults.py index 701aebabd6..aea54c963f 100644 --- a/django/views/defaults.py +++ b/django/views/defaults.py @@ -76,7 +76,7 @@ def page_not_found(request, template_name='404.html'): The path of the requested URL (e.g., '/app/pages/bad_page/') """ t = loader.get_template(template_name) # You need to create a 404.html template. - return http.HttpResponseNotFound(t.render(RequestContext(request, {'request_path': request.path}))) + return http.HttpResponseNotFound(t.iter_render(RequestContext(request, {'request_path': request.path}))) def server_error(request, template_name='500.html'): """ @@ -86,4 +86,4 @@ def server_error(request, template_name='500.html'): Context: None """ t = loader.get_template(template_name) # You need to create a 500.html template. - return http.HttpResponseServerError(t.render(Context({}))) + return http.HttpResponseServerError(t.iter_render(Context({}))) diff --git a/django/views/generic/create_update.py b/django/views/generic/create_update.py index 28987f7544..d1b8e34037 100644 --- a/django/views/generic/create_update.py +++ b/django/views/generic/create_update.py @@ -68,7 +68,7 @@ def create_object(request, model, template_name=None, c[key] = value() else: c[key] = value - return HttpResponse(t.render(c)) + return HttpResponse(t.iter_render(c)) def update_object(request, model, object_id=None, slug=None, slug_field=None, template_name=None, template_loader=loader, @@ -141,7 +141,7 @@ def update_object(request, model, object_id=None, slug=None, c[key] = value() else: c[key] = value - response = HttpResponse(t.render(c)) + response = HttpResponse(t.iter_render(c)) populate_xheaders(request, response, model, getattr(object, object._meta.pk.attname)) return response @@ -195,6 +195,6 @@ def delete_object(request, model, post_delete_redirect, c[key] = value() else: c[key] = value - response = HttpResponse(t.render(c)) + response = HttpResponse(t.iter_render(c)) populate_xheaders(request, response, model, getattr(object, object._meta.pk.attname)) return response diff --git a/django/views/generic/date_based.py b/django/views/generic/date_based.py index d13c0293be..d4941388dd 100644 --- a/django/views/generic/date_based.py +++ b/django/views/generic/date_based.py @@ -44,7 +44,7 @@ def archive_index(request, queryset, date_field, num_latest=15, c[key] = value() else: c[key] = value - return HttpResponse(t.render(c), mimetype=mimetype) + return HttpResponse(t.iter_render(c), mimetype=mimetype) def archive_year(request, year, queryset, date_field, template_name=None, template_loader=loader, extra_context=None, allow_empty=False, @@ -92,7 +92,7 @@ def archive_year(request, year, queryset, date_field, template_name=None, c[key] = value() else: c[key] = value - return HttpResponse(t.render(c), mimetype=mimetype) + return HttpResponse(t.iter_render(c), mimetype=mimetype) def archive_month(request, year, month, queryset, date_field, month_format='%b', template_name=None, template_loader=loader, @@ -158,7 +158,7 @@ def archive_month(request, year, month, queryset, date_field, c[key] = value() else: c[key] = value - return HttpResponse(t.render(c), mimetype=mimetype) + return HttpResponse(t.iter_render(c), mimetype=mimetype) def archive_week(request, year, week, queryset, date_field, template_name=None, template_loader=loader, @@ -206,7 +206,7 @@ def archive_week(request, year, week, queryset, date_field, c[key] = value() else: c[key] = value - return HttpResponse(t.render(c), mimetype=mimetype) + return HttpResponse(t.iter_render(c), mimetype=mimetype) def archive_day(request, year, month, day, queryset, date_field, month_format='%b', day_format='%d', template_name=None, @@ -270,7 +270,7 @@ def archive_day(request, year, month, day, queryset, date_field, c[key] = value() else: c[key] = value - return HttpResponse(t.render(c), mimetype=mimetype) + return HttpResponse(t.iter_render(c), mimetype=mimetype) def archive_today(request, **kwargs): """ @@ -339,6 +339,6 @@ def object_detail(request, year, month, day, queryset, date_field, c[key] = value() else: c[key] = value - response = HttpResponse(t.render(c), mimetype=mimetype) + response = HttpResponse(t.iter_render(c), mimetype=mimetype) populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.name)) return response diff --git a/django/views/generic/list_detail.py b/django/views/generic/list_detail.py index 16d55202da..b2a68d61f1 100644 --- a/django/views/generic/list_detail.py +++ b/django/views/generic/list_detail.py @@ -84,7 +84,7 @@ def object_list(request, queryset, paginate_by=None, page=None, model = queryset.model template_name = "%s/%s_list.html" % (model._meta.app_label, model._meta.object_name.lower()) t = template_loader.get_template(template_name) - return HttpResponse(t.render(c), mimetype=mimetype) + return HttpResponse(t.iter_render(c), mimetype=mimetype) def object_detail(request, queryset, object_id=None, slug=None, slug_field=None, template_name=None, template_name_field=None, @@ -126,6 +126,6 @@ def object_detail(request, queryset, object_id=None, slug=None, c[key] = value() else: c[key] = value - response = HttpResponse(t.render(c), mimetype=mimetype) + response = HttpResponse(t.iter_render(c), mimetype=mimetype) populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.name)) return response diff --git a/django/views/generic/simple.py b/django/views/generic/simple.py index 69a494931e..f4afb07aa0 100644 --- a/django/views/generic/simple.py +++ b/django/views/generic/simple.py @@ -15,7 +15,7 @@ def direct_to_template(request, template, extra_context={}, mimetype=None, **kwa dictionary[key] = value c = RequestContext(request, dictionary) t = loader.get_template(template) - return HttpResponse(t.render(c), mimetype=mimetype) + return HttpResponse(t.iter_render(c), mimetype=mimetype) def redirect_to(request, url, **kwargs): """ diff --git a/django/views/static.py b/django/views/static.py index 3ec4ca14a1..1e99c8c50a 100644 --- a/django/views/static.py +++ b/django/views/static.py @@ -92,7 +92,7 @@ def directory_index(path, fullpath): 'directory' : path + '/', 'file_list' : files, }) - return HttpResponse(t.render(c)) + return HttpResponse(t.iter_render(c)) def was_modified_since(header=None, mtime=0, size=0): """ diff --git a/docs/templates_python.txt b/docs/templates_python.txt index f3e2f2c64b..c967df1a49 100644 --- a/docs/templates_python.txt +++ b/docs/templates_python.txt @@ -219,13 +219,13 @@ be replaced with the name of the invalid variable. While ``TEMPLATE_STRING_IF_INVALID`` can be a useful debugging tool, it is a bad idea to turn it on as a 'development default'. - + Many templates, including those in the Admin site, rely upon the silence of the template system when a non-existent variable is encountered. If you assign a value other than ``''`` to ``TEMPLATE_STRING_IF_INVALID``, you will experience rendering problems with these templates and sites. - + Generally, ``TEMPLATE_STRING_IF_INVALID`` should only be enabled in order to debug a specific template problem, then cleared once debugging is complete. @@ -693,14 +693,15 @@ how the compilation works and how the rendering works. When Django compiles a template, it splits the raw template text into ''nodes''. Each node is an instance of ``django.template.Node`` and has -a ``render()`` method. A compiled template is, simply, a list of ``Node`` -objects. When you call ``render()`` on a compiled template object, the template -calls ``render()`` on each ``Node`` in its node list, with the given context. -The results are all concatenated together to form the output of the template. +either a ``render()`` or ``iter_render()`` method. A compiled template is, +simply, a list of ``Node`` objects. When you call ``render()`` on a compiled +template object, the template calls ``render()`` on each ``Node`` in its node +list, with the given context. The results are all concatenated together to +form the output of the template. Thus, to define a custom template tag, you specify how the raw template tag is converted into a ``Node`` (the compilation function), and what the node's -``render()`` method does. +``render()`` or ``iter_render()`` method does. Writing the compilation function ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -770,7 +771,8 @@ Writing the renderer ~~~~~~~~~~~~~~~~~~~~ The second step in writing custom tags is to define a ``Node`` subclass that -has a ``render()`` method. +has a ``render()`` method (we will discuss the ``iter_render()`` alternative +in `Improving rendering speed`_, below). Continuing the above example, we need to define ``CurrentTimeNode``:: @@ -874,7 +876,7 @@ current context, available in the ``render`` method:: def __init__(self, date_to_be_formatted, format_string): self.date_to_be_formatted = date_to_be_formatted self.format_string = format_string - + def render(self, context): try: actual_date = resolve_variable(self.date_to_be_formatted, context) @@ -1175,6 +1177,48 @@ For more examples of complex rendering, see the source code for ``{% if %}``, .. _configuration: +Improving rendering speed +~~~~~~~~~~~~~~~~~~~~~~~~~ + +For most practical purposes, the ``render()`` method on a ``Node`` will be +sufficient and the simplest way to implement a new tag. However, if your +template tag is expected to produce large strings via ``render()``, you can +speed up the rendering process (and reduce memory usage) using iterative +rendering via the ``iter_render()`` method. + +The ``iter_render()`` method should either be an iterator that yields string +chunks, one at a time, or a method that returns a sequence of string chunks. +The template renderer will join the successive chunks together when creating +the final output. The improvement over the ``render()`` method here is that +you do not need to create one large string containing all the output of the +``Node``, instead you can produce the output in smaller chunks. + +By way of example, here's a trivial ``Node`` subclass that simply returns the +contents of a file it is given:: + + class FileNode(Node): + def __init__(self, filename): + self.filename = filename + + def iter_render(self): + for line in file(self.filename): + yield line + +For very large files, the full file contents will never be read entirely into +memory when this tag is used, which is a useful optimisation. + +If you define an ``iter_render()`` method on your ``Node`` subclass, you do +not need to define a ``render()`` method. The reverse is true as well: the +default ``Node.iter_render()`` method will call your ``render()`` method if +necessary. A useful side-effect of this is that you can develop a new tag +using ``render()`` and producing all the output at once, which is easy to +debug. Then you can rewrite the method as an iterator, rename it to +``iter_render()`` and everything will still work. + +It is compulsory, however, to define *either* ``render()`` or ``iter_render()`` +in your subclass. If you omit them both, a ``TypeError`` will be raised when +the code is imported. + Configuring the template system in standalone mode ================================================== @@ -1206,3 +1250,4 @@ is of obvious interest. .. _settings file: ../settings/#using-settings-without-the-django-settings-module-environment-variable .. _settings documentation: ../settings/ +