Fixed #15053 -- Enabled recursive template loading.

This commit is contained in:
Preston Timmons 2015-03-03 15:48:26 -06:00
parent 1b1b58bc7b
commit fc21471526
25 changed files with 740 additions and 129 deletions

View File

@ -59,8 +59,8 @@ from .base import (TemplateDoesNotExist, TemplateSyntaxError, # NOQA
from .context import ContextPopException # NOQA from .context import ContextPopException # NOQA
# Template parts # Template parts
from .base import (Context, Node, NodeList, RequestContext, # NOQA from .base import (Context, Node, NodeList, Origin, RequestContext, # NOQA
StringOrigin, Template, Variable) Template, Variable)
# Deprecated in Django 1.8, will be removed in Django 2.0. # Deprecated in Django 1.8, will be removed in Django 2.0.
from .base import resolve_variable # NOQA from .base import resolve_variable # NOQA

View File

@ -134,7 +134,15 @@ class TemplateSyntaxError(Exception):
class TemplateDoesNotExist(Exception): class TemplateDoesNotExist(Exception):
pass """
This exception is used when template loaders are unable to find a
template. The tried argument is an optional list of tuples containing
(origin, status), where origin is an Origin object and status is a string
with the reason the template wasn't found.
"""
def __init__(self, msg, tried=None):
self.tried = tried or []
super(TemplateDoesNotExist, self).__init__(msg)
class TemplateEncodingError(Exception): class TemplateEncodingError(Exception):
@ -157,23 +165,29 @@ class InvalidTemplateLibrary(Exception):
class Origin(object): class Origin(object):
def __init__(self, name): def __init__(self, name, template_name=None, loader=None):
self.name = name self.name = name
self.template_name = template_name
def reload(self): self.loader = loader
raise NotImplementedError('subclasses of Origin must provide a reload() method')
def __str__(self): def __str__(self):
return self.name return self.name
def __eq__(self, other):
if not isinstance(other, Origin):
return False
class StringOrigin(Origin): return (
def __init__(self, source): self.name == other.name and
super(StringOrigin, self).__init__(UNKNOWN_SOURCE) self.loader == other.loader
self.source = source )
def reload(self): @property
return self.source def loader_name(self):
if self.loader:
return '%s.%s' % (
self.loader.__module__, self.loader.__class__.__name__,
)
class Template(object): class Template(object):
@ -191,7 +205,7 @@ class Template(object):
from .engine import Engine from .engine import Engine
engine = Engine.get_default() engine = Engine.get_default()
if origin is None: if origin is None:
origin = StringOrigin(template_string) origin = Origin(UNKNOWN_SOURCE)
self.name = name self.name = name
self.origin = origin self.origin = origin
self.engine = engine self.engine = engine

View File

@ -124,15 +124,25 @@ class Engine(object):
raise ImproperlyConfigured( raise ImproperlyConfigured(
"Invalid value in template loaders configuration: %r" % loader) "Invalid value in template loaders configuration: %r" % loader)
def find_template(self, name, dirs=None): def find_template(self, name, dirs=None, skip=None):
tried = []
for loader in self.template_loaders: for loader in self.template_loaders:
try: if loader.supports_recursion:
source, display_name = loader(name, dirs) try:
origin = self.make_origin(display_name, loader, name, dirs) template = loader.get_template(
return source, origin name, template_dirs=dirs, skip=skip,
except TemplateDoesNotExist: )
pass return template, template.origin
raise TemplateDoesNotExist(name) except TemplateDoesNotExist as e:
tried.extend(e.tried)
else:
# RemovedInDjango21Warning: Use old api for non-recursive
# loaders.
try:
return loader(name, dirs)
except TemplateDoesNotExist:
pass
raise TemplateDoesNotExist(name, tried=tried)
def from_string(self, template_code): def from_string(self, template_code):
""" """
@ -234,11 +244,3 @@ class Engine(object):
continue continue
# If we get here, none of the templates could be loaded # If we get here, none of the templates could be loaded
raise TemplateDoesNotExist(', '.join(not_found)) raise TemplateDoesNotExist(', '.join(not_found))
def make_origin(self, display_name, loader, name, dirs):
if self.debug and display_name:
# Inner import to avoid circular dependency
from .loader import LoaderOrigin
return LoaderOrigin(display_name, loader, name, dirs)
else:
return None

View File

@ -4,25 +4,20 @@ from django.utils.deprecation import RemovedInDjango20Warning
from . import engines from . import engines
from .backends.django import DjangoTemplates from .backends.django import DjangoTemplates
from .base import Origin, TemplateDoesNotExist from .base import TemplateDoesNotExist
from .engine import ( from .engine import (
_context_instance_undefined, _dictionary_undefined, _dirs_undefined, _context_instance_undefined, _dictionary_undefined, _dirs_undefined,
) )
from .loaders import base from .loaders import base
class LoaderOrigin(Origin):
def __init__(self, display_name, loader, name, dirs):
super(LoaderOrigin, self).__init__(display_name)
self.loader, self.loadname, self.dirs = loader, name, dirs
def get_template(template_name, dirs=_dirs_undefined, using=None): def get_template(template_name, dirs=_dirs_undefined, using=None):
""" """
Loads and returns a template for the given name. Loads and returns a template for the given name.
Raises TemplateDoesNotExist if no such template exists. Raises TemplateDoesNotExist if no such template exists.
""" """
tried = []
engines = _engine_list(using) engines = _engine_list(using)
for engine in engines: for engine in engines:
try: try:
@ -37,10 +32,10 @@ def get_template(template_name, dirs=_dirs_undefined, using=None):
stacklevel=2) stacklevel=2)
else: else:
return engine.get_template(template_name) return engine.get_template(template_name)
except TemplateDoesNotExist: except TemplateDoesNotExist as e:
pass tried.extend(e.tried)
raise TemplateDoesNotExist(template_name) raise TemplateDoesNotExist(template_name, tried=tried)
def select_template(template_name_list, dirs=_dirs_undefined, using=None): def select_template(template_name_list, dirs=_dirs_undefined, using=None):
@ -51,6 +46,7 @@ def select_template(template_name_list, dirs=_dirs_undefined, using=None):
Raises TemplateDoesNotExist if no such template exists. Raises TemplateDoesNotExist if no such template exists.
""" """
tried = []
engines = _engine_list(using) engines = _engine_list(using)
for template_name in template_name_list: for template_name in template_name_list:
for engine in engines: for engine in engines:
@ -66,11 +62,11 @@ def select_template(template_name_list, dirs=_dirs_undefined, using=None):
stacklevel=2) stacklevel=2)
else: else:
return engine.get_template(template_name) return engine.get_template(template_name)
except TemplateDoesNotExist: except TemplateDoesNotExist as e:
pass tried.extend(e.tried)
if template_name_list: if template_name_list:
raise TemplateDoesNotExist(', '.join(template_name_list)) raise TemplateDoesNotExist(', '.join(template_name_list), tried=tried)
else: else:
raise TemplateDoesNotExist("No template names provided") raise TemplateDoesNotExist("No template names provided")
@ -96,6 +92,7 @@ def render_to_string(template_name, context=None,
return template.render(context, request) return template.render(context, request)
else: else:
tried = []
# Some deprecated arguments were passed - use the legacy code path # Some deprecated arguments were passed - use the legacy code path
for engine in _engine_list(using): for engine in _engine_list(using):
try: try:
@ -126,13 +123,14 @@ def render_to_string(template_name, context=None,
"Skipping template backend %s because its render_to_string " "Skipping template backend %s because its render_to_string "
"method doesn't support the dictionary argument." % "method doesn't support the dictionary argument." %
engine.name, stacklevel=2) engine.name, stacklevel=2)
except TemplateDoesNotExist: except TemplateDoesNotExist as e:
tried.extend(e.tried)
continue continue
if template_name: if template_name:
if isinstance(template_name, (list, tuple)): if isinstance(template_name, (list, tuple)):
template_name = ', '.join(template_name) template_name = ', '.join(template_name)
raise TemplateDoesNotExist(template_name) raise TemplateDoesNotExist(template_name, tried=tried)
else: else:
raise TemplateDoesNotExist("No template names provided") raise TemplateDoesNotExist("No template names provided")

