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
This commit is contained in:
Malcolm Tredinnick 2007-06-17 07:11:37 +00:00
parent 44dd91ec6d
commit bccb8897e6
21 changed files with 284 additions and 161 deletions

View File

@ -94,15 +94,15 @@ class FieldWidgetNode(template.Node):
return cls.nodelists[klass] return cls.nodelists[klass]
get_nodelist = classmethod(get_nodelist) get_nodelist = classmethod(get_nodelist)
def render(self, context): def iter_render(self, context):
bound_field = template.resolve_variable(self.bound_field_var, context) bound_field = template.resolve_variable(self.bound_field_var, context)
context.push() context.push()
context['bound_field'] = bound_field 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() context.pop()
return output
class FieldWrapper(object): class FieldWrapper(object):
def __init__(self, field ): def __init__(self, field ):
@ -157,7 +157,7 @@ class EditInlineNode(template.Node):
def __init__(self, rel_var): def __init__(self, rel_var):
self.rel_var = rel_var self.rel_var = rel_var
def render(self, context): def iter_render(self, context):
relation = template.resolve_variable(self.rel_var, context) relation = template.resolve_variable(self.rel_var, context)
context.push() context.push()
if relation.field.rel.edit_inline == models.TABULAR: if relation.field.rel.edit_inline == models.TABULAR:
@ -169,10 +169,9 @@ class EditInlineNode(template.Node):
original = context.get('original', None) original = context.get('original', None)
bound_related_object = relation.bind(context['form'], original, bound_related_object_class) bound_related_object = relation.bind(context['form'], original, bound_related_object_class)
context['bound_related_object'] = bound_related_object context['bound_related_object'] = bound_related_object
t = loader.get_template(bound_related_object.template_name()) for chunk in loader.get_template(bound_related_object.template_name()).iter_render(context):
output = t.render(context) yield chunk
context.pop() context.pop()
return output
def output_all(form_fields): def output_all(form_fields):
return ''.join([str(f) for f in form_fields]) return ''.join([str(f) for f in form_fields])

View File

@ -7,7 +7,7 @@ class AdminApplistNode(template.Node):
def __init__(self, varname): def __init__(self, varname):
self.varname = varname self.varname = varname
def render(self, context): def iter_render(self, context):
from django.db import models from django.db import models
from django.utils.text import capfirst from django.utils.text import capfirst
app_list = [] app_list = []
@ -54,7 +54,7 @@ class AdminApplistNode(template.Node):
'models': model_list, 'models': model_list,
}) })
context[self.varname] = app_list context[self.varname] = app_list
return '' return ()
def get_admin_app_list(parser, token): def get_admin_app_list(parser, token):
""" """

View File

@ -10,14 +10,14 @@ class AdminLogNode(template.Node):
def __repr__(self): def __repr__(self):
return "<GetAdminLog Node>" return "<GetAdminLog Node>"
def render(self, context): def iter_render(self, context):
if self.user is None: if self.user is None:
context[self.varname] = LogEntry.objects.all().select_related()[:self.limit] context[self.varname] = LogEntry.objects.all().select_related()[:self.limit]
else: else:
if not self.user.isdigit(): if not self.user.isdigit():
self.user = context[self.user].id self.user = context[self.user].id
context[self.varname] = LogEntry.objects.filter(user__id__exact=self.user).select_related()[:self.limit] context[self.varname] = LogEntry.objects.filter(user__id__exact=self.user).select_related()[:self.limit]
return '' return ()
class DoGetAdminLog: class DoGetAdminLog:
""" """

View File

