From bccb8897e6ab0fe8d2e5b9bcb725ac28b1c8e566 Mon Sep 17 00:00:00 2001
From: Malcolm Tredinnick <malcolm.tredinnick@gmail.com>
Date: Sun, 17 Jun 2007 07:11:37 +0000
Subject: [PATCH] Fixed #4565 -- Changed template rendering to use iterators,
 rather than creating large strings, as much as possible. This is all
 backwards compatible. Thanks, Brian Harring.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@5482 bcc190cf-cafb-0310-a4f2-bffc1f526a37
---
 .../admin/templatetags/admin_modify.py        |  13 +-
 .../admin/templatetags/adminapplist.py        |   4 +-
 django/contrib/admin/templatetags/log.py      |   4 +-
 .../contrib/comments/templatetags/comments.py |  18 +--
 django/core/servers/basehttp.py               |   9 +-
 django/http/__init__.py                       |  18 +--
 django/oldforms/__init__.py                   |   4 +
 django/shortcuts/__init__.py                  |   2 +-
 django/template/__init__.py                   |  84 ++++++++-----
 django/template/defaulttags.py                | 112 ++++++++++--------
 django/template/loader.py                     |  33 ++++--
 django/template/loader_tags.py                |  25 ++--
 django/test/utils.py                          |  20 +++-
 django/views/debug.py                         |   6 +-
 django/views/defaults.py                      |   4 +-
 django/views/generic/create_update.py         |   6 +-
 django/views/generic/date_based.py            |  12 +-
 django/views/generic/list_detail.py           |   4 +-
 django/views/generic/simple.py                |   2 +-
 django/views/static.py                        |   2 +-
 docs/templates_python.txt                     |  63 ++++++++--
 21 files changed, 284 insertions(+), 161 deletions(-)

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 "<GetAdminLog Node>"
 
-    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<html>\n\n</html>\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 "<Text Node: '%s'>" % 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 "<IfEqualNode>"
 
-    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 "<WithNode>"
 
-    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 "<Block Node: %s. Contents: %r>" % (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/
+