View File

@ -82,6 +82,7 @@ class BlockNode(Node):
class ExtendsNode(Node): class ExtendsNode(Node):
must_be_first = True must_be_first = True
context_key = 'extends_context'
def __init__(self, nodelist, parent_name, template_dirs=None): def __init__(self, nodelist, parent_name, template_dirs=None):
self.nodelist = nodelist self.nodelist = nodelist
@ -92,6 +93,39 @@ class ExtendsNode(Node):
def __repr__(self): def __repr__(self):
return '<ExtendsNode: extends %s>' % self.parent_name.token return '<ExtendsNode: extends %s>' % self.parent_name.token
def find_template(self, template_name, context):
"""
This is a wrapper around engine.find_template(). A history is kept in
the render_context attribute between successive extends calls and
passed as the skip argument. This enables extends to work recursively
without extending the same template twice.
"""
# RemovedInDjango21Warning: If any non-recursive loaders are installed
# do a direct template lookup. If the same template name appears twice,
# raise an exception to avoid system recursion.
for loader in context.template.engine.template_loaders:
if not loader.supports_recursion:
history = context.render_context.setdefault(
self.context_key, [context.template.origin.template_name],
)
if template_name in history:
raise ExtendsError(
"Cannot extend templates recursively when using "
"non-recursive template loaders",
)
template = context.template.engine.get_template(template_name)
history.append(template_name)
return template
history = context.render_context.setdefault(
self.context_key, [context.template.origin],
)
template, origin = context.template.engine.find_template(
template_name, skip=history,
)
history.append(origin)
return template
def get_parent(self, context): def get_parent(self, context):
parent = self.parent_name.resolve(context) parent = self.parent_name.resolve(context)
if not parent: if not parent:
@ -107,7 +141,7 @@ class ExtendsNode(Node):
if isinstance(getattr(parent, 'template', None), Template): if isinstance(getattr(parent, 'template', None), Template):
# parent is a django.template.backends.django.Template # parent is a django.template.backends.django.Template
return parent.template return parent.template
return context.template.engine.get_template(parent) return self.find_template(parent, context)
def render(self, context): def render(self, context):
compiled_parent = self.get_parent(context) compiled_parent = self.get_parent(context)

View File

@ -1,4 +1,8 @@
from django.template.base import Template, TemplateDoesNotExist import warnings
from inspect import getargspec
from django.template.base import Origin, Template, TemplateDoesNotExist
from django.utils.deprecation import RemovedInDjango21Warning
class Loader(object): class Loader(object):
@ -9,15 +13,54 @@ class Loader(object):
self.engine = engine self.engine = engine
def __call__(self, template_name, template_dirs=None): def __call__(self, template_name, template_dirs=None):
# RemovedInDjango21Warning: Allow loaders to be called like functions.
return self.load_template(template_name, template_dirs) return self.load_template(template_name, template_dirs)
def load_template(self, template_name, template_dirs=None): def get_template(self, template_name, template_dirs=None, skip=None):
source, display_name = self.load_template_source( """
template_name, template_dirs) Calls self.get_template_sources() and returns a Template object for
origin = self.engine.make_origin( the first template matching template_name. If skip is provided,
display_name, self.load_template_source, template origins in skip are ignored. This is used to avoid recursion
template_name, template_dirs) during template extending.
"""
tried = []
args = [template_name]
# RemovedInDjango21Warning: Add template_dirs for compatibility with
# old loaders
if 'template_dirs' in getargspec(self.get_template_sources)[0]:
args.append(template_dirs)
for origin in self.get_template_sources(*args):
if skip is not None and origin in skip:
tried.append((origin, 'Skipped'))
continue
try:
contents = self.get_contents(origin)
except TemplateDoesNotExist:
tried.append((origin, 'Source does not exist'))
continue
else:
return Template(
contents, origin, origin.template_name, self.engine,
)
raise TemplateDoesNotExist(template_name, tried=tried)
def load_template(self, template_name, template_dirs=None):
warnings.warn(
'The load_template() method is deprecated. Use get_template() '
'instead.', RemovedInDjango21Warning,
)
source, display_name = self.load_template_source(
template_name, template_dirs,
)
origin = Origin(
name=display_name,
template_name=template_name,
loader=self,
)
try: try:
template = Template(source, origin, template_name, self.engine) template = Template(source, origin, template_name, self.engine)
except TemplateDoesNotExist: except TemplateDoesNotExist:
@ -29,14 +72,23 @@ class Loader(object):
else: else:
return template, None return template, None
def load_template_source(self, template_name, template_dirs=None): def get_template_sources(self, template_name):
""" """
Returns a tuple containing the source and origin for the given An iterator that yields possible matching template paths for a
template name. template name.
""" """
raise NotImplementedError( raise NotImplementedError(
"subclasses of Loader must provide " 'subclasses of Loader must provide a get_template_sources() method'
"a load_template_source() method") )
def load_template_source(self, template_name, template_dirs=None):
"""
RemovedInDjango21Warning: Returns a tuple containing the source and
origin for the given template name.
"""
raise NotImplementedError(
'subclasses of Loader must provide a load_template_source() method'
)
def reset(self): def reset(self):
""" """
@ -44,3 +96,11 @@ class Loader(object):
templates or cached loader modules). templates or cached loader modules).
""" """
pass pass
@property
def supports_recursion(self):
"""
RemovedInDjango21Warning: This is an internal property used by the
ExtendsNode during the deprecation of non-recursive loaders.
"""
return hasattr(self, 'get_contents')