@ -24,7 +24,7 @@ class CommentFormNode(template.Node):
self.photo_options, self.rating_options = photo_options, rating_options self.photo_options, self.rating_options = photo_options, rating_options
self.is_public = is_public self.is_public = is_public
def render(self, context): def iter_render(self, context):
from django.conf import settings from django.conf import settings
from django.utils.text import normalize_newlines from django.utils.text import normalize_newlines
import base64 import base64
@ -33,7 +33,7 @@ class CommentFormNode(template.Node):
try: try:
self.obj_id = template.resolve_variable(self.obj_id_lookup_var, context) self.obj_id = template.resolve_variable(self.obj_id_lookup_var, context)
except template.VariableDoesNotExist: except template.VariableDoesNotExist:
return '' return
# Validate that this object ID is valid for this content-type. # 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, # We only have to do this validation if obj_id_lookup_var is provided,
# because do_comment_form() validates hard-coded object IDs. # 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['hash'] = Comment.objects.get_security_hash(context['options'], context['photo_options'], context['rating_options'], context['target'])
context['logout_url'] = settings.LOGOUT_URL context['logout_url'] = settings.LOGOUT_URL
default_form = loader.get_template(COMMENT_FORM) default_form = loader.get_template(COMMENT_FORM)
output = default_form.render(context) for chunk in default_form.iter_render(context):
yield chunk
context.pop() context.pop()
return output
class CommentCountNode(template.Node): class CommentCountNode(template.Node):
def __init__(self, package, module, context_var_name, obj_id, var_name, free): 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.context_var_name, self.obj_id = context_var_name, obj_id
self.var_name, self.free = var_name, free self.var_name, self.free = var_name, free
def render(self, context): def iter_render(self, context):
from django.conf import settings from django.conf import settings
manager = self.free and FreeComment.objects or Comment.objects manager = self.free and FreeComment.objects or Comment.objects
if self.context_var_name is not None: 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__app_label__exact=self.package,
content_type__model__exact=self.module, site__id__exact=settings.SITE_ID).count() content_type__model__exact=self.module, site__id__exact=settings.SITE_ID).count()
context[self.var_name] = comment_count context[self.var_name] = comment_count
return '' return ()
class CommentListNode(template.Node): class CommentListNode(template.Node):
def __init__(self, package, module, context_var_name, obj_id, var_name, free, ordering, extra_kwargs=None): 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.ordering = ordering
self.extra_kwargs = extra_kwargs or {} self.extra_kwargs = extra_kwargs or {}
def render(self, context): def iter_render(self, context):
from django.conf import settings from django.conf import settings
get_list_function = self.free and FreeComment.objects.filter or Comment.objects.get_list_with_karma get_list_function = self.free and FreeComment.objects.filter or Comment.objects.get_list_with_karma
if self.context_var_name is not None: if self.context_var_name is not None:
try: try:
self.obj_id = template.resolve_variable(self.context_var_name, context) self.obj_id = template.resolve_variable(self.context_var_name, context)
except template.VariableDoesNotExist: except template.VariableDoesNotExist:
return '' return ()
kwargs = { kwargs = {
'object_id__exact': self.obj_id, 'object_id__exact': self.obj_id,
'content_type__app_label__exact': self.package, '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)] 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 context[self.var_name] = comment_list
return '' return ()
class DoCommentForm: class DoCommentForm:
""" """

View File

@ -309,7 +309,7 @@ class ServerHandler(object):
""" """
if not self.result_is_file() and not self.sendfile(): if not self.result_is_file() and not self.sendfile():
for data in self.result: for data in self.result:
self.write(data) self.write(data, False)
self.finish_content() self.finish_content()
self.close() self.close()
@ -377,7 +377,7 @@ class ServerHandler(object):
else: else:
self._write('Status: %s\r\n' % self.status) 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""" """'write()' callable as specified by PEP 333"""
assert type(data) is StringType,"write() argument must be string" assert type(data) is StringType,"write() argument must be string"
@ -394,6 +394,7 @@ class ServerHandler(object):
# XXX check Content-Length and truncate if too many bytes written? # XXX check Content-Length and truncate if too many bytes written?
self._write(data) self._write(data)
if flush:
self._flush() self._flush()
def sendfile(self): def sendfile(self):
@ -421,8 +422,6 @@ class ServerHandler(object):
if not self.headers_sent: if not self.headers_sent:
self.headers['Content-Length'] = "0" self.headers['Content-Length'] = "0"
self.send_headers() self.send_headers()
else:
pass # XXX check if content-length was too short?
def close(self): def close(self):
try: try:

View File

@ -222,6 +222,12 @@ class HttpResponse(object):
content = ''.join(self._container) content = ''.join(self._container)
if isinstance(content, unicode): if isinstance(content, unicode):
content = content.encode(self._charset) 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 return content
def _set_content(self, value): def _set_content(self, value):
@ -231,14 +237,10 @@ class HttpResponse(object):
content = property(_get_content, _set_content) content = property(_get_content, _set_content)
def __iter__(self): def __iter__(self):
self._iterator = self._container.__iter__() for chunk in self._container:
return self
def next(self):
chunk = self._iterator.next()
if isinstance(chunk, unicode): if isinstance(chunk, unicode):
chunk = chunk.encode(self._charset) chunk = chunk.encode(self._charset)
return chunk yield chunk
def close(self): def close(self):
if hasattr(self._container, 'close'): if hasattr(self._container, 'close'):

View File

@ -309,6 +309,10 @@ class FormField(object):
return data return data
html2python = staticmethod(html2python) html2python = staticmethod(html2python)
def iter_render(self, data):
# this even needed?
return (self.render(data),)
def render(self, data): def render(self, data):
raise NotImplementedError raise NotImplementedError

View File

@ -7,7 +7,7 @@ from django.http import HttpResponse, Http404
from django.db.models.manager import Manager from django.db.models.manager import Manager
def render_to_response(*args, **kwargs): 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. load_and_render = render_to_response # For backwards compatibility.
def get_object_or_404(klass, *args, **kwargs): def get_object_or_404(klass, *args, **kwargs):

View File

@ -55,6 +55,7 @@ times with multiple contexts)
'\n<html>\n\n</html>\n' '\n<html>\n\n</html>\n'
""" """
import re import re
import types
from inspect import getargspec from inspect import getargspec
from django.conf import settings from django.conf import settings
from django.template.context import Context, RequestContext, ContextPopException from django.template.context import Context, RequestContext, ContextPopException
@ -167,9 +168,12 @@ class Template(object):
for subnode in node: for subnode in node:
yield subnode yield subnode
def render(self, context): def iter_render(self, context):
"Display stage -- can be called many times" "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): def compile_string(template_string, origin):
"Compiles template_string into NodeList ready for rendering" "Compiles template_string into NodeList ready for rendering"
@ -698,10 +702,26 @@ def resolve_variable(path, context):
del bits[0] del bits[0]
return current 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): class Node(object):
__metaclass__ = NodeBase
def iter_render(self, context):
return (self.render(context),)
def render(self, context): def render(self, context):
"Return the node rendered as a string" "Return the node rendered as a string"
pass return ''.join(self.iter_render(context))
def __iter__(self): def __iter__(self):
yield self yield self
@ -717,13 +737,12 @@ class Node(object):
class NodeList(list): class NodeList(list):
def render(self, context): def render(self, context):
bits = [] return ''.join(self.iter_render(context))
def iter_render(self, context):
for node in self: for node in self:
if isinstance(node, Node): for chunk in node.iter_render(context):
bits.append(self.render_node(node, context)) yield chunk
else:
bits.append(node)
return ''.join(bits)
def get_nodes_by_type(self, nodetype): def get_nodes_by_type(self, nodetype):
"Return a list of all nodes of the given type" "Return a list of all nodes of the given type"
@ -732,13 +751,16 @@ class NodeList(list):
nodes.extend(node.get_nodes_by_type(nodetype)) nodes.extend(node.get_nodes_by_type(nodetype))
return nodes return nodes
def render_node(self, node, context):
return(node.render(context))
class DebugNodeList(NodeList): class DebugNodeList(NodeList):
def render_node(self, node, context): def iter_render(self, context):
for node in self:
if not isinstance(node, Node):
yield node
continue
try: try:
result = node.render(context) for chunk in node.iter_render(context):
yield chunk
except TemplateSyntaxError, e: except TemplateSyntaxError, e:
if not hasattr(e, 'source'): if not hasattr(e, 'source'):
e.source = node.source e.source = node.source
@ -749,7 +771,6 @@ class DebugNodeList(NodeList):
wrapped.source = node.source wrapped.source = node.source
wrapped.exc_info = exc_info() wrapped.exc_info = exc_info()
raise wrapped raise wrapped
return result
class TextNode(Node): class TextNode(Node):
def __init__(self, s): def __init__(self, s):
@ -758,6 +779,9 @@ class TextNode(Node):
def __repr__(self): def __repr__(self):
return "<Text Node: '%s'>" % self.s[:25] return "<Text Node: '%s'>" % self.s[:25]
def iter_render(self, context):
return (self.s,)
def render(self, context): def render(self, context):
return self.s return self.s
@ -781,6 +805,9 @@ class VariableNode(Node):
else: else:
return output return output
def iter_render(self, context):
return (self.render(context),)
def render(self, context): def render(self, context):
output = self.filter_expression.resolve(context) output = self.filter_expression.resolve(context)
return self.encode_output(output) return self.encode_output(output)
@ -869,6 +896,9 @@ class Library(object):
def __init__(self, vars_to_resolve): def __init__(self, vars_to_resolve):
self.vars_to_resolve = vars_to_resolve self.vars_to_resolve = vars_to_resolve
#def iter_render(self, context):
# return (self.render(context),)
def render(self, context): def render(self, context):
resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve] resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve]
return func(*resolved_vars) return func(*resolved_vars)
@ -891,7 +921,7 @@ class Library(object):
def __init__(self, vars_to_resolve): def __init__(self, vars_to_resolve):
self.vars_to_resolve = 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] resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve]
if takes_context: if takes_context:
args = [context] + resolved_vars args = [context] + resolved_vars
@ -907,7 +937,7 @@ class Library(object):
else: else:
t = get_template(file_name) t = get_template(file_name)
self.nodelist = t.nodelist 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 = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, InclusionNode)
compile_func.__doc__ = func.__doc__ compile_func.__doc__ = func.__doc__

View File

@ -14,12 +14,11 @@ if not hasattr(__builtins__, 'reversed'):
for index in xrange(len(data)-1, -1, -1): for index in xrange(len(data)-1, -1, -1):
yield data[index] yield data[index]
register = Library() register = Library()
class CommentNode(Node): class CommentNode(Node):
def render(self, context): def iter_render(self, context):
return '' return ()
class CycleNode(Node): class CycleNode(Node):
def __init__(self, cyclevars, variable_name=None): def __init__(self, cyclevars, variable_name=None):
@ -28,6 +27,9 @@ class CycleNode(Node):
self.counter = -1 self.counter = -1
self.variable_name = variable_name self.variable_name = variable_name
def iter_render(self, context):
return (self.render(context),)
def render(self, context): def render(self, context):
self.counter += 1 self.counter += 1
value = self.cyclevars[self.counter % self.cyclevars_len] value = self.cyclevars[self.counter % self.cyclevars_len]
@ -36,29 +38,32 @@ class CycleNode(Node):
return value return value
class DebugNode(Node): class DebugNode(Node):
def render(self, context): def iter_render(self, context):
from pprint import pformat from pprint import pformat
output = [pformat(val) for val in context] for val in context:
output.append('\n\n') yield pformat(val)
output.append(pformat(sys.modules)) yield "\n\n"
return ''.join(output) yield pformat(sys.modules)
class FilterNode(Node): class FilterNode(Node):
def __init__(self, filter_expr, nodelist): def __init__(self, filter_expr, nodelist):
self.filter_expr, self.nodelist = 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) output = self.nodelist.render(context)
# apply filters # apply filters
context.update({'var': output}) context.update({'var': output})
filtered = self.filter_expr.resolve(context) filtered = self.filter_expr.resolve(context)
context.pop() context.pop()
return filtered return (filtered,)
class FirstOfNode(Node): class FirstOfNode(Node):
def __init__(self, vars): def __init__(self, vars):
self.vars = vars self.vars = vars
def iter_render(self, context):
return (self.render(context),)
def render(self, context): def render(self, context):
for var in self.vars: for var in self.vars:
try: try:
@ -94,8 +99,7 @@ class ForNode(Node):
nodes.extend(self.nodelist_loop.get_nodes_by_type(nodetype)) nodes.extend(self.nodelist_loop.get_nodes_by_type(nodetype))
return nodes return nodes
def render(self, context): def iter_render(self, context):
nodelist = NodeList()
if 'forloop' in context: if 'forloop' in context:
parentloop = context['forloop'] parentloop = context['forloop']
else: else:
@ -103,12 +107,12 @@ class ForNode(Node):
context.push() context.push()
try: try:
values = self.sequence.resolve(context, True) values = self.sequence.resolve(context, True)
except VariableDoesNotExist:
values = []
if values is None: if values is None:
values = [] values = ()
if not hasattr(values, '__len__'): elif not hasattr(values, '__len__'):
values = list(values) values = list(values)
except VariableDoesNotExist:
values = ()
len_values = len(values) len_values = len(values)
if self.reversed: if self.reversed:
values = reversed(values) values = reversed(values)
@ -127,12 +131,17 @@ class ForNode(Node):
'parentloop': parentloop, 'parentloop': parentloop,
} }
if unpack: 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))) context.update(dict(zip(self.loopvars, item)))
else: else:
context[self.loopvars[0]] = item context[self.loopvars[0]] = item
# We inline this to avoid the overhead since ForNode is pretty
# common.
for node in self.nodelist_loop: for node in self.nodelist_loop:
nodelist.append(node.render(context)) for chunk in node.iter_render(context):
yield chunk
if unpack: if unpack:
# The loop variables were pushed on to the context so pop them # The loop variables were pushed on to the context so pop them
# off again. This is necessary because the tag lets the length # off again. This is necessary because the tag lets the length
@ -141,7 +150,6 @@ class ForNode(Node):
# context. # context.
context.pop() context.pop()
context.pop() context.pop()
return nodelist.render(context)
class IfChangedNode(Node): class IfChangedNode(Node):
def __init__(self, nodelist, *varlist): def __init__(self, nodelist, *varlist):
@ -149,7 +157,7 @@ class IfChangedNode(Node):
self._last_seen = None self._last_seen = None
self._varlist = varlist self._varlist = varlist
def render(self, context): def iter_render(self, context):
if 'forloop' in context and context['forloop']['first']: if 'forloop' in context and context['forloop']['first']:
self._last_seen = None self._last_seen = None
try: try:
@ -167,11 +175,9 @@ class IfChangedNode(Node):
self._last_seen = compare_to self._last_seen = compare_to
context.push() context.push()
context['ifchanged'] = {'firstloop': firstloop} context['ifchanged'] = {'firstloop': firstloop}
content = self.nodelist.render(context) for chunk in self.nodelist.iter_render(context):
yield chunk
context.pop() context.pop()
return content
else:
return ''
class IfEqualNode(Node): class IfEqualNode(Node):
def __init__(self, var1, var2, nodelist_true, nodelist_false, negate): def __init__(self, var1, var2, nodelist_true, nodelist_false, negate):
@ -182,7 +188,7 @@ class IfEqualNode(Node):
def __repr__(self): def __repr__(self):
return "<IfEqualNode>" return "<IfEqualNode>"
def render(self, context): def iter_render(self, context):
try: try:
val1 = resolve_variable(self.var1, context) val1 = resolve_variable(self.var1, context)
except VariableDoesNotExist: except VariableDoesNotExist:
@ -192,8 +198,8 @@ class IfEqualNode(Node):
except VariableDoesNotExist: except VariableDoesNotExist:
val2 = None val2 = None
if (self.negate and val1 != val2) or (not self.negate and val1 == val2): if (self.negate and val1 != val2) or (not self.negate and val1 == val2):
return self.nodelist_true.render(context) return self.nodelist_true.iter_render(context)
return self.nodelist_false.render(context) return self.nodelist_false.iter_render(context)
class IfNode(Node): class IfNode(Node):
def __init__(self, bool_exprs, nodelist_true, nodelist_false, link_type): 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)) nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype))
return nodes return nodes
def render(self, context): def iter_render(self, context):
if self.link_type == IfNode.LinkTypes.or_: if self.link_type == IfNode.LinkTypes.or_:
for ifnot, bool_expr in self.bool_exprs: for ifnot, bool_expr in self.bool_exprs:
try: try:
@ -226,8 +232,8 @@ class IfNode(Node):
except VariableDoesNotExist: except VariableDoesNotExist:
value = None value = None
if (value and not ifnot) or (ifnot and not value): if (value and not ifnot) or (ifnot and not value):
return self.nodelist_true.render(context) return self.nodelist_true.iter_render(context)
return self.nodelist_false.render(context) return self.nodelist_false.iter_render(context)
else: else:
for ifnot, bool_expr in self.bool_exprs: for ifnot, bool_expr in self.bool_exprs:
try: try:
@ -235,8 +241,8 @@ class IfNode(Node):
except VariableDoesNotExist: except VariableDoesNotExist:
value = None value = None
if not ((value and not ifnot) or (ifnot and not value)): if not ((value and not ifnot) or (ifnot and not value)):
return self.nodelist_false.render(context) return self.nodelist_false.iter_render(context)
return self.nodelist_true.render(context) return self.nodelist_true.iter_render(context)
class LinkTypes: class LinkTypes:
and_ = 0, and_ = 0,
@ -247,11 +253,11 @@ class RegroupNode(Node):
self.target, self.expression = target, expression self.target, self.expression = target, expression
self.var_name = var_name self.var_name = var_name
def render(self, context): def iter_render(self, context):
obj_list = self.target.resolve(context, True) obj_list = self.target.resolve(context, True)
if obj_list == None: # target_var wasn't found in context; fail silently if obj_list == None: # target_var wasn't found in context; fail silently
context[self.var_name] = [] context[self.var_name] = []
return '' return ()
output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]} output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]}
for obj in obj_list: for obj in obj_list:
grouper = self.expression.resolve(obj, True) grouper = self.expression.resolve(obj, True)
@ -261,7 +267,7 @@ class RegroupNode(Node):
else: else:
output.append({'grouper': grouper, 'list': [obj]}) output.append({'grouper': grouper, 'list': [obj]})
context[self.var_name] = output context[self.var_name] = output
return '' return ()
def include_is_allowed(filepath): def include_is_allowed(filepath):
for root in settings.ALLOWED_INCLUDE_ROOTS: for root in settings.ALLOWED_INCLUDE_ROOTS:
@ -273,10 +279,10 @@ class SsiNode(Node):
def __init__(self, filepath, parsed): def __init__(self, filepath, parsed):
self.filepath, self.parsed = 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 not include_is_allowed(self.filepath):
if settings.DEBUG: if settings.DEBUG:
return "[Didn't have permission to include file]" return ("[Didn't have permission to include file]",)
else: else:
return '' # Fail silently for invalid includes. return '' # Fail silently for invalid includes.
try: try:
@ -287,23 +293,25 @@ class SsiNode(Node):
output = '' output = ''
if self.parsed: if self.parsed:
try: try:
t = Template(output, name=self.filepath) return Template(output, name=self.filepath).iter_render(context)
return t.render(context)
except TemplateSyntaxError, e: except TemplateSyntaxError, e:
if settings.DEBUG: if settings.DEBUG:
return "[Included template had syntax error: %s]" % e return "[Included template had syntax error: %s]" % e
else: else:
return '' # Fail silently for invalid included templates. return '' # Fail silently for invalid included templates.
return output return (output,)
class LoadNode(Node): class LoadNode(Node):
def render(self, context): def iter_render(self, context):
return '' return ()
class NowNode(Node): class NowNode(Node):
def __init__(self, format_string): def __init__(self, format_string):
self.format_string = format_string self.format_string = format_string
def iter_render(self, context):
return (self.render(context),)
def render(self, context): def render(self, context):
from datetime import datetime from datetime import datetime
from django.utils.dateformat import DateFormat from django.utils.dateformat import DateFormat
@ -332,6 +340,9 @@ class TemplateTagNode(Node):
def __init__(self, tagtype): def __init__(self, tagtype):
self.tagtype = tagtype self.tagtype = tagtype
def iter_render(self, context):
return (self.render(context),)
def render(self, context): def render(self, context):
return self.mapping.get(self.tagtype, '') return self.mapping.get(self.tagtype, '')
@ -341,18 +352,18 @@ class URLNode(Node):
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
def render(self, context): def iter_render(self, context):
from django.core.urlresolvers import reverse, NoReverseMatch from django.core.urlresolvers import reverse, NoReverseMatch
args = [arg.resolve(context) for arg in self.args] args = [arg.resolve(context) for arg in self.args]
kwargs = dict([(k, v.resolve(context)) for k, v in self.kwargs.items()]) kwargs = dict([(k, v.resolve(context)) for k, v in self.kwargs.items()])
try: try:
return reverse(self.view_name, args=args, kwargs=kwargs) return (reverse(self.view_name, args=args, kwargs=kwargs),)
except NoReverseMatch: except NoReverseMatch:
try: try:
project_name = settings.SETTINGS_MODULE.split('.')[0] project_name = settings.SETTINGS_MODULE.split('.')[0]
return reverse(project_name + '.' + self.view_name, args=args, kwargs=kwargs) return reverse(project_name + '.' + self.view_name, args=args, kwargs=kwargs)
except NoReverseMatch: except NoReverseMatch:
return '' return ()
class WidthRatioNode(Node): class WidthRatioNode(Node):
def __init__(self, val_expr, max_expr, max_width): def __init__(self, val_expr, max_expr, max_width):
@ -360,6 +371,9 @@ class WidthRatioNode(Node):
self.max_expr = max_expr self.max_expr = max_expr
self.max_width = max_width self.max_width = max_width
def iter_render(self, context):
return (self.render(context),)
def render(self, context): def render(self, context):
try: try:
value = self.val_expr.resolve(context) value = self.val_expr.resolve(context)
@ -383,13 +397,13 @@ class WithNode(Node):
def __repr__(self): def __repr__(self):
return "<WithNode>" return "<WithNode>"
def render(self, context): def iter_render(self, context):
val = self.var.resolve(context) val = self.var.resolve(context)
context.push() context.push()
context[self.name] = val context[self.name] = val
output = self.nodelist.render(context) for chunk in self.nodelist.iter_render(context):
yield chunk
context.pop() context.pop()
return output
#@register.tag #@register.tag
def comment(parser, token): def comment(parser, token):

View File

@ -87,14 +87,12 @@ def get_template_from_string(source, origin=None, name=None):
""" """
return Template(source, origin, name) 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 Common setup code for render_to_string and render_to_iter.
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.
""" """
dictionary = dictionary or {} if dictionary is None:
dictionary = {}
if isinstance(template_name, (list, tuple)): if isinstance(template_name, (list, tuple)):
t = select_template(template_name) t = select_template(template_name)
else: else:
@ -103,7 +101,28 @@ def render_to_string(template_name, dictionary=None, context_instance=None):
context_instance.update(dictionary) context_instance.update(dictionary)
else: else:
context_instance = Context(dictionary) 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): def select_template(template_name_list):
"Given a list of template names, returns the first that can be loaded." "Given a list of template names, returns the first that can be loaded."

View File

@ -15,14 +15,14 @@ class BlockNode(Node):
def __repr__(self): def __repr__(self):
return "<Block Node: %s. Contents: %r>" % (self.name, self.nodelist) return "<Block Node: %s. Contents: %r>" % (self.name, self.nodelist)
def render(self, context): def iter_render(self, context):
context.push() context.push()
# Save context in case of block.super(). # Save context in case of block.super().
self.context = context self.context = context
context['block'] = self context['block'] = self
result = self.nodelist.render(context) for chunk in self.nodelist.iter_render(context):
yield chunk
context.pop() context.pop()
return result
def super(self): def super(self):
if self.parent: if self.parent:
@ -59,7 +59,7 @@ class ExtendsNode(Node):
else: else:
return get_template_from_string(source, origin, parent) return get_template_from_string(source, origin, parent)
def render(self, context): def iter_render(self, context):
compiled_parent = self.get_parent(context) compiled_parent = self.get_parent(context)
parent_is_child = isinstance(compiled_parent.nodelist[0], ExtendsNode) 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)]) 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.parent = block_node.parent
parent_block.add_parent(parent_block.nodelist) parent_block.add_parent(parent_block.nodelist)
parent_block.nodelist = block_node.nodelist parent_block.nodelist = block_node.nodelist
return compiled_parent.render(context) return compiled_parent.iter_render(context)
class ConstantIncludeNode(Node): class ConstantIncludeNode(Node):
def __init__(self, template_path): def __init__(self, template_path):
@ -91,27 +91,26 @@ class ConstantIncludeNode(Node):
raise raise
self.template = None self.template = None
def render(self, context): def iter_render(self, context):
if self.template: if self.template:
return self.template.render(context) return self.template.iter_render(context)
else: return ()
return ''
class IncludeNode(Node): class IncludeNode(Node):
def __init__(self, template_name): def __init__(self, template_name):
self.template_name = template_name self.template_name = template_name
def render(self, context): def iter_render(self, context):
try: try:
template_name = resolve_variable(self.template_name, context) template_name = resolve_variable(self.template_name, context)
t = get_template(template_name) t = get_template(template_name)
return t.render(context) return t.iter_render(context)
except TemplateSyntaxError, e: except TemplateSyntaxError, e:
if settings.TEMPLATE_DEBUG: if settings.TEMPLATE_DEBUG:
raise raise
return '' return ()
except: except:
return '' # Fail silently for invalid included templates. return () # Fail silently for invalid included templates.
def do_block(parser, token): def do_block(parser, token):
""" """

View File

@ -11,13 +11,22 @@ from django.template import Template
TEST_DATABASE_PREFIX = 'test_' TEST_DATABASE_PREFIX = 'test_'
def instrumented_test_render(self, context): 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) dispatcher.send(signal=signals.template_rendered, sender=self, template=self, context=context)
return self.nodelist.render(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): class TestSMTPConnection(object):
"""A substitute SMTP connection for use during test sessions. """A substitute SMTP connection for use during test sessions.
The test connection stores email messages in a dummy outbox, The test connection stores email messages in a dummy outbox,
@ -44,7 +53,9 @@ def setup_test_environment():
""" """
Template.original_render = Template.render Template.original_render = Template.render
Template.original_iter_render = Template.iter_render
Template.render = instrumented_test_render Template.render = instrumented_test_render
Template.iter_render = instrumented_test_render
mail.original_SMTPConnection = mail.SMTPConnection mail.original_SMTPConnection = mail.SMTPConnection
mail.SMTPConnection = TestSMTPConnection mail.SMTPConnection = TestSMTPConnection
@ -59,7 +70,8 @@ def teardown_test_environment():
""" """
Template.render = Template.original_render 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 mail.SMTPConnection = mail.original_SMTPConnection
del mail.original_SMTPConnection del mail.original_SMTPConnection

View File

@ -137,7 +137,7 @@ def technical_500_response(request, exc_type, exc_value, tb):
'template_does_not_exist': template_does_not_exist, 'template_does_not_exist': template_does_not_exist,
'loader_debug_info': loader_debug_info, '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): def technical_404_response(request, exception):
"Create a technical 404 error response. The exception should be the Http404." "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", 'request_protocol': request.is_secure() and "https" or "http",
'settings': get_safe_settings(), '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): def empty_urlconf(request):
"Create an empty URLconf 404 error response." "Create an empty URLconf 404 error response."
@ -168,7 +168,7 @@ def empty_urlconf(request):
c = Context({ c = Context({
'project_name': settings.SETTINGS_MODULE.split('.')[0] '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): def _get_lines_from_file(filename, lineno, context_lines, loader=None, module_name=None):
""" """

View File

@ -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/') 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. 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'): def server_error(request, template_name='500.html'):
""" """
@ -86,4 +86,4 @@ def server_error(request, template_name='500.html'):
Context: None Context: None
""" """
t = loader.get_template(template_name) # You need to create a 500.html template. 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({})))

View File

@ -68,7 +68,7 @@ def create_object(request, model, template_name=None,
c[key] = value() c[key] = value()
else: else:
c[key] = value c[key] = value
return HttpResponse(t.render(c)) return HttpResponse(t.iter_render(c))
def update_object(request, model, object_id=None, slug=None, def update_object(request, model, object_id=None, slug=None,
slug_field=None, template_name=None, template_loader=loader, 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() c[key] = value()
else: else:
c[key] = value 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)) populate_xheaders(request, response, model, getattr(object, object._meta.pk.attname))
return response return response
@ -195,6 +195,6 @@ def delete_object(request, model, post_delete_redirect,
c[key] = value() c[key] = value()
else: else:
c[key] = value 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)) populate_xheaders(request, response, model, getattr(object, object._meta.pk.attname))
return response return response

View File

@ -44,7 +44,7 @@ def archive_index(request, queryset, date_field, num_latest=15,
c[key] = value() c[key] = value()
else: else:
c[key] = value 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, def archive_year(request, year, queryset, date_field, template_name=None,
template_loader=loader, extra_context=None, allow_empty=False, 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() c[key] = value()
else: else:
c[key] = value 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, def archive_month(request, year, month, queryset, date_field,
month_format='%b', template_name=None, template_loader=loader, 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() c[key] = value()
else: else:
c[key] = value 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, def archive_week(request, year, week, queryset, date_field,
template_name=None, template_loader=loader, template_name=None, template_loader=loader,
@ -206,7 +206,7 @@ def archive_week(request, year, week, queryset, date_field,
c[key] = value() c[key] = value()
else: else:
c[key] = value 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, def archive_day(request, year, month, day, queryset, date_field,
month_format='%b', day_format='%d', template_name=None, 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() c[key] = value()
else: else:
c[key] = value c[key] = value
return HttpResponse(t.render(c), mimetype=mimetype) return HttpResponse(t.iter_render(c), mimetype=mimetype)
def archive_today(request, **kwargs): def archive_today(request, **kwargs):
""" """
@ -339,6 +339,6 @@ def object_detail(request, year, month, day, queryset, date_field,
c[key] = value() c[key] = value()
else: else:
c[key] = value 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)) populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.name))
return response return response

View File

@ -84,7 +84,7 @@ def object_list(request, queryset, paginate_by=None, page=None,
model = queryset.model model = queryset.model
template_name = "%s/%s_list.html" % (model._meta.app_label, model._meta.object_name.lower()) template_name = "%s/%s_list.html" % (model._meta.app_label, model._meta.object_name.lower())
t = template_loader.get_template(template_name) 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, def object_detail(request, queryset, object_id=None, slug=None,
slug_field=None, template_name=None, template_name_field=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() c[key] = value()
else: else:
c[key] = value 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)) populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.name))
return response return response

View File

@ -15,7 +15,7 @@ def direct_to_template(request, template, extra_context={}, mimetype=None, **kwa
dictionary[key] = value dictionary[key] = value
c = RequestContext(request, dictionary) c = RequestContext(request, dictionary)
t = loader.get_template(template) 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): def redirect_to(request, url, **kwargs):
""" """

View File

@ -92,7 +92,7 @@ def directory_index(path, fullpath):
'directory' : path + '/', 'directory' : path + '/',
'file_list' : files, 'file_list' : files,
}) })
return HttpResponse(t.render(c)) return HttpResponse(t.iter_render(c))
def was_modified_since(header=None, mtime=0, size=0): def was_modified_since(header=None, mtime=0, size=0):
""" """

View File

@ -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 When Django compiles a template, it splits the raw template text into
''nodes''. Each node is an instance of ``django.template.Node`` and has ''nodes''. Each node is an instance of ``django.template.Node`` and has
a ``render()`` method. A compiled template is, simply, a list of ``Node`` either a ``render()`` or ``iter_render()`` method. A compiled template is,
objects. When you call ``render()`` on a compiled template object, the template simply, a list of ``Node`` objects. When you call ``render()`` on a compiled
calls ``render()`` on each ``Node`` in its node list, with the given context. template object, the template calls ``render()`` on each ``Node`` in its node
The results are all concatenated together to form the output of the template. 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 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 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 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 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``:: Continuing the above example, we need to define ``CurrentTimeNode``::
@ -1175,6 +1177,48 @@ For more examples of complex rendering, see the source code for ``{% if %}``,
.. _configuration: .. _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 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 file: ../settings/#using-settings-without-the-django-settings-module-environment-variable
.. _settings documentation: ../settings/ .. _settings documentation: ../settings/