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
# 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

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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)

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):
@ -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 load_template(self, template_name, template_dirs=None):
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)
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 = 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')

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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",

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):
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):
"""

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.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_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.
"""
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')

View File

@ -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):