View File

@ -4,8 +4,11 @@ to load templates from them in order, caching the result.
""" """
import hashlib import hashlib
import warnings
from inspect import getargspec
from django.template.base import Template, TemplateDoesNotExist from django.template.base import Origin, Template, TemplateDoesNotExist
from django.utils.deprecation import RemovedInDjango21Warning
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
from .base import Loader as BaseLoader from .base import Loader as BaseLoader
@ -15,20 +18,84 @@ class Loader(BaseLoader):
def __init__(self, engine, loaders): def __init__(self, engine, loaders):
self.template_cache = {} self.template_cache = {}
self.find_template_cache = {} self.find_template_cache = {} # RemovedInDjango21Warning
self.get_template_cache = {}
self.loaders = engine.get_template_loaders(loaders) self.loaders = engine.get_template_loaders(loaders)
super(Loader, self).__init__(engine) super(Loader, self).__init__(engine)
def cache_key(self, template_name, template_dirs): def get_contents(self, origin):
if template_dirs: return origin.loader.get_contents(origin)
# If template directories were specified, use a hash to differentiate
return '-'.join([template_name, hashlib.sha1(force_bytes('|'.join(template_dirs))).hexdigest()]) def get_template(self, template_name, template_dirs=None, skip=None):
key = self.cache_key(template_name, template_dirs, skip)
cached = self.get_template_cache.get(key)
if cached:
if isinstance(cached, TemplateDoesNotExist):
raise cached
return cached
try:
template = super(Loader, self).get_template(
template_name, template_dirs, skip,
)
except TemplateDoesNotExist as e:
self.get_template_cache[key] = e
raise
else: else:
return template_name self.get_template_cache[key] = template
return template
def get_template_sources(self, template_name, template_dirs=None):
for loader in self.loaders:
args = [template_name]
# RemovedInDjango21Warning: Add template_dirs for compatibility
# with old loaders
if 'template_dirs' in getargspec(loader.get_template_sources)[0]:
args.append(template_dirs)
for origin in loader.get_template_sources(*args):
yield origin
def cache_key(self, template_name, template_dirs, skip=None):
"""
Generate a cache key for the template name, dirs, and skip.
If skip is provided, only origins that match template_name are included
in the cache key. This ensures each template is only parsed and cached
once if contained in different extend chains like:
x -> a -> a
y -> a -> a
z -> a -> a
"""
dirs_prefix = ''
skip_prefix = ''
if skip:
matching = [origin.name for origin in skip if origin.template_name == template_name]
if matching:
skip_prefix = self.generate_hash(matching)
if template_dirs:
dirs_prefix = self.generate_hash(template_dirs)
return ("%s-%s-%s" % (template_name, skip_prefix, dirs_prefix)).strip('-')
def generate_hash(self, values):
return hashlib.sha1(force_bytes('|'.join(values))).hexdigest()
@property
def supports_recursion(self):
"""
RemovedInDjango21Warning: This is an internal property used by the
ExtendsNode during the deprecation of non-recursive loaders.
"""
return all(hasattr(loader, 'get_contents') for loader in self.loaders)
def find_template(self, name, dirs=None): def find_template(self, name, dirs=None):
""" """
Helper method. Lookup the template :param name: in all the configured loaders RemovedInDjango21Warning: An internal method to lookup the template
name in all the configured loaders.
""" """
key = self.cache_key(name, dirs) key = self.cache_key(name, dirs)
try: try:
@ -41,7 +108,11 @@ class Loader(BaseLoader):
except TemplateDoesNotExist: except TemplateDoesNotExist:
pass pass
else: else:
origin = self.engine.make_origin(display_name, loader, name, dirs) origin = Origin(
name=display_name,
template_name=name,
loader=loader,
)
result = template, origin result = template, origin
break break
self.find_template_cache[key] = result self.find_template_cache[key] = result
@ -52,6 +123,10 @@ class Loader(BaseLoader):
raise TemplateDoesNotExist(name) raise TemplateDoesNotExist(name)
def load_template(self, template_name, template_dirs=None): def load_template(self, template_name, template_dirs=None):
warnings.warn(
'The load_template() method is deprecated. Use get_template() '
'instead.', RemovedInDjango21Warning,
)
key = self.cache_key(template_name, template_dirs) key = self.cache_key(template_name, template_dirs)
template_tuple = self.template_cache.get(key) template_tuple = self.template_cache.get(key)
# A cached previous failure: # A cached previous failure:
@ -74,4 +149,5 @@ class Loader(BaseLoader):
def reset(self): def reset(self):
"Empty the template cache." "Empty the template cache."
self.template_cache.clear() self.template_cache.clear()
self.find_template_cache.clear() self.find_template_cache.clear() # RemovedInDjango21Warning
self.get_template_cache.clear()

View File

