diff --git a/AUTHORS b/AUTHORS index 585b1c8db61..1bd0863fff5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -288,6 +288,7 @@ answer newbie questions, and generally made Django that much better: Martin Mahner Matt McClanahan Frantisek Malina + Mike Malone Martin Maney masonsimon+django@gmail.com Manuzhai diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index a195b57b1bb..828aef524b2 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -158,9 +158,9 @@ TEMPLATE_DIRS = () # See the comments in django/core/template/loader.py for interface # documentation. TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.load_template_source', - 'django.template.loaders.app_directories.load_template_source', -# 'django.template.loaders.eggs.load_template_source', + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', ) # List of processors used by RequestContext to populate the context. diff --git a/django/conf/project_template/settings.py b/django/conf/project_template/settings.py index 69fb3683a1d..b66120fb8f8 100644 --- a/django/conf/project_template/settings.py +++ b/django/conf/project_template/settings.py @@ -52,9 +52,9 @@ SECRET_KEY = '' # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.load_template_source', - 'django.template.loaders.app_directories.load_template_source', -# 'django.template.loaders.eggs.load_template_source', + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', ) MIDDLEWARE_CLASSES = ( diff --git a/django/template/__init__.py b/django/template/__init__.py index 5b52d36089d..8764bfada46 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -173,9 +173,16 @@ class Template(object): for subnode in node: yield subnode + def _render(self, context): + return self.nodelist.render(context) + def render(self, context): "Display stage -- can be called many times" - return self.nodelist.render(context) + context.render_context.push() + try: + return self._render(context) + finally: + context.render_context.pop() def compile_string(template_string, origin): "Compiles template_string into NodeList ready for rendering" diff --git a/django/template/context.py b/django/template/context.py index f57a3aaa641..323c1446b2f 100644 --- a/django/template/context.py +++ b/django/template/context.py @@ -12,45 +12,42 @@ class ContextPopException(Exception): "pop() has been called more times than push()" pass -class Context(object): - "A stack container for variable context" - def __init__(self, dict_=None, autoescape=True, current_app=None): +class BaseContext(object): + def __init__(self, dict_=None): dict_ = dict_ or {} self.dicts = [dict_] - self.autoescape = autoescape - self.current_app = current_app def __repr__(self): return repr(self.dicts) def __iter__(self): - for d in self.dicts: + for d in reversed(self.dicts): yield d def push(self): d = {} - self.dicts = [d] + self.dicts + self.dicts.append(d) return d def pop(self): if len(self.dicts) == 1: raise ContextPopException - return self.dicts.pop(0) + return self.dicts.pop() def __setitem__(self, key, value): "Set a variable in the current context" - self.dicts[0][key] = value + self.dicts[-1][key] = value def __getitem__(self, key): "Get a variable's value, starting at the current context and going upward" - for d in self.dicts: + for d in reversed(self.dicts): if key in d: return d[key] raise KeyError(key) def __delitem__(self, key): "Delete a variable from the current context" - del self.dicts[0][key] + del self.dicts[-1][key] def has_key(self, key): for d in self.dicts: @@ -58,21 +55,58 @@ class Context(object): return True return False - __contains__ = has_key + def __contains__(self, key): + return self.has_key(key) def get(self, key, otherwise=None): - for d in self.dicts: + for d in reversed(self.dicts): if key in d: return d[key] return otherwise +class Context(BaseContext): + "A stack container for variable context" + def __init__(self, dict_=None, autoescape=True, current_app=None): + self.autoescape = autoescape + self.current_app = current_app + self.render_context = RenderContext() + super(Context, self).__init__(dict_) + def update(self, other_dict): "Like dict.update(). Pushes an entire dictionary's keys and values onto the context." if not hasattr(other_dict, '__getitem__'): raise TypeError('other_dict must be a mapping (dictionary-like) object.') - self.dicts = [other_dict] + self.dicts + self.dicts.append(other_dict) return other_dict +class RenderContext(BaseContext): + """ + A stack container for storing Template state. + + RenderContext simplifies the implementation of template Nodes by providing a + safe place to store state between invocations of a node's `render` method. + + The RenderContext also provides scoping rules that are more sensible for + 'template local' variables. The render context stack is pushed before each + template is rendered, creating a fresh scope with nothing in it. Name + resolution fails if a variable is not found at the top of the RequestContext + stack. Thus, variables are local to a specific template and don't affect the + rendering of other templates as they would if they were stored in the normal + template context. + """ + def __iter__(self): + for d in self.dicts[-1]: + yield d + + def has_key(self, key): + return key in self.dicts[-1] + + def get(self, key, otherwise=None): + d = self.dicts[-1] + if key in d: + return d[key] + return otherwise + # This is a function rather than module-level procedural code because we only # want it to execute if somebody uses RequestContext. def get_standard_processors(): diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 77b9b9795ce..2ccfc6a5e12 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -57,11 +57,14 @@ class CsrfTokenNode(Node): class CycleNode(Node): def __init__(self, cyclevars, variable_name=None): - self.cycle_iter = itertools_cycle(cyclevars) + self.cyclevars = cyclevars self.variable_name = variable_name def render(self, context): - value = self.cycle_iter.next().resolve(context) + if self not in context.render_context: + context.render_context[self] = itertools_cycle(self.cyclevars) + cycle_iter = context.render_context[self] + value = cycle_iter.next().resolve(context) if self.variable_name: context[self.variable_name] = value return value diff --git a/django/template/loader.py b/django/template/loader.py index 8195c4b7982..8b3c8e3fb5c 100644 --- a/django/template/loader.py +++ b/django/template/loader.py @@ -27,6 +27,36 @@ from django.conf import settings template_source_loaders = None +class BaseLoader(object): + is_usable = False + + def __init__(self, *args, **kwargs): + pass + + def __call__(self, template_name, template_dirs=None): + return self.load_template(template_name, template_dirs) + + def load_template(self, template_name, template_dirs=None): + source, origin = self.load_template_source(template_name, template_dirs) + template = get_template_from_string(source, name=template_name) + return template, origin + + def load_template_source(self, template_name, template_dirs=None): + """ + Returns a tuple containing the source and origin for the given template + name. + + """ + raise NotImplementedError + + def reset(self): + """ + Resets any state maintained by the loader instance (e.g., cached + templates or cached loader modules). + + """ + pass + class LoaderOrigin(Origin): def __init__(self, display_name, loader, name, dirs): super(LoaderOrigin, self).__init__(display_name) @@ -41,29 +71,50 @@ def make_origin(display_name, loader, name, dirs): else: return None -def find_template_source(name, dirs=None): +def find_template_loader(loader): + if hasattr(loader, '__iter__'): + loader, args = loader[0], loader[1:] + else: + args = [] + if isinstance(loader, basestring): + module, attr = loader.rsplit('.', 1) + try: + mod = import_module(module) + except ImportError: + raise ImproperlyConfigured('Error importing template source loader %s: "%s"' % (loader, e)) + try: + TemplateLoader = getattr(mod, attr) + except AttributeError, e: + raise ImproperlyConfigured('Error importing template source loader %s: "%s"' % (loader, e)) + + if hasattr(TemplateLoader, 'load_template_source'): + func = TemplateLoader(*args) + else: + # Try loading module the old way - string is full path to callable + if args: + raise ImproperlyConfigured("Error importing template source loader %s - can't pass arguments to function-based loader." % loader) + func = TemplateLoader + + if not func.is_usable: + import warnings + warnings.warn("Your TEMPLATE_LOADERS setting includes %r, but your Python installation doesn't support that type of template loading. Consider removing that line from TEMPLATE_LOADERS." % loader) + return None + else: + return func + else: + raise ImproperlyConfigured('Loader does not define a "load_template" callable template source loader') + +def find_template(name, dirs=None): # Calculate template_source_loaders the first time the function is executed # because putting this logic in the module-level namespace may cause # circular import errors. See Django ticket #1292. global template_source_loaders if template_source_loaders is None: loaders = [] - for path in settings.TEMPLATE_LOADERS: - i = path.rfind('.') - module, attr = path[:i], path[i+1:] - try: - mod = import_module(module) - except ImportError, e: - raise ImproperlyConfigured, 'Error importing template source loader %s: "%s"' % (module, e) - try: - func = getattr(mod, attr) - except AttributeError: - raise ImproperlyConfigured, 'Module "%s" does not define a "%s" callable template source loader' % (module, attr) - if not func.is_usable: - import warnings - warnings.warn("Your TEMPLATE_LOADERS setting includes %r, but your Python installation doesn't support that type of template loading. Consider removing that line from TEMPLATE_LOADERS." % path) - else: - loaders.append(func) + for loader_name in settings.TEMPLATE_LOADERS: + loader = find_template_loader(loader_name) + if loader is not None: + loaders.append(loader) template_source_loaders = tuple(loaders) for loader in template_source_loaders: try: @@ -73,13 +124,27 @@ def find_template_source(name, dirs=None): pass raise TemplateDoesNotExist, name +def find_template_source(name, dirs=None): + # For backward compatibility + import warnings + warnings.warn( + "`django.template.loaders.find_template_source` is deprecated; use `django.template.loaders.find_template` instead.", + PendingDeprecationWarning + ) + template, origin = find_template(name, dirs) + if hasattr(template, 'render'): + raise Exception("Found a compiled template that is incompatible with the deprecated `django.template.loaders.find_template_source` function.") + return template, origin + def get_template(template_name): """ Returns a compiled Template object for the given template name, handling template inheritance recursively. """ - source, origin = find_template_source(template_name) - template = get_template_from_string(source, origin, template_name) + template, origin = find_template(template_name) + if not hasattr(template, 'render'): + # template needs to be compiled + template = get_template_from_string(template, origin, template_name) return template def get_template_from_string(source, origin=None, name=None): diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py index f91699d7f28..39c6f497e28 100644 --- a/django/template/loader_tags.py +++ b/django/template/loader_tags.py @@ -1,14 +1,43 @@ from django.template import TemplateSyntaxError, TemplateDoesNotExist, Variable from django.template import Library, Node, TextNode -from django.template.loader import get_template, get_template_from_string, find_template_source +from django.template.loader import get_template from django.conf import settings from django.utils.safestring import mark_safe register = Library() +BLOCK_CONTEXT_KEY = 'block_context' + class ExtendsError(Exception): pass +class BlockContext(object): + def __init__(self): + # Dictionary of FIFO queues. + self.blocks = {} + + def add_blocks(self, blocks): + for name, block in blocks.iteritems(): + if name in self.blocks: + self.blocks[name].insert(0, block) + else: + self.blocks[name] = [block] + + def pop(self, name): + try: + return self.blocks[name].pop() + except (IndexError, KeyError): + return None + + def push(self, name, block): + self.blocks[name].append(block) + + def get_block(self, name): + try: + return self.blocks[name][-1] + except (IndexError, KeyError): + return None + class BlockNode(Node): def __init__(self, name, nodelist, parent=None): self.name, self.nodelist, self.parent = name, nodelist, parent @@ -17,25 +46,32 @@ class BlockNode(Node): return "" % (self.name, self.nodelist) def render(self, context): + block_context = context.render_context.get(BLOCK_CONTEXT_KEY) context.push() - # Save context in case of block.super(). - self.context = context - context['block'] = self - result = self.nodelist.render(context) + if block_context is None: + context['block'] = self + result = self.nodelist.render(context) + else: + push = block = block_context.pop(self.name) + if block is None: + block = self + # Create new block so we can store context without thread-safety issues. + block = BlockNode(block.name, block.nodelist) + block.context = context + context['block'] = block + result = block.nodelist.render(context) + if push is not None: + block_context.push(self.name, push) context.pop() return result def super(self): - if self.parent: - return mark_safe(self.parent.render(self.context)) + render_context = self.context.render_context + if (BLOCK_CONTEXT_KEY in render_context and + render_context[BLOCK_CONTEXT_KEY].get_block(self.name) is not None): + return mark_safe(self.render(self.context)) return '' - def add_parent(self, nodelist): - if self.parent: - self.parent.add_parent(nodelist) - else: - self.parent = BlockNode(self.name, nodelist) - class ExtendsNode(Node): must_be_first = True @@ -43,6 +79,7 @@ class ExtendsNode(Node): self.nodelist = nodelist self.parent_name, self.parent_name_expr = parent_name, parent_name_expr self.template_dirs = template_dirs + self.blocks = dict([(n.name, n) for n in nodelist.get_nodes_by_type(BlockNode)]) def __repr__(self): if self.parent_name_expr: @@ -61,40 +98,34 @@ class ExtendsNode(Node): if hasattr(parent, 'render'): return parent # parent is a Template object try: - source, origin = find_template_source(parent, self.template_dirs) + return get_template(parent) except TemplateDoesNotExist: raise TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent - else: - return get_template_from_string(source, origin, parent) def render(self, context): compiled_parent = self.get_parent(context) - parent_blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)]) - for block_node in self.nodelist.get_nodes_by_type(BlockNode): - # Check for a BlockNode with this node's name, and replace it if found. - try: - parent_block = parent_blocks[block_node.name] - except KeyError: - # This BlockNode wasn't found in the parent template, but the - # parent block might be defined in the parent's *parent*, so we - # add this BlockNode to the parent's ExtendsNode nodelist, so - # it'll be checked when the parent node's render() is called. - # Find out if the parent template has a parent itself - for node in compiled_parent.nodelist: - if not isinstance(node, TextNode): - # If the first non-text node is an extends, handle it. - if isinstance(node, ExtendsNode): - node.nodelist.append(block_node) - # Extends must be the first non-text node, so once you find - # the first non-text node you can stop looking. - break - else: - # Keep any existing parents and add a new one. Used by BlockNode. - parent_block.parent = block_node.parent - parent_block.add_parent(parent_block.nodelist) - parent_block.nodelist = block_node.nodelist - return compiled_parent.render(context) + if BLOCK_CONTEXT_KEY not in context.render_context: + context.render_context[BLOCK_CONTEXT_KEY] = BlockContext() + block_context = context.render_context[BLOCK_CONTEXT_KEY] + + # Add the block nodes from this node to the block context + block_context.add_blocks(self.blocks) + + # If this block's parent doesn't have an extends node it is the root, + # and its block nodes also need to be added to the block context. + for node in compiled_parent.nodelist: + # The ExtendsNode has to be the first non-text node. + if not isinstance(node, TextNode): + if not isinstance(node, ExtendsNode): + blocks = dict([(n.name, n) for n in + compiled_parent.nodelist.get_nodes_by_type(BlockNode)]) + block_context.add_blocks(blocks) + break + + # Call Template._render explicitly so the parser context stays + # the same. + return compiled_parent._render(context) class ConstantIncludeNode(Node): def __init__(self, template_path): diff --git a/django/template/loaders/app_directories.py b/django/template/loaders/app_directories.py index b93a6993768..2c778c1c2a6 100644 --- a/django/template/loaders/app_directories.py +++ b/django/template/loaders/app_directories.py @@ -9,6 +9,7 @@ import sys from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.template import TemplateDoesNotExist +from django.template.loader import BaseLoader from django.utils._os import safe_join from django.utils.importlib import import_module @@ -27,29 +28,47 @@ for app in settings.INSTALLED_APPS: # It won't change, so convert it to a tuple to save memory. app_template_dirs = tuple(app_template_dirs) -def get_template_sources(template_name, template_dirs=None): - """ - Returns the absolute paths to "template_name", when appended to each - directory in "template_dirs". Any paths that don't lie inside one of the - template dirs are excluded from the result set, for security reasons. - """ - if not template_dirs: - template_dirs = app_template_dirs - for template_dir in template_dirs: - try: - yield safe_join(template_dir, template_name) - except UnicodeDecodeError: - # The template dir name was a bytestring that wasn't valid UTF-8. - raise - except ValueError: - # The joined path was located outside of template_dir. - pass +class Loader(BaseLoader): + is_usable = True + + def get_template_sources(self, template_name, template_dirs=None): + """ + Returns the absolute paths to "template_name", when appended to each + directory in "template_dirs". Any paths that don't lie inside one of the + template dirs are excluded from the result set, for security reasons. + """ + if not template_dirs: + template_dirs = app_template_dirs + for template_dir in template_dirs: + try: + yield safe_join(template_dir, template_name) + except UnicodeDecodeError: + # The template dir name was a bytestring that wasn't valid UTF-8. + raise + except ValueError: + # The joined path was located outside of template_dir. + pass + + def load_template_source(self, template_name, template_dirs=None): + for filepath in self.get_template_sources(template_name, template_dirs): + try: + file = open(filepath) + try: + return (file.read().decode(settings.FILE_CHARSET), filepath) + finally: + file.close() + except IOError: + pass + raise TemplateDoesNotExist, template_name + +_loader = Loader() def load_template_source(template_name, template_dirs=None): - for filepath in get_template_sources(template_name, template_dirs): - try: - return (open(filepath).read().decode(settings.FILE_CHARSET), filepath) - except IOError: - pass - raise TemplateDoesNotExist, template_name + # For backwards compatibility + import warnings + warnings.warn( + "'django.template.loaders.app_directories.load_template_source' is deprecated; use 'django.template.loaders.app_directories.Loader' instead.", + PendingDeprecationWarning + ) + return _loader.load_template_source(template_name, template_dirs) load_template_source.is_usable = True diff --git a/django/template/loaders/cached.py b/django/template/loaders/cached.py new file mode 100644 index 00000000000..4c960ad0dc0 --- /dev/null +++ b/django/template/loaders/cached.py @@ -0,0 +1,46 @@ +""" +Wrapper class that takes a list of template loaders as an argument and attempts +to load templates from them in order, caching the result. +""" + +from django.template import TemplateDoesNotExist +from django.template.loader import BaseLoader, get_template_from_string, find_template_loader, make_origin +from django.utils.importlib import import_module +from django.core.exceptions import ImproperlyConfigured + +class Loader(BaseLoader): + is_usable = True + + def __init__(self, loaders): + self.template_cache = {} + self._loaders = loaders + self._cached_loaders = [] + + @property + def loaders(self): + # Resolve loaders on demand to avoid circular imports + if not self._cached_loaders: + for loader in self._loaders: + self._cached_loaders.append(find_template_loader(loader)) + return self._cached_loaders + + def find_template(self, name, dirs=None): + for loader in self.loaders: + try: + template, display_name = loader(name, dirs) + return (template, make_origin(display_name, loader, name, dirs)) + except TemplateDoesNotExist: + pass + raise TemplateDoesNotExist, name + + def load_template(self, template_name, template_dirs=None): + if template_name not in self.template_cache: + template, origin = self.find_template(template_name, template_dirs) + if not hasattr(template, 'render'): + template = get_template_from_string(template, origin, template_name) + self.template_cache[template_name] = (template, origin) + return self.template_cache[template_name] + + def reset(self): + "Empty the template cache." + self.template_cache.clear() diff --git a/django/template/loaders/eggs.py b/django/template/loaders/eggs.py index 946c2b4759d..c7bc749cea8 100644 --- a/django/template/loaders/eggs.py +++ b/django/template/loaders/eggs.py @@ -6,20 +6,34 @@ except ImportError: resource_string = None from django.template import TemplateDoesNotExist +from django.template.loader import BaseLoader from django.conf import settings -def load_template_source(template_name, template_dirs=None): - """ - Loads templates from Python eggs via pkg_resource.resource_string. +class Loader(BaseLoader): + is_usable = resource_string is not None - For every installed app, it tries to get the resource (app, template_name). - """ - if resource_string is not None: - pkg_name = 'templates/' + template_name - for app in settings.INSTALLED_APPS: - try: - return (resource_string(app, pkg_name).decode(settings.FILE_CHARSET), 'egg:%s:%s' % (app, pkg_name)) - except: - pass - raise TemplateDoesNotExist, template_name + def load_template_source(self, template_name, template_dirs=None): + """ + Loads templates from Python eggs via pkg_resource.resource_string. + + For every installed app, it tries to get the resource (app, template_name). + """ + if resource_string is not None: + pkg_name = 'templates/' + template_name + for app in settings.INSTALLED_APPS: + try: + return (resource_string(app, pkg_name).decode(settings.FILE_CHARSET), 'egg:%s:%s' % (app, pkg_name)) + except: + pass + raise TemplateDoesNotExist, template_name + +_loader = Loader() + +def load_template_source(template_name, template_dirs=None): + import warnings + warnings.warn( + "'django.template.loaders.eggs.load_template_source' is deprecated; use 'django.template.loaders.eggs.Loader' instead.", + PendingDeprecationWarning + ) + return _loader.load_template_source(template_name, template_dirs) load_template_source.is_usable = resource_string is not None diff --git a/django/template/loaders/filesystem.py b/django/template/loaders/filesystem.py index afee3c05191..aad21fff58b 100644 --- a/django/template/loaders/filesystem.py +++ b/django/template/loaders/filesystem.py @@ -4,38 +4,58 @@ Wrapper for loading templates from the filesystem. from django.conf import settings from django.template import TemplateDoesNotExist +from django.template.loader import BaseLoader from django.utils._os import safe_join -def get_template_sources(template_name, template_dirs=None): - """ - Returns the absolute paths to "template_name", when appended to each - directory in "template_dirs". Any paths that don't lie inside one of the - template dirs are excluded from the result set, for security reasons. - """ - if not template_dirs: - template_dirs = settings.TEMPLATE_DIRS - for template_dir in template_dirs: - try: - yield safe_join(template_dir, template_name) - except UnicodeDecodeError: - # The template dir name was a bytestring that wasn't valid UTF-8. - raise - except ValueError: - # The joined path was located outside of this particular - # template_dir (it might be inside another one, so this isn't - # fatal). - pass +class Loader(BaseLoader): + is_usable = True + + def get_template_sources(self, template_name, template_dirs=None): + """ + Returns the absolute paths to "template_name", when appended to each + directory in "template_dirs". Any paths that don't lie inside one of the + template dirs are excluded from the result set, for security reasons. + """ + if not template_dirs: + template_dirs = settings.TEMPLATE_DIRS + for template_dir in template_dirs: + try: + yield safe_join(template_dir, template_name) + except UnicodeDecodeError: + # The template dir name was a bytestring that wasn't valid UTF-8. + raise + except ValueError: + # The joined path was located outside of this particular + # template_dir (it might be inside another one, so this isn't + # fatal). + pass + + def load_template_source(self, template_name, template_dirs=None): + tried = [] + for filepath in self.get_template_sources(template_name, template_dirs): + try: + file = open(filepath) + try: + return (file.read().decode(settings.FILE_CHARSET), filepath) + finally: + file.close() + except IOError: + tried.append(filepath) + if tried: + error_msg = "Tried %s" % tried + else: + error_msg = "Your TEMPLATE_DIRS setting is empty. Change it to point to at least one template directory." + raise TemplateDoesNotExist, error_msg + load_template_source.is_usable = True + +_loader = Loader() def load_template_source(template_name, template_dirs=None): - tried = [] - for filepath in get_template_sources(template_name, template_dirs): - try: - return (open(filepath).read().decode(settings.FILE_CHARSET), filepath) - except IOError: - tried.append(filepath) - if tried: - error_msg = "Tried %s" % tried - else: - error_msg = "Your TEMPLATE_DIRS setting is empty. Change it to point to at least one template directory." - raise TemplateDoesNotExist, error_msg + # For backwards compatibility + import warnings + warnings.warn( + "'django.template.loaders.filesystem.load_template_source' is deprecated; use 'django.template.loaders.filesystem.Loader' instead.", + PendingDeprecationWarning + ) + return _loader.load_template_source(template_name, template_dirs) load_template_source.is_usable = True diff --git a/django/test/utils.py b/django/test/utils.py index 9d39eee9262..6e9baa02207 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -37,8 +37,8 @@ def setup_test_environment(): - Set the email backend to the locmem email backend. - Setting the active locale to match the LANGUAGE_CODE setting. """ - Template.original_render = Template.render - Template.render = instrumented_test_render + Template.original_render = Template._render + Template._render = instrumented_test_render mail.original_SMTPConnection = mail.SMTPConnection mail.SMTPConnection = locmem.EmailBackend @@ -57,7 +57,7 @@ def teardown_test_environment(): - Restoring the email sending functions """ - Template.render = Template.original_render + Template._render = Template.original_render del Template.original_render mail.SMTPConnection = mail.original_SMTPConnection diff --git a/django/views/debug.py b/django/views/debug.py index 6da89ffcf5b..08b6e23fb70 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -76,8 +76,12 @@ class ExceptionReporter: for t in source_list_func(str(self.exc_value))] except (ImportError, AttributeError): template_list = [] + if hasattr(loader, '__class__'): + loader_name = loader.__module__ + '.' + loader.__class__.__name__ + else: + loader_name = loader.__module__ + '.' + loader.__name__ self.loader_debug_info.append({ - 'loader': loader.__module__ + '.' + loader.__name__, + 'loader': loader_name, 'templates': template_list, }) if settings.TEMPLATE_DEBUG and hasattr(self.exc_value, 'source'): diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index c6f76772def..774d12dc44a 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -463,6 +463,85 @@ new ``Context`` in this example, the results would have *always* been automatically escaped, which may not be the desired behavior if the template tag is used inside a ``{% autoescape off %}`` block. +.. _template_tag_thread_safety: + +Thread-safety considerations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.2 + +Once a node is parsed, its ``render`` method may be called any number of times. +Since Django is sometimes run in multi-threaded environments, a single node may +be simultaneously rendering with different contexts in response to two separate +requests. Therefore, it's important to make sure your template tags are thread +safe. + +To make sure your template tags are thread safe, you should never store state +information on the node itself. For example, Django provides a builtin ``cycle`` +template tag that cycles among a list of given strings each time it's rendered:: + + {% for o in some_list %} + {{ message }} + +Template caching +---------------- + +In previous versions of Django, every time you rendered a template it +would be reloaded from disk. In Django 1.2, you can use a :ref:`cached +template loader ` to load templates once, then use a +cached the result for every subsequent render. This can lead to a +significant performance improvement if your templates are broken into +lots of smaller subtemplates (using the ``{% extends %}`` or ``{% +include %}`` tags). + +As a side effect, it is now much easier to support non-Django template +languages. For more details, see the :ref:`notes on supporting +non-Django template languages`. diff --git a/tests/regressiontests/templates/context.py b/tests/regressiontests/templates/context.py index d8b0f39abe0..7886c8328bd 100644 --- a/tests/regressiontests/templates/context.py +++ b/tests/regressiontests/templates/context.py @@ -10,9 +10,13 @@ context_tests = r""" >>> c['a'] = 2 >>> c['a'] 2 +>>> c.get('a') +2 >>> c.pop() {'a': 2} >>> c['a'] 1 +>>> c.get('foo', 42) +42 """ diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index c29c53ae444..29462086d87 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -15,7 +15,7 @@ import unittest from django import template from django.core import urlresolvers from django.template import loader -from django.template.loaders import app_directories, filesystem +from django.template.loaders import app_directories, filesystem, cached from django.utils.translation import activate, deactivate, ugettext as _ from django.utils.safestring import mark_safe from django.utils.tzinfo import LocalTimezone @@ -101,13 +101,15 @@ class UTF8Class: class Templates(unittest.TestCase): def test_loaders_security(self): + ad_loader = app_directories.Loader() + fs_loader = filesystem.Loader() def test_template_sources(path, template_dirs, expected_sources): if isinstance(expected_sources, list): # Fix expected sources so they are normcased and abspathed expected_sources = [os.path.normcase(os.path.abspath(s)) for s in expected_sources] # Test the two loaders (app_directores and filesystem). - func1 = lambda p, t: list(app_directories.get_template_sources(p, t)) - func2 = lambda p, t: list(filesystem.get_template_sources(p, t)) + func1 = lambda p, t: list(ad_loader.get_template_sources(p, t)) + func2 = lambda p, t: list(fs_loader.get_template_sources(p, t)) for func in (func1, func2): if isinstance(expected_sources, list): self.assertEqual(func(path, template_dirs), expected_sources) @@ -198,8 +200,11 @@ class Templates(unittest.TestCase): except KeyError: raise template.TemplateDoesNotExist, template_name + cache_loader = cached.Loader(('test_template_loader',)) + cache_loader._cached_loaders = (test_template_loader,) + old_template_loaders = loader.template_source_loaders - loader.template_source_loaders = [test_template_loader] + loader.template_source_loaders = [cache_loader] failures = [] tests = template_tests.items() @@ -232,20 +237,22 @@ class Templates(unittest.TestCase): for invalid_str, result in [('', normal_string_result), (expected_invalid_str, invalid_string_result)]: settings.TEMPLATE_STRING_IF_INVALID = invalid_str - try: - test_template = loader.get_template(name) - output = self.render(test_template, vals) - except ContextStackException: - failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Context stack was left imbalanced" % (invalid_str, name)) - continue - except Exception: - exc_type, exc_value, exc_tb = sys.exc_info() - if exc_type != result: - tb = '\n'.join(traceback.format_exception(exc_type, exc_value, exc_tb)) - failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Got %s, exception: %s\n%s" % (invalid_str, name, exc_type, exc_value, tb)) - continue - if output != result: - failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Expected %r, got %r" % (invalid_str, name, result, output)) + for is_cached in (False, True): + try: + test_template = loader.get_template(name) + output = self.render(test_template, vals) + except ContextStackException: + failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Context stack was left imbalanced" % (is_cached, invalid_str, name)) + continue + except Exception: + exc_type, exc_value, exc_tb = sys.exc_info() + if exc_type != result: + tb = '\n'.join(traceback.format_exception(exc_type, exc_value, exc_tb)) + failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Got %s, exception: %s\n%s" % (is_cached, invalid_str, name, exc_type, exc_value, tb)) + continue + if output != result: + failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Expected %r, got %r" % (is_cached, invalid_str, name, result, output)) + cache_loader.reset() if 'LANGUAGE_CODE' in vals[1]: deactivate() diff --git a/tests/regressiontests/test_client_regress/models.py b/tests/regressiontests/test_client_regress/models.py index 58693cc3953..e532c90afc6 100644 --- a/tests/regressiontests/test_client_regress/models.py +++ b/tests/regressiontests/test_client_regress/models.py @@ -10,6 +10,7 @@ from django.test.utils import ContextList from django.core.urlresolvers import reverse from django.core.exceptions import SuspiciousOperation from django.template import TemplateDoesNotExist, TemplateSyntaxError, Context +from django.template import loader class AssertContainsTests(TestCase): def setUp(self): @@ -436,6 +437,11 @@ class ExceptionTests(TestCase): class TemplateExceptionTests(TestCase): def setUp(self): + # Reset the loaders so they don't try to render cached templates. + if loader.template_source_loaders is not None: + for template_loader in loader.template_source_loaders: + if hasattr(template_loader, 'reset'): + template_loader.reset() self.old_templates = settings.TEMPLATE_DIRS settings.TEMPLATE_DIRS = ()