summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPreston Timmons <prestontimmons@gmail.com>2015-03-03 15:48:26 -0600
committerPreston Timmons <prestontimmons@gmail.com>2015-04-22 14:49:00 -0500
commitfc2147152637e21bc73f991b50fa06254af02739 (patch)
treec9f80c73aabf680e67c6646361d8de3a29a62f93
parent1b1b58bc7b2917b203ee3eb3b9e8b3a1fafaee64 (diff)
Fixed #15053 -- Enabled recursive template loading.
-rw-r--r--django/template/__init__.py4
-rw-r--r--django/template/base.py38
-rw-r--r--django/template/engine.py34
-rw-r--r--django/template/loader.py28
-rw-r--r--django/template/loader_tags.py36
-rw-r--r--django/template/loaders/base.py80
-rw-r--r--django/template/loaders/cached.py96
-rw-r--r--django/template/loaders/eggs.py52
-rw-r--r--django/template/loaders/filesystem.py44
-rw-r--r--django/template/loaders/locmem.py23
-rw-r--r--docs/internals/deprecation.txt20
-rw-r--r--tests/template_loader/tests.py32
-rw-r--r--tests/template_tests/recursive_templates/fs/extend-missing.html1
-rw-r--r--tests/template_tests/recursive_templates/fs/one.html3
-rw-r--r--tests/template_tests/recursive_templates/fs/other-recursive.html1
-rw-r--r--tests/template_tests/recursive_templates/fs/recursive.html3
-rw-r--r--tests/template_tests/recursive_templates/fs/self.html1
-rw-r--r--tests/template_tests/recursive_templates/fs/three.html1
-rw-r--r--tests/template_tests/recursive_templates/fs/two.html3
-rw-r--r--tests/template_tests/recursive_templates/fs2/recursive.html3
-rw-r--r--tests/template_tests/recursive_templates/fs3/recursive.html1
-rw-r--r--tests/template_tests/test_engine.py2
-rw-r--r--tests/template_tests/test_extends.py178
-rw-r--r--tests/template_tests/test_loaders.py178
-rw-r--r--tests/template_tests/tests.py5
25 files changed, 739 insertions, 128 deletions
diff --git a/django/template/__init__.py b/django/template/__init__.py
index b2566decad..780dbc29fc 100644
--- a/django/template/__init__.py
+++ b/django/template/__init__.py
@@ -59,8 +59,8 @@ from .base import (TemplateDoesNotExist, TemplateSyntaxError, # NOQA
from .context import ContextPopException # NOQA
# Template parts
-from .base import (Context, Node, NodeList, RequestContext, # NOQA
- StringOrigin, Template, Variable)
+from .base import (Context, Node, NodeList, Origin, RequestContext, # NOQA
+ Template, Variable)
# Deprecated in Django 1.8, will be removed in Django 2.0.
from .base import resolve_variable # NOQA
diff --git a/django/template/base.py b/django/template/base.py
index 19b6b76d45..e1849999f2 100644
--- a/django/template/base.py
+++ b/django/template/base.py
@@ -134,7 +134,15 @@ class TemplateSyntaxError(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):
@@ -157,23 +165,29 @@ class InvalidTemplateLibrary(Exception):
class Origin(object):
- def __init__(self, name):
+ def __init__(self, name, template_name=None, loader=None):
self.name = name
-
- def reload(self):
- raise NotImplementedError('subclasses of Origin must provide a reload() method')
+ self.template_name = template_name
+ self.loader = loader
def __str__(self):
return self.name
+ def __eq__(self, other):
+ if not isinstance(other, Origin):
+ return False
-class StringOrigin(Origin):
- def __init__(self, source):
- super(StringOrigin, self).__init__(UNKNOWN_SOURCE)
- self.source = source
+ return (
+ self.name == other.name and
+ self.loader == other.loader
+ )
- def reload(self):
- return self.source
+ @property
+ def loader_name(self):
+ if self.loader:
+ return '%s.%s' % (
+ self.loader.__module__, self.loader.__class__.__name__,
+ )
class Template(object):
@@ -191,7 +205,7 @@ class Template(object):
from .engine import Engine
engine = Engine.get_default()
if origin is None:
- origin = StringOrigin(template_string)
+ origin = Origin(UNKNOWN_SOURCE)
self.name = name
self.origin = origin
self.engine = engine
diff --git a/django/template/engine.py b/django/template/engine.py
index ed398b217b..90015dfacc 100644
--- a/django/template/engine.py
+++ b/django/template/engine.py
@@ -124,15 +124,25 @@ class Engine(object):
raise ImproperlyConfigured(
"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:
- try:
- source, display_name = loader(name, dirs)
- origin = self.make_origin(display_name, loader, name, dirs)
- return source, origin
- except TemplateDoesNotExist:
- pass
- raise TemplateDoesNotExist(name)
+ if loader.supports_recursion:
+ try:
+ template = loader.get_template(
+ name, template_dirs=dirs, skip=skip,
+ )
+ return template, template.origin
+ 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):
"""
@@ -234,11 +244,3 @@ class Engine(object):
continue
# If we get here, none of the templates could be loaded
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
diff --git a/django/template/loader.py b/django/template/loader.py
index 182d9aacf6..2f9afd3071 100644
--- a/django/template/loader.py
+++ b/django/template/loader.py
@@ -4,25 +4,20 @@ from django.utils.deprecation import RemovedInDjango20Warning
from . import engines
from .backends.django import DjangoTemplates
-from .base import Origin, TemplateDoesNotExist
+from .base import TemplateDoesNotExist
from .engine import (
_context_instance_undefined, _dictionary_undefined, _dirs_undefined,
)
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):
"""
Loads and returns a template for the given name.
Raises TemplateDoesNotExist if no such template exists.
"""
+ tried = []
engines = _engine_list(using)
for engine in engines:
try:
@@ -37,10 +32,10 @@ def get_template(template_name, dirs=_dirs_undefined, using=None):
stacklevel=2)
else:
return engine.get_template(template_name)
- except TemplateDoesNotExist:
- pass
+ except TemplateDoesNotExist as e:
+ 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):
@@ -51,6 +46,7 @@ def select_template(template_name_list, dirs=_dirs_undefined, using=None):
Raises TemplateDoesNotExist if no such template exists.
"""
+ tried = []
engines = _engine_list(using)
for template_name in template_name_list:
for engine in engines:
@@ -66,11 +62,11 @@ def select_template(template_name_list, dirs=_dirs_undefined, using=None):
stacklevel=2)
else:
return engine.get_template(template_name)
- except TemplateDoesNotExist:
- pass
+ except TemplateDoesNotExist as e:
+ tried.extend(e.tried)
if template_name_list:
- raise TemplateDoesNotExist(', '.join(template_name_list))
+ raise TemplateDoesNotExist(', '.join(template_name_list), tried=tried)
else:
raise TemplateDoesNotExist("No template names provided")
@@ -96,6 +92,7 @@ def render_to_string(template_name, context=None,
return template.render(context, request)
else:
+ tried = []
# Some deprecated arguments were passed - use the legacy code path
for engine in _engine_list(using):
try:
@@ -126,13 +123,14 @@ def render_to_string(template_name, context=None,
"Skipping template backend %s because its render_to_string "
"method doesn't support the dictionary argument." %
engine.name, stacklevel=2)
- except TemplateDoesNotExist:
+ except TemplateDoesNotExist as e:
+ tried.extend(e.tried)
continue
if template_name:
if isinstance(template_name, (list, tuple)):
template_name = ', '.join(template_name)
- raise TemplateDoesNotExist(template_name)
+ raise TemplateDoesNotExist(template_name, tried=tried)
else:
raise TemplateDoesNotExist("No template names provided")
diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py
index 398914b6b7..3b846a4099 100644
--- a/django/template/loader_tags.py
+++ b/django/template/loader_tags.py
@@ -82,6 +82,7 @@ class BlockNode(Node):
class ExtendsNode(Node):
must_be_first = True
+ context_key = 'extends_context'
def __init__(self, nodelist, parent_name, template_dirs=None):
self.nodelist = nodelist
@@ -92,6 +93,39 @@ class ExtendsNode(Node):
def __repr__(self):
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):
parent = self.parent_name.resolve(context)
if not parent:
@@ -107,7 +141,7 @@ class ExtendsNode(Node):
if isinstance(getattr(parent, 'template', None), Template):
# parent is a django.template.backends.django.Template
return parent.template
- return context.template.engine.get_template(parent)
+ return self.find_template(parent, context)
def render(self, context):
compiled_parent = self.get_parent(context)
diff --git a/django/template/loaders/base.py b/django/template/loaders/base.py
index 66c2986343..b848bc05ce 100644
--- a/django/template/loaders/base.py
+++ b/django/template/loaders/base.py
@@ -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):
@@ -9,15 +13,54 @@ class Loader(object):
self.engine = engine
def __call__(self, template_name, template_dirs=None):
+ # RemovedInDjango21Warning: Allow loaders to be called like functions.
return self.load_template(template_name, template_dirs)
+ def get_template(self, template_name, template_dirs=None, skip=None):
+ """
+ Calls self.get_template_sources() and returns a Template object for
+ the first template matching template_name. If skip is provided,
+ template origins in skip are ignored. This is used to avoid recursion
+ 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 = self.engine.make_origin(
- display_name, self.load_template_source,
- template_name, template_dirs)
-
+ template_name, template_dirs,
+ )
+ origin = Origin(
+ name=display_name,
+ template_name=template_name,
+ loader=self,
+ )
try:
template = Template(source, origin, template_name, self.engine)
except TemplateDoesNotExist:
@@ -29,14 +72,23 @@ class Loader(object):
else:
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.
"""
raise NotImplementedError(
- "subclasses of Loader must provide "
- "a load_template_source() method")
+ 'subclasses of Loader must provide a get_template_sources() 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):
"""
@@ -44,3 +96,11 @@ class Loader(object):
templates or cached loader modules).
"""
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')
diff --git a/django/template/loaders/cached.py b/django/template/loaders/cached.py
index 188268e92e..b543a83026 100644
--- a/django/template/loaders/cached.py
+++ b/django/template/loaders/cached.py
@@ -4,8 +4,11 @@ to load templates from them in order, caching the result.
"""
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 .base import Loader as BaseLoader
@@ -15,20 +18,84 @@ class Loader(BaseLoader):
def __init__(self, engine, loaders):
self.template_cache = {}
- self.find_template_cache = {}
+ self.find_template_cache = {} # RemovedInDjango21Warning
+ self.get_template_cache = {}
self.loaders = engine.get_template_loaders(loaders)
super(Loader, self).__init__(engine)
- def cache_key(self, template_name, template_dirs):
- if template_dirs:
- # If template directories were specified, use a hash to differentiate
- return '-'.join([template_name, hashlib.sha1(force_bytes('|'.join(template_dirs))).hexdigest()])
+ def get_contents(self, origin):
+ return origin.loader.get_contents(origin)
+
+ 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:
- 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):
"""
- 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)
try:
@@ -41,7 +108,11 @@ class Loader(BaseLoader):
except TemplateDoesNotExist:
pass
else:
- origin = self.engine.make_origin(display_name, loader, name, dirs)
+ origin = Origin(
+ name=display_name,
+ template_name=name,
+ loader=loader,
+ )
result = template, origin
break
self.find_template_cache[key] = result
@@ -52,6 +123,10 @@ class Loader(BaseLoader):
raise TemplateDoesNotExist(name)
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)
template_tuple = self.template_cache.get(key)
# A cached previous failure:
@@ -74,4 +149,5 @@ class Loader(BaseLoader):
def reset(self):
"Empty the template cache."
self.template_cache.clear()
- self.find_template_cache.clear()
+ self.find_template_cache.clear() # RemovedInDjango21Warning
+ self.get_template_cache.clear()
diff --git a/django/template/loaders/eggs.py b/django/template/loaders/eggs.py
index 14a586877d..9dc291e485 100644
--- a/django/template/loaders/eggs.py
+++ b/django/template/loaders/eggs.py
@@ -1,9 +1,12 @@
# Wrapper for loading templates from eggs via pkg_resources.resource_string.
from __future__ import unicode_literals
+import warnings
+
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.deprecation import RemovedInDjango21Warning
from .base import Loader as BaseLoader
@@ -13,6 +16,14 @@ except ImportError:
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):
def __init__(self, engine):
@@ -20,19 +31,42 @@ class Loader(BaseLoader):
raise RuntimeError("Setuptools must be installed to use the egg loader")
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):
"""
Loads templates from Python eggs via pkg_resource.resource_string.
For every installed app, it tries to get the resource (app, template_name).
"""
- pkg_name = 'templates/' + template_name
- for app_config in apps.get_app_configs():
+ 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):
try:
- resource = resource_string(app_config.name, pkg_name)
- except Exception:
- continue
- if six.PY2:
- resource = resource.decode(self.engine.file_charset)
- return (resource, 'egg:%s:%s' % (app_config.name, pkg_name))
+ return self.get_contents(origin), origin.name
+ except TemplateDoesNotExist:
+ pass
raise TemplateDoesNotExist(template_name)
diff --git a/django/template/loaders/filesystem.py b/django/template/loaders/filesystem.py
index 74801efe56..e1ed0ba5e0 100644
--- a/django/template/loaders/filesystem.py
+++ b/django/template/loaders/filesystem.py
@@ -4,10 +4,12 @@ Wrapper for loading templates from the filesystem.
import errno
import io
+import warnings
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.deprecation import RemovedInDjango21Warning
from .base import Loader as BaseLoader
@@ -17,28 +19,46 @@ class Loader(BaseLoader):
def get_dirs(self):
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):
"""
- 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.
+ Return an Origin object pointing to an absolute path in each directory
+ in template_dirs. For security reasons, if a path doesn't lie inside
+ one of the template_dirs it is excluded from the result set.
"""
if not template_dirs:
template_dirs = self.get_dirs()
for template_dir in template_dirs:
try:
- yield safe_join(template_dir, template_name)
+ name = safe_join(template_dir, template_name)
except SuspiciousFileOperation:
# The joined path was located outside of this template_dir
# (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):
- 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:
- with io.open(filepath, encoding=self.engine.file_charset) as fp:
- return fp.read(), filepath
- except IOError as e:
- if e.errno != errno.ENOENT:
- raise
+ return self.get_contents(origin), origin.name
+ except TemplateDoesNotExist:
+ pass
raise TemplateDoesNotExist(template_name)
diff --git a/django/template/loaders/locmem.py b/django/template/loaders/locmem.py
index 598907bb71..f0ef792b35 100644
--- a/django/template/loaders/locmem.py
+++ b/django/template/loaders/locmem.py
@@ -2,7 +2,10 @@
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
@@ -13,7 +16,25 @@ class Loader(BaseLoader):
self.templates_dict = templates_dict
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):
+ warnings.warn(
+ 'The load_template_sources() method is deprecated. Use '
+ 'get_template() or get_contents() instead.',
+ RemovedInDjango21Warning,
+ )
try:
return self.templates_dict[template_name], template_name
except KeyError:
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index 98432a7e38..ce9b330b69 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -37,6 +37,26 @@ details on these changes.
* 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:
2.0
diff --git a/tests/template_loader/tests.py b/tests/template_loader/tests.py
index a4a4542f32..c698c0a398 100644
--- a/tests/template_loader/tests.py
+++ b/tests/template_loader/tests.py
@@ -33,8 +33,12 @@ class TemplateLoaderTests(SimpleTestCase):
self.assertEqual(template.render(), "Hello! (Django templates)\n")
def test_get_template_not_found(self):
- with self.assertRaises(TemplateDoesNotExist):
+ with self.assertRaises(TemplateDoesNotExist) as e:
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):
template = select_template(["template_loader/unknown.html",
@@ -56,9 +60,17 @@ class TemplateLoaderTests(SimpleTestCase):
select_template([])
def test_select_template_not_found(self):
- with self.assertRaises(TemplateDoesNotExist):
+ with self.assertRaises(TemplateDoesNotExist) as e:
select_template(["template_loader/unknown.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):
template = select_template(["template_loader/goodbye.html",
@@ -83,8 +95,12 @@ class TemplateLoaderTests(SimpleTestCase):
self.assertEqual(content, "Hello! (Django templates)\n")
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")
+ self.assertEqual(
+ e.exception.tried[-1][0].template_name,
+ 'template_loader/unknown.html',
+ )
def test_render_to_string_with_list_first_engine(self):
content = render_to_string(["template_loader/unknown.html",
@@ -106,9 +122,17 @@ class TemplateLoaderTests(SimpleTestCase):
render_to_string([])
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",
"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):
content = render_to_string(["template_loader/goodbye.html",
diff --git a/tests/template_tests/recursive_templates/fs/extend-missing.html b/tests/template_tests/recursive_templates/fs/extend-missing.html
new file mode 100644
index 0000000000..e3c106e2c1
--- /dev/null
+++ b/tests/template_tests/recursive_templates/fs/extend-missing.html
@@ -0,0 +1 @@
+{% extends "missing.html" %}
diff --git a/tests/template_tests/recursive_templates/fs/one.html b/tests/template_tests/recursive_templates/fs/one.html
new file mode 100644
index 0000000000..f72e72e1a6
--- /dev/null
+++ b/tests/template_tests/recursive_templates/fs/one.html
@@ -0,0 +1,3 @@
+{% extends "two.html" %}
+
+{% block content %}{{ block.super }} one{% endblock %}
diff --git a/tests/template_tests/recursive_templates/fs/other-recursive.html b/tests/template_tests/recursive_templates/fs/other-recursive.html
new file mode 100644
index 0000000000..84e5ac9eae
--- /dev/null
+++ b/tests/template_tests/recursive_templates/fs/other-recursive.html
@@ -0,0 +1 @@
+{% extends "recursive.html" %}
diff --git a/tests/template_tests/recursive_templates/fs/recursive.html b/tests/template_tests/recursive_templates/fs/recursive.html
new file mode 100644
index 0000000000..cbf242d8d5
--- /dev/null
+++ b/tests/template_tests/recursive_templates/fs/recursive.html
@@ -0,0 +1,3 @@
+{% extends "recursive.html" %}
+
+{% block content %}{{ block.super }} fs/recursive{% endblock %}
diff --git a/tests/template_tests/recursive_templates/fs/self.html b/tests/template_tests/recursive_templates/fs/self.html
new file mode 100644
index 0000000000..f3e5bbf301
--- /dev/null
+++ b/tests/template_tests/recursive_templates/fs/self.html
@@ -0,0 +1 @@
+{% extends "self.html" %}
diff --git a/tests/template_tests/recursive_templates/fs/three.html b/tests/template_tests/recursive_templates/fs/three.html
new file mode 100644
index 0000000000..360aeeea5e
--- /dev/null
+++ b/tests/template_tests/recursive_templates/fs/three.html
@@ -0,0 +1 @@
+{% block content %}three{% endblock %}
diff --git a/tests/template_tests/recursive_templates/fs/two.html b/tests/template_tests/recursive_templates/fs/two.html
new file mode 100644
index 0000000000..b9b80ec7a0
--- /dev/null
+++ b/tests/template_tests/recursive_templates/fs/two.html
@@ -0,0 +1,3 @@
+{% extends "three.html" %}
+
+{% block content %}{{ block.super }} two{% endblock %}
diff --git a/tests/template_tests/recursive_templates/fs2/recursive.html b/tests/template_tests/recursive_templates/fs2/recursive.html
new file mode 100644
index 0000000000..52a338ca9f
--- /dev/null
+++ b/tests/template_tests/recursive_templates/fs2/recursive.html
@@ -0,0 +1,3 @@
+{% extends "recursive.html" %}
+
+{% block content %}{{ block.super }} fs2/recursive{% endblock %}
diff --git a/tests/template_tests/recursive_templates/fs3/recursive.html b/tests/template_tests/recursive_templates/fs3/recursive.html
new file mode 100644
index 0000000000..aefbad4582
--- /dev/null
+++ b/tests/template_tests/recursive_templates/fs3/recursive.html
@@ -0,0 +1 @@
+{% block content %}fs3/recursive{% endblock %}
diff --git a/tests/template_tests/test_engine.py b/tests/template_tests/test_engine.py
index 6b43fd9dd2..d9929660cc 100644
--- a/tests/template_tests/test_engine.py
+++ b/tests/template_tests/test_engine.py
@@ -55,7 +55,7 @@ class LoaderTests(SimpleTestCase):
def test_origin(self):
engine = Engine(dirs=[TEMPLATE_DIR], debug=True)
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):
"""
diff --git a/tests/template_tests/test_extends.py b/tests/template_tests/test_extends.py
new file mode 100644
index 0000000000..48290547e7
--- /dev/null
+++ b/tests/template_tests/test_extends.py
@@ -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')
diff --git a/tests/template_tests/test_loaders.py b/tests/template_tests/test_loaders.py
index caa0b85db3..41cac5cb9d 100644
--- a/tests/template_tests/test_loaders.py
+++ b/tests/template_tests/test_loaders.py
@@ -10,8 +10,9 @@ from contextlib import contextmanager
from django.template import Context, TemplateDoesNotExist
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.deprecation import RemovedInDjango21Warning
from .utils import TEMPLATE_DIR
@@ -23,8 +24,9 @@ except ImportError:
class CachedLoaderTests(SimpleTestCase):
- def create_engine(self, **kwargs):
- return Engine(
+ def setUp(self):
+ self.engine = Engine(
+ dirs=[TEMPLATE_DIR],
loaders=[
('django.template.loaders.cached.Loader', [
'django.template.loaders.filesystem.Loader',
@@ -32,26 +34,47 @@ class CachedLoaderTests(SimpleTestCase):
],
)
- def test_templatedir_caching(self):
- """
- #13573 -- Template directories should be part of the cache key.
- """
- engine = self.create_engine()
+ 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].loaders[0])
- # Retrieve a template specifying a template directory to check
- t1, name = engine.find_template('test.html', (os.path.join(TEMPLATE_DIR, 'first'),))
- # Now retrieve the same template name, but from a different directory
- t2, name = engine.find_template('test.html', (os.path.join(TEMPLATE_DIR, 'second'),))
+ cache = self.engine.template_loaders[0].get_template_cache
+ self.assertEqual(cache['index.html'], template)
- # The two templates should not have the same content
- self.assertNotEqual(t1.render(Context({})), t2.render(Context({})))
+ # Run a second time from cache
+ 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_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')
- def test_missing_template_is_cached(self):
+ @ignore_warnings(category=RemovedInDjango21Warning)
+ def test_load_template_missing(self):
"""
#19949 -- TemplateDoesNotExist exceptions should be cached.
"""
- engine = self.create_engine()
- loader = engine.template_loaders[0]
+ loader = self.engine.template_loaders[0]
self.assertFalse('missing.html' in loader.template_cache)
@@ -64,6 +87,18 @@ class CachedLoaderTests(SimpleTestCase):
"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')
class EggLoaderTests(SimpleTestCase):
@@ -117,22 +152,43 @@ class EggLoaderTests(SimpleTestCase):
del sys.modules[name]
del pkg_resources._provider_factories[MockLoader]
- def setUp(self):
- engine = Engine(loaders=[
+ @classmethod
+ def setUpClass(cls):
+ cls.engine = Engine(loaders=[
'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 = {
os.path.normcase('templates/y.html'): six.StringIO("y"),
}
with self.create_egg('egg', templates):
with override_settings(INSTALLED_APPS=['egg']):
- contents, template_name = self.loader.load_template_source("y.html")
- self.assertEqual(contents, "y")
- self.assertEqual(template_name, "egg:egg:templates/y.html")
+ template = self.engine.get_template("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):
"""
@@ -141,7 +197,7 @@ class EggLoaderTests(SimpleTestCase):
with self.create_egg('egg', {}):
with override_settings(INSTALLED_APPS=['egg']):
with self.assertRaises(TemplateDoesNotExist):
- self.loader.load_template_source("not-existing.html")
+ self.engine.get_template('not-existing.html')
def test_not_installed(self):
"""
@@ -153,13 +209,15 @@ class EggLoaderTests(SimpleTestCase):
with self.create_egg('egg', templates):
with self.assertRaises(TemplateDoesNotExist):
- self.loader.load_template_source("y.html")
+ self.engine.get_template('y.html')
class FileSystemLoaderTests(SimpleTestCase):
- def setUp(self):
- self.engine = Engine(dirs=[TEMPLATE_DIR])
+ @classmethod
+ def setUpClass(cls):
+ cls.engine = Engine(dirs=[TEMPLATE_DIR])
+ super(FileSystemLoaderTests, cls).setUpClass()
@contextmanager
def set_dirs(self, dirs):
@@ -177,13 +235,27 @@ class FileSystemLoaderTests(SimpleTestCase):
def check_sources(path, expected_sources):
expected_sources = [os.path.abspath(s) for s in expected_sources]
self.assertEqual(
- list(loader.get_template_sources(path)),
+ [origin.name for origin in loader.get_template_sources(path)],
expected_sources,
)
with self.set_dirs(dirs):
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):
with self.source_checker(['/dir1', '/dir2']) as check_sources:
check_sources('index.html', ['/dir1/index.html', '/dir2/index.html'])
@@ -248,18 +320,56 @@ class FileSystemLoaderTests(SimpleTestCase):
self.engine.get_template('first')
-class AppDirectoriesLoaderTest(SimpleTestCase):
+class AppDirectoriesLoaderTests(SimpleTestCase):
- def setUp(self):
- self.engine = Engine(
+ @classmethod
+ def setUpClass(cls):
+ cls.engine = Engine(
loaders=['django.template.loaders.app_directories.Loader'],
)
+ super(AppDirectoriesLoaderTests, cls).setUpClass()
@override_settings(INSTALLED_APPS=['template_tests'])
- def test_load_template(self):
- self.engine.get_template('index.html')
+ 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])
+
+ @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=[])
def test_not_installed(self):
with self.assertRaises(TemplateDoesNotExist):
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')
diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py
index dec21559ae..0a71489f3e 100644
--- a/tests/template_tests/tests.py
+++ b/tests/template_tests/tests.py
@@ -6,6 +6,7 @@ import sys
from django.contrib.auth.models import Group
from django.core import urlresolvers
from django.template import Context, Engine, TemplateSyntaxError
+from django.template.base import UNKNOWN_SOURCE
from django.test import SimpleTestCase, override_settings
@@ -13,7 +14,9 @@ class TemplateTests(SimpleTestCase):
def test_string_origin(self):
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)
def test_url_reverse_no_settings_module(self):