@ -1,9 +1,12 @@
# Wrapper for loading templates from eggs via pkg_resources.resource_string. # Wrapper for loading templates from eggs via pkg_resources.resource_string.
from __future__ import unicode_literals from __future__ import unicode_literals
import warnings
from django.apps import apps from django.apps import apps
from django.template.base import TemplateDoesNotExist from django.template.base import Origin, TemplateDoesNotExist
from django.utils import six from django.utils import six
from django.utils.deprecation import RemovedInDjango21Warning
from .base import Loader as BaseLoader from .base import Loader as BaseLoader
@ -13,6 +16,14 @@ except ImportError:
resource_string = None resource_string = None
class EggOrigin(Origin):
def __init__(self, app_name, pkg_name, *args, **kwargs):
self.app_name = app_name
self.pkg_name = pkg_name
return super(EggOrigin, self).__init__(*args, **kwargs)
class Loader(BaseLoader): class Loader(BaseLoader):
def __init__(self, engine): def __init__(self, engine):
@ -20,19 +31,42 @@ class Loader(BaseLoader):
raise RuntimeError("Setuptools must be installed to use the egg loader") raise RuntimeError("Setuptools must be installed to use the egg loader")
super(Loader, self).__init__(engine) super(Loader, self).__init__(engine)
def get_contents(self, origin):
try:
source = resource_string(origin.app_name, origin.pkg_name)
except:
raise TemplateDoesNotExist(origin)
if six.PY2:
source = source.decode(self.engine.file_charset)
return source
def get_template_sources(self, template_name):
pkg_name = 'templates/' + template_name
for app_config in apps.get_app_configs():
yield EggOrigin(
app_name=app_config.name,
pkg_name=pkg_name,
name="egg:%s:%s" % (app_config.name, pkg_name),
template_name=template_name,
loader=self,
)
def load_template_source(self, template_name, template_dirs=None): def load_template_source(self, template_name, template_dirs=None):
""" """
Loads templates from Python eggs via pkg_resource.resource_string. Loads templates from Python eggs via pkg_resource.resource_string.
For every installed app, it tries to get the resource (app, template_name). For every installed app, it tries to get the resource (app, template_name).
""" """
pkg_name = 'templates/' + template_name warnings.warn(
for app_config in apps.get_app_configs(): 'The load_template_sources() method is deprecated. Use '
'get_template() or get_contents() instead.',
RemovedInDjango21Warning,
)
for origin in self.get_template_sources(template_name):
try: try:
resource = resource_string(app_config.name, pkg_name) return self.get_contents(origin), origin.name
except Exception: except TemplateDoesNotExist:
continue pass
if six.PY2:
resource = resource.decode(self.engine.file_charset)
return (resource, 'egg:%s:%s' % (app_config.name, pkg_name))
raise TemplateDoesNotExist(template_name) raise TemplateDoesNotExist(template_name)

View File

@ -4,10 +4,12 @@ Wrapper for loading templates from the filesystem.
import errno import errno
import io import io
import warnings
from django.core.exceptions import SuspiciousFileOperation from django.core.exceptions import SuspiciousFileOperation
from django.template.base import TemplateDoesNotExist from django.template.base import Origin, TemplateDoesNotExist
from django.utils._os import safe_join from django.utils._os import safe_join
from django.utils.deprecation import RemovedInDjango21Warning
from .base import Loader as BaseLoader from .base import Loader as BaseLoader
@ -17,28 +19,46 @@ class Loader(BaseLoader):
def get_dirs(self): def get_dirs(self):
return self.engine.dirs return self.engine.dirs
def get_contents(self, origin):
try:
with io.open(origin.name, encoding=self.engine.file_charset) as fp:
return fp.read()
except IOError as e:
if e.errno == errno.ENOENT:
raise TemplateDoesNotExist(origin)
raise
def get_template_sources(self, template_name, template_dirs=None): def get_template_sources(self, template_name, template_dirs=None):
""" """
Returns the absolute paths to "template_name", when appended to each Return an Origin object pointing to an absolute path in each directory
directory in "template_dirs". Any paths that don't lie inside one of the in template_dirs. For security reasons, if a path doesn't lie inside
template dirs are excluded from the result set, for security reasons. one of the template_dirs it is excluded from the result set.
""" """
if not template_dirs: if not template_dirs:
template_dirs = self.get_dirs() template_dirs = self.get_dirs()
for template_dir in template_dirs: for template_dir in template_dirs:
try: try:
yield safe_join(template_dir, template_name) name = safe_join(template_dir, template_name)
except SuspiciousFileOperation: except SuspiciousFileOperation:
# The joined path was located outside of this template_dir # The joined path was located outside of this template_dir
# (it might be inside another one, so this isn't fatal). # (it might be inside another one, so this isn't fatal).
pass continue
yield Origin(
name=name,
template_name=template_name,
loader=self,
)
def load_template_source(self, template_name, template_dirs=None): def load_template_source(self, template_name, template_dirs=None):
for filepath in self.get_template_sources(template_name, template_dirs): warnings.warn(
'The load_template_sources() method is deprecated. Use '
'get_template() or get_contents() instead.',
RemovedInDjango21Warning,
)
for origin in self.get_template_sources(template_name, template_dirs):
try: try:
with io.open(filepath, encoding=self.engine.file_charset) as fp: return self.get_contents(origin), origin.name
return fp.read(), filepath except TemplateDoesNotExist:
except IOError as e: pass
if e.errno != errno.ENOENT:
raise
raise TemplateDoesNotExist(template_name) raise TemplateDoesNotExist(template_name)

View File

@ -2,7 +2,10 @@
Wrapper for loading templates from a plain Python dict. Wrapper for loading templates from a plain Python dict.
""" """
from django.template.base import TemplateDoesNotExist import warnings
from django.template.base import Origin, TemplateDoesNotExist
from django.utils.deprecation import RemovedInDjango21Warning
from .base import Loader as BaseLoader from .base import Loader as BaseLoader
@ -13,7 +16,25 @@ class Loader(BaseLoader):
self.templates_dict = templates_dict self.templates_dict = templates_dict
super(Loader, self).__init__(engine) super(Loader, self).__init__(engine)
def get_contents(self, origin):
try:
return self.templates_dict[origin.name]
except KeyError:
raise TemplateDoesNotExist(origin)
def get_template_sources(self, template_name):
yield Origin(
name=template_name,
template_name=template_name,
loader=self,
)
def load_template_source(self, template_name, template_dirs=None): def load_template_source(self, template_name, template_dirs=None):
warnings.warn(
'The load_template_sources() method is deprecated. Use '
'get_template() or get_contents() instead.',
RemovedInDjango21Warning,
)
try: try:
return self.templates_dict[template_name], template_name return self.templates_dict[template_name], template_name
except KeyError: except KeyError:

View File

@ -37,6 +37,26 @@ details on these changes.
* The ``GeoManager`` and ``GeoQuerySet`` classes will be removed. * The ``GeoManager`` and ``GeoQuerySet`` classes will be removed.
* The ``supports_recursion`` check for template loaders will be removed from:
* ``django.template.engine.Engine.find_template()``
* ``django.template.loader_tags.ExtendsNode.find_template()``
* ``django.template.loaders.base.Loader.supports_recursion()``
* ``django.template.loaders.cached.Loader.supports_recursion()``
* The ``load_template and ``load_template_sources`` template loader methods
will be removed.
* The ``template_dirs`` argument for template loaders will be removed:
* ``django.template.loaders.base.Loader.get_template()``
* ``django.template.loaders.cached.Loader.cache_key()``
* ``django.template.loaders.cached.Loader.get_template()``
* ``django.template.loaders.cached.Loader.get_template_sources()``
* ``django.template.loaders.filesystem.Loader.get_template_sources()``
* The ``django.template.loaders.base.Loader.__call__`` method will be removed.
.. _deprecation-removed-in-2.0: .. _deprecation-removed-in-2.0:
2.0 2.0

View File

@ -33,8 +33,12 @@ class TemplateLoaderTests(SimpleTestCase):
self.assertEqual(template.render(), "Hello! (Django templates)\n") self.assertEqual(template.render(), "Hello! (Django templates)\n")
def test_get_template_not_found(self): def test_get_template_not_found(self):
with self.assertRaises(TemplateDoesNotExist): with self.assertRaises(TemplateDoesNotExist) as e:
get_template("template_loader/unknown.html") get_template("template_loader/unknown.html")
self.assertEqual(
e.exception.tried[-1][0].template_name,
'template_loader/unknown.html',
)
def test_select_template_first_engine(self): def test_select_template_first_engine(self):
template = select_template(["template_loader/unknown.html", template = select_template(["template_loader/unknown.html",
@ -56,9 +60,17 @@ class TemplateLoaderTests(SimpleTestCase):
select_template([]) select_template([])
def test_select_template_not_found(self): def test_select_template_not_found(self):
with self.assertRaises(TemplateDoesNotExist): with self.assertRaises(TemplateDoesNotExist) as e:
select_template(["template_loader/unknown.html", select_template(["template_loader/unknown.html",
"template_loader/missing.html"]) "template_loader/missing.html"])
self.assertEqual(
e.exception.tried[0][0].template_name,
'template_loader/unknown.html',
)
self.assertEqual(
e.exception.tried[-1][0].template_name,
'template_loader/missing.html',
)
def test_select_template_tries_all_engines_before_names(self): def test_select_template_tries_all_engines_before_names(self):
template = select_template(["template_loader/goodbye.html", template = select_template(["template_loader/goodbye.html",
@ -83,8 +95,12 @@ class TemplateLoaderTests(SimpleTestCase):
self.assertEqual(content, "Hello! (Django templates)\n") self.assertEqual(content, "Hello! (Django templates)\n")
def test_render_to_string_not_found(self): def test_render_to_string_not_found(self):
with self.assertRaises(TemplateDoesNotExist): with self.assertRaises(TemplateDoesNotExist) as e:
render_to_string("template_loader/unknown.html") render_to_string("template_loader/unknown.html")
self.assertEqual(
e.exception.tried[-1][0].template_name,
'template_loader/unknown.html',
)
def test_render_to_string_with_list_first_engine(self): def test_render_to_string_with_list_first_engine(self):
content = render_to_string(["template_loader/unknown.html", content = render_to_string(["template_loader/unknown.html",
@ -106,9 +122,17 @@ class TemplateLoaderTests(SimpleTestCase):
render_to_string([]) render_to_string([])
def test_render_to_string_with_list_not_found(self): def test_render_to_string_with_list_not_found(self):
with self.assertRaises(TemplateDoesNotExist): with self.assertRaises(TemplateDoesNotExist) as e:
render_to_string(["template_loader/unknown.html", render_to_string(["template_loader/unknown.html",
"template_loader/missing.html"]) "template_loader/missing.html"])
self.assertEqual(
e.exception.tried[0][0].template_name,
'template_loader/unknown.html',
)
self.assertEqual(
e.exception.tried[-1][0].template_name,
'template_loader/missing.html',
)
def test_render_to_string_with_list_tries_all_engines_before_names(self): def test_render_to_string_with_list_tries_all_engines_before_names(self):
content = render_to_string(["template_loader/goodbye.html", content = render_to_string(["template_loader/goodbye.html",

View File

@ -0,0 +1 @@
{% extends "missing.html" %}

View File

@ -0,0 +1,3 @@
{% extends "two.html" %}
{% block content %}{{ block.super }} one{% endblock %}

View File

@ -0,0 +1 @@
{% extends "recursive.html" %}

View File

@ -0,0 +1,3 @@
{% extends "recursive.html" %}
{% block content %}{{ block.super }} fs/recursive{% endblock %}

View File

@ -0,0 +1 @@
{% extends "self.html" %}

View File

@ -0,0 +1 @@
{% block content %}three{% endblock %}

View File

@ -0,0 +1,3 @@
{% extends "three.html" %}
{% block content %}{{ block.super }} two{% endblock %}

View File

@ -0,0 +1,3 @@
{% extends "recursive.html" %}
{% block content %}{{ block.super }} fs2/recursive{% endblock %}

View File

@ -0,0 +1 @@
{% block content %}fs3/recursive{% endblock %}

View File

@ -55,7 +55,7 @@ class LoaderTests(SimpleTestCase):
def test_origin(self): def test_origin(self):
engine = Engine(dirs=[TEMPLATE_DIR], debug=True) engine = Engine(dirs=[TEMPLATE_DIR], debug=True)
template = engine.get_template('index.html') template = engine.get_template('index.html')
self.assertEqual(template.origin.loadname, 'index.html') self.assertEqual(template.origin.template_name, 'index.html')
def test_loader_priority(self): def test_loader_priority(self):
""" """

View File

@ -0,0 +1,178 @@
import os
from django.template import Context, Engine, TemplateDoesNotExist
from django.template.loader_tags import ExtendsError
from django.template.loaders.base import Loader
from django.test import SimpleTestCase, ignore_warnings
from django.utils.deprecation import RemovedInDjango21Warning
from .utils import ROOT
RECURSIVE = os.path.join(ROOT, 'recursive_templates')
class ExtendsBehaviorTests(SimpleTestCase):
def test_normal_extend(self):
engine = Engine(dirs=[os.path.join(RECURSIVE, 'fs')])
template = engine.get_template('one.html')
output = template.render(Context({}))
self.assertEqual(output.strip(), 'three two one')
def test_extend_recursive(self):
engine = Engine(dirs=[
os.path.join(RECURSIVE, 'fs'),
os.path.join(RECURSIVE, 'fs2'),
os.path.join(RECURSIVE, 'fs3'),
])
template = engine.get_template('recursive.html')
output = template.render(Context({}))
self.assertEqual(output.strip(), 'fs3/recursive fs2/recursive fs/recursive')
def test_extend_missing(self):
engine = Engine(dirs=[os.path.join(RECURSIVE, 'fs')])
template = engine.get_template('extend-missing.html')
with self.assertRaises(TemplateDoesNotExist) as e:
template.render(Context({}))
tried = e.exception.tried
self.assertEqual(len(tried), 1)
self.assertEqual(tried[0][0].template_name, 'missing.html')
def test_recursive_multiple_loaders(self):
engine = Engine(
dirs=[os.path.join(RECURSIVE, 'fs')],
loaders=[
('django.template.loaders.locmem.Loader', {
'one.html': '{% extends "one.html" %}{% block content %}{{ block.super }} locmem-one{% endblock %}',
'two.html': '{% extends "two.html" %}{% block content %}{{ block.super }} locmem-two{% endblock %}',
'three.html': (
'{% extends "three.html" %}{% block content %}{{ block.super }} locmem-three{% endblock %}'
),
}),
'django.template.loaders.filesystem.Loader',
],
)
template = engine.get_template('one.html')
output = template.render(Context({}))
self.assertEqual(output.strip(), 'three locmem-three two locmem-two one locmem-one')
def test_extend_self_error(self):
"""
Catch if a template extends itself and no other matching
templates are found.
"""
engine = Engine(dirs=[os.path.join(RECURSIVE, 'fs')])
template = engine.get_template('self.html')
with self.assertRaises(TemplateDoesNotExist):
template.render(Context({}))
def test_extend_cached(self):
engine = Engine(
dirs=[
os.path.join(RECURSIVE, 'fs'),
os.path.join(RECURSIVE, 'fs2'),
os.path.join(RECURSIVE, 'fs3'),
],
loaders=[
('django.template.loaders.cached.Loader', [
'django.template.loaders.filesystem.Loader',
]),
],
)
template = engine.get_template('recursive.html')
output = template.render(Context({}))
self.assertEqual(output.strip(), 'fs3/recursive fs2/recursive fs/recursive')
cache = engine.template_loaders[0].get_template_cache
self.assertEqual(len(cache), 3)
self.assertTrue(cache['recursive.html'].origin.name.endswith('fs/recursive.html'))
# Render another path that uses the same templates from the cache
template = engine.get_template('other-recursive.html')
output = template.render(Context({}))
self.assertEqual(output.strip(), 'fs3/recursive fs2/recursive fs/recursive')
# Template objects should not be duplicated.
self.assertEqual(len(cache), 4)
self.assertTrue(cache['other-recursive.html'].origin.name.endswith('fs/other-recursive.html'))
def test_unique_history_per_loader(self):
"""
Extending should continue even if two loaders return the same
name for a template.
"""
engine = Engine(
loaders=[
['django.template.loaders.locmem.Loader', {
'base.html': '{% extends "base.html" %}{% block content %}{{ block.super }} loader1{% endblock %}',
}],
['django.template.loaders.locmem.Loader', {
'base.html': '{% block content %}loader2{% endblock %}',
}],
]
)
template = engine.get_template('base.html')
output = template.render(Context({}))
self.assertEqual(output.strip(), 'loader2 loader1')
class NonRecursiveLoader(Loader):
def __init__(self, engine, templates_dict):
self.templates_dict = templates_dict
super(NonRecursiveLoader, self).__init__(engine)
def load_template_source(self, template_name, template_dirs=None):
try:
return self.templates_dict[template_name], template_name
except KeyError:
raise TemplateDoesNotExist(template_name)
@ignore_warnings(category=RemovedInDjango21Warning)
class NonRecursiveLoaderExtendsTests(SimpleTestCase):
loaders = [
('template_tests.test_extends.NonRecursiveLoader', {
'base.html': 'base',
'index.html': '{% extends "base.html" %}',
'recursive.html': '{% extends "recursive.html" %}',
'other-recursive.html': '{% extends "recursive.html" %}',
'a.html': '{% extends "b.html" %}',
'b.html': '{% extends "a.html" %}',
}),
]
def test_extend(self):
engine = Engine(loaders=self.loaders)
output = engine.render_to_string('index.html')
self.assertEqual(output, 'base')
def test_extend_cached(self):
engine = Engine(loaders=[
('django.template.loaders.cached.Loader', self.loaders),
])
output = engine.render_to_string('index.html')
self.assertEqual(output, 'base')
cache = engine.template_loaders[0].template_cache
self.assertTrue('base.html' in cache)
self.assertTrue('index.html' in cache)
# Render a second time from cache
output = engine.render_to_string('index.html')
self.assertEqual(output, 'base')
def test_extend_error(self):
engine = Engine(loaders=self.loaders)
msg = 'Cannot extend templates recursively when using non-recursive template loaders'
with self.assertRaisesMessage(ExtendsError, msg):
engine.render_to_string('recursive.html')
with self.assertRaisesMessage(ExtendsError, msg):
engine.render_to_string('other-recursive.html')
with self.assertRaisesMessage(ExtendsError, msg):
engine.render_to_string('a.html')

View File

@ -10,8 +10,9 @@ from contextlib import contextmanager
from django.template import Context, TemplateDoesNotExist from django.template import Context, TemplateDoesNotExist
from django.template.engine import Engine from django.template.engine import Engine
from django.test import SimpleTestCase, override_settings from django.test import SimpleTestCase, ignore_warnings, override_settings
from django.utils import six from django.utils import six
from django.utils.deprecation import RemovedInDjango21Warning
from .utils import TEMPLATE_DIR from .utils import TEMPLATE_DIR
@ -23,8 +24,9 @@ except ImportError:
class CachedLoaderTests(SimpleTestCase): class CachedLoaderTests(SimpleTestCase):
def create_engine(self, **kwargs): def setUp(self):
return Engine( self.engine = Engine(
dirs=[TEMPLATE_DIR],
loaders=[ loaders=[
('django.template.loaders.cached.Loader', [ ('django.template.loaders.cached.Loader', [
'django.template.loaders.filesystem.Loader', 'django.template.loaders.filesystem.Loader',
@ -32,26 +34,47 @@ class CachedLoaderTests(SimpleTestCase):
], ],
) )
def test_templatedir_caching(self): def test_get_template(self):
""" template = self.engine.get_template('index.html')
#13573 -- Template directories should be part of the cache key. self.assertEqual(template.origin.name, os.path.join(TEMPLATE_DIR, 'index.html'))
""" self.assertEqual(template.origin.template_name, 'index.html')
engine = self.create_engine() self.assertEqual(template.origin.loader, self.engine.template_loaders[0].loaders[0])
# Retrieve a template specifying a template directory to check cache = self.engine.template_loaders[0].get_template_cache
t1, name = engine.find_template('test.html', (os.path.join(TEMPLATE_DIR, 'first'),)) self.assertEqual(cache['index.html'], template)
# Now retrieve the same template name, but from a different directory
t2, name = engine.find_template('test.html', (os.path.join(TEMPLATE_DIR, 'second'),))
# The two templates should not have the same content # Run a second time from cache
self.assertNotEqual(t1.render(Context({})), t2.render(Context({}))) template = self.engine.get_template('index.html')
self.assertEqual(template.origin.name, os.path.join(TEMPLATE_DIR, 'index.html'))
self.assertEqual(template.origin.template_name, 'index.html')
self.assertEqual(template.origin.loader, self.engine.template_loaders[0].loaders[0])
def test_missing_template_is_cached(self): def test_get_template_missing(self):
with self.assertRaises(TemplateDoesNotExist):
self.engine.get_template('doesnotexist.html')
e = self.engine.template_loaders[0].get_template_cache['doesnotexist.html']
self.assertEqual(e.args[0], 'doesnotexist.html')
@ignore_warnings(category=RemovedInDjango21Warning)
def test_load_template(self):
loader = self.engine.template_loaders[0]
template, origin = loader.load_template('index.html')
self.assertEqual(template.origin.template_name, 'index.html')
cache = self.engine.template_loaders[0].template_cache
self.assertEqual(cache['index.html'][0], template)
# Run a second time from cache
loader = self.engine.template_loaders[0]
source, name = loader.load_template('index.html')
self.assertEqual(template.origin.template_name, 'index.html')
@ignore_warnings(category=RemovedInDjango21Warning)
def test_load_template_missing(self):
""" """
#19949 -- TemplateDoesNotExist exceptions should be cached. #19949 -- TemplateDoesNotExist exceptions should be cached.
""" """
engine = self.create_engine() loader = self.engine.template_loaders[0]
loader = engine.template_loaders[0]
self.assertFalse('missing.html' in loader.template_cache) self.assertFalse('missing.html' in loader.template_cache)
@ -64,6 +87,18 @@ class CachedLoaderTests(SimpleTestCase):
"Cached loader failed to cache the TemplateDoesNotExist exception", "Cached loader failed to cache the TemplateDoesNotExist exception",
) )
def test_templatedir_caching(self):
"""
#13573 -- Template directories should be part of the cache key.
"""
# Retrieve a template specifying a template directory to check
t1, name = self.engine.find_template('test.html', (os.path.join(TEMPLATE_DIR, 'first'),))
# Now retrieve the same template name, but from a different directory
t2, name = self.engine.find_template('test.html', (os.path.join(TEMPLATE_DIR, 'second'),))
# The two templates should not have the same content
self.assertNotEqual(t1.render(Context({})), t2.render(Context({})))
@unittest.skipUnless(pkg_resources, 'setuptools is not installed') @unittest.skipUnless(pkg_resources, 'setuptools is not installed')
class EggLoaderTests(SimpleTestCase): class EggLoaderTests(SimpleTestCase):
@ -117,22 +152,43 @@ class EggLoaderTests(SimpleTestCase):
del sys.modules[name] del sys.modules[name]
del pkg_resources._provider_factories[MockLoader] del pkg_resources._provider_factories[MockLoader]
def setUp(self): @classmethod
engine = Engine(loaders=[ def setUpClass(cls):
cls.engine = Engine(loaders=[
'django.template.loaders.eggs.Loader', 'django.template.loaders.eggs.Loader',
]) ])
self.loader = engine.template_loaders[0] cls.loader = cls.engine.template_loaders[0]
super(EggLoaderTests, cls).setUpClass()
def test_existing(self): def test_get_template(self):
templates = { templates = {
os.path.normcase('templates/y.html'): six.StringIO("y"), os.path.normcase('templates/y.html'): six.StringIO("y"),
} }
with self.create_egg('egg', templates): with self.create_egg('egg', templates):
with override_settings(INSTALLED_APPS=['egg']): with override_settings(INSTALLED_APPS=['egg']):
contents, template_name = self.loader.load_template_source("y.html") template = self.engine.get_template("y.html")
self.assertEqual(contents, "y")
self.assertEqual(template_name, "egg:egg:templates/y.html") self.assertEqual(template.origin.name, 'egg:egg:templates/y.html')
self.assertEqual(template.origin.template_name, 'y.html')
self.assertEqual(template.origin.loader, self.engine.template_loaders[0])
output = template.render(Context({}))
self.assertEqual(output, "y")
@ignore_warnings(category=RemovedInDjango21Warning)
def test_load_template_source(self):
loader = self.engine.template_loaders[0]
templates = {
os.path.normcase('templates/y.html'): six.StringIO("y"),
}
with self.create_egg('egg', templates):
with override_settings(INSTALLED_APPS=['egg']):
source, name = loader.load_template_source('y.html')
self.assertEqual(source.strip(), 'y')
self.assertEqual(name, 'egg:egg:templates/y.html')
def test_non_existing(self): def test_non_existing(self):
""" """
@ -141,7 +197,7 @@ class EggLoaderTests(SimpleTestCase):
with self.create_egg('egg', {}): with self.create_egg('egg', {}):
with override_settings(INSTALLED_APPS=['egg']): with override_settings(INSTALLED_APPS=['egg']):
with self.assertRaises(TemplateDoesNotExist): with self.assertRaises(TemplateDoesNotExist):
self.loader.load_template_source("not-existing.html") self.engine.get_template('not-existing.html')
def test_not_installed(self): def test_not_installed(self):
""" """
@ -153,13 +209,15 @@ class EggLoaderTests(SimpleTestCase):
with self.create_egg('egg', templates): with self.create_egg('egg', templates):
with self.assertRaises(TemplateDoesNotExist): with self.assertRaises(TemplateDoesNotExist):
self.loader.load_template_source("y.html") self.engine.get_template('y.html')
class FileSystemLoaderTests(SimpleTestCase): class FileSystemLoaderTests(SimpleTestCase):
def setUp(self): @classmethod
self.engine = Engine(dirs=[TEMPLATE_DIR]) def setUpClass(cls):
cls.engine = Engine(dirs=[TEMPLATE_DIR])
super(FileSystemLoaderTests, cls).setUpClass()
@contextmanager @contextmanager
def set_dirs(self, dirs): def set_dirs(self, dirs):
@ -177,13 +235,27 @@ class FileSystemLoaderTests(SimpleTestCase):
def check_sources(path, expected_sources): def check_sources(path, expected_sources):
expected_sources = [os.path.abspath(s) for s in expected_sources] expected_sources = [os.path.abspath(s) for s in expected_sources]
self.assertEqual( self.assertEqual(
list(loader.get_template_sources(path)), [origin.name for origin in loader.get_template_sources(path)],
expected_sources, expected_sources,
) )
with self.set_dirs(dirs): with self.set_dirs(dirs):
yield check_sources yield check_sources
def test_get_template(self):
template = self.engine.get_template('index.html')
self.assertEqual(template.origin.name, os.path.join(TEMPLATE_DIR, 'index.html'))
self.assertEqual(template.origin.template_name, 'index.html')
self.assertEqual(template.origin.loader, self.engine.template_loaders[0])
self.assertEqual(template.origin.loader_name, 'django.template.loaders.filesystem.Loader')
@ignore_warnings(category=RemovedInDjango21Warning)
def test_load_template_source(self):
loader = self.engine.template_loaders[0]
source, name = loader.load_template_source('index.html')
self.assertEqual(source.strip(), 'index')
self.assertEqual(name, os.path.join(TEMPLATE_DIR, 'index.html'))
def test_directory_security(self): def test_directory_security(self):
with self.source_checker(['/dir1', '/dir2']) as check_sources: with self.source_checker(['/dir1', '/dir2']) as check_sources:
check_sources('index.html', ['/dir1/index.html', '/dir2/index.html']) check_sources('index.html', ['/dir1/index.html', '/dir2/index.html'])
@ -248,18 +320,56 @@ class FileSystemLoaderTests(SimpleTestCase):
self.engine.get_template('first') self.engine.get_template('first')
class AppDirectoriesLoaderTest(SimpleTestCase): class AppDirectoriesLoaderTests(SimpleTestCase):
def setUp(self): @classmethod
self.engine = Engine( def setUpClass(cls):
cls.engine = Engine(
loaders=['django.template.loaders.app_directories.Loader'], loaders=['django.template.loaders.app_directories.Loader'],
) )
super(AppDirectoriesLoaderTests, cls).setUpClass()
@override_settings(INSTALLED_APPS=['template_tests']) @override_settings(INSTALLED_APPS=['template_tests'])
def test_load_template(self): def test_get_template(self):
self.engine.get_template('index.html') template = self.engine.get_template('index.html')
self.assertEqual(template.origin.name, os.path.join(TEMPLATE_DIR, 'index.html'))
self.assertEqual(template.origin.template_name, 'index.html')
self.assertEqual(template.origin.loader, self.engine.template_loaders[0])
@ignore_warnings(category=RemovedInDjango21Warning)
@override_settings(INSTALLED_APPS=['template_tests'])
def test_load_template_source(self):
loader = self.engine.template_loaders[0]
source, name = loader.load_template_source('index.html')
self.assertEqual(source.strip(), 'index')
self.assertEqual(name, os.path.join(TEMPLATE_DIR, 'index.html'))
@override_settings(INSTALLED_APPS=[]) @override_settings(INSTALLED_APPS=[])
def test_not_installed(self): def test_not_installed(self):
with self.assertRaises(TemplateDoesNotExist): with self.assertRaises(TemplateDoesNotExist):
self.engine.get_template('index.html') self.engine.get_template('index.html')
class LocmemLoaderTests(SimpleTestCase):
@classmethod
def setUpClass(cls):
cls.engine = Engine(
loaders=[('django.template.loaders.locmem.Loader', {
'index.html': 'index',
})],
)
super(LocmemLoaderTests, cls).setUpClass()
def test_get_template(self):
template = self.engine.get_template('index.html')
self.assertEqual(template.origin.name, 'index.html')
self.assertEqual(template.origin.template_name, 'index.html')
self.assertEqual(template.origin.loader, self.engine.template_loaders[0])
@ignore_warnings(category=RemovedInDjango21Warning)
def test_load_template_source(self):
loader = self.engine.template_loaders[0]
source, name = loader.load_template_source('index.html')
self.assertEqual(source.strip(), 'index')
self.assertEqual(name, 'index.html')

View File

@ -6,6 +6,7 @@ import sys
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.core import urlresolvers from django.core import urlresolvers
from django.template import Context, Engine, TemplateSyntaxError from django.template import Context, Engine, TemplateSyntaxError
from django.template.base import UNKNOWN_SOURCE
from django.test import SimpleTestCase, override_settings from django.test import SimpleTestCase, override_settings
@ -13,7 +14,9 @@ class TemplateTests(SimpleTestCase):
def test_string_origin(self): def test_string_origin(self):
template = Engine().from_string('string template') template = Engine().from_string('string template')
self.assertEqual(template.origin.source, 'string template') self.assertEqual(template.origin.name, UNKNOWN_SOURCE)
self.assertEqual(template.origin.loader_name, None)
self.assertEqual(template.source, 'string template')
@override_settings(SETTINGS_MODULE=None) @override_settings(SETTINGS_MODULE=None)
def test_url_reverse_no_settings_module(self): def test_url_reverse_no_settings_module(self):