Refactored getting the list of template loaders.

This provides the opportunity to move utility functions specific to the
Django Template Language outside of django.template.loader.
This commit is contained in:
Aymeric Augustin 2014-11-15 20:58:26 +01:00
parent 1851dcf377
commit 9eeb788cfb
8 changed files with 102 additions and 93 deletions

View File

@ -1,13 +1,9 @@
import warnings import warnings
from django.core.exceptions import ImproperlyConfigured
from django.template.base import Origin, Template, Context, TemplateDoesNotExist
from django.conf import settings from django.conf import settings
from django.template.base import Origin, Template, Context, TemplateDoesNotExist
from django.template.loaders.utils import get_template_loaders
from django.utils.deprecation import RemovedInDjango20Warning from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.module_loading import import_string
from django.utils import six
template_source_loaders = None
class LoaderOrigin(Origin): class LoaderOrigin(Origin):
@ -26,58 +22,8 @@ def make_origin(display_name, loader, name, dirs):
return None return None
def find_template_loader(loader):
if isinstance(loader, (tuple, list)):
loader, args = loader[0], loader[1:]
else:
args = []
if isinstance(loader, six.string_types):
TemplateLoader = import_string(loader)
if hasattr(TemplateLoader, 'load_template_source'):
func = TemplateLoader(*args)
else:
warnings.warn(
"Function-based template loaders are deprecated. Please use "
"class-based template loaders instead. Inherit base.Loader "
"and provide a load_template_source() method.",
RemovedInDjango20Warning, stacklevel=2)
# Try loading module the old way - string is full path to callable
if args:
raise ImproperlyConfigured(
"Error importing template source loader %s - can't pass "
"arguments to function-based loader." % loader
)
func = TemplateLoader
if not func.is_usable:
import warnings
warnings.warn(
"Your TEMPLATE_LOADERS setting includes %r, but your Python "
"installation doesn't support that type of template loading. "
"Consider removing that line from TEMPLATE_LOADERS." % loader
)
return None
else:
return func
else:
raise ImproperlyConfigured('Loader does not define a "load_template" callable template source loader')
def find_template(name, dirs=None): def find_template(name, dirs=None):
# Calculate template_source_loaders the first time the function is executed for loader in get_template_loaders():
# because putting this logic in the module-level namespace may cause
# circular import errors. See Django ticket #1292.
global template_source_loaders
if template_source_loaders is None:
loaders = []
for loader_name in settings.TEMPLATE_LOADERS:
loader = find_template_loader(loader_name)
if loader is not None:
loaders.append(loader)
template_source_loaders = tuple(loaders)
for loader in template_source_loaders:
try: try:
source, display_name = loader(name, dirs) source, display_name = loader(name, dirs)
return (source, make_origin(display_name, loader, name, dirs)) return (source, make_origin(display_name, loader, name, dirs))

View File

@ -5,7 +5,8 @@ to load templates from them in order, caching the result.
import hashlib import hashlib
from django.template.base import TemplateDoesNotExist from django.template.base import TemplateDoesNotExist
from django.template.loader import get_template_from_string, find_template_loader, make_origin from django.template.loader import get_template_from_string, make_origin
from django.template.loaders.utils import find_template_loader
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

View File

@ -0,0 +1,57 @@
import warnings
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils import lru_cache
from django.utils import six
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.module_loading import import_string
@lru_cache.lru_cache()
def get_template_loaders():
loaders = []
for loader_name in settings.TEMPLATE_LOADERS:
loader = find_template_loader(loader_name)
if loader is not None:
loaders.append(loader)
# Immutable return value because it will be cached and shared by callers.
return tuple(loaders)
def find_template_loader(loader):
if isinstance(loader, (tuple, list)):
loader, args = loader[0], loader[1:]
else:
args = []
if isinstance(loader, six.string_types):
TemplateLoader = import_string(loader)
if hasattr(TemplateLoader, 'load_template_source'):
func = TemplateLoader(*args)
else:
warnings.warn(
"Function-based template loaders are deprecated. "
"Please use class-based template loaders instead. "
"Inherit django.template.loaders.base.Loader "
"and provide a load_template_source() method.",
RemovedInDjango20Warning, stacklevel=2)
# Try loading module the old way - string is full path to callable
if args:
raise ImproperlyConfigured(
"Error importing template source loader %s - can't pass "
"arguments to function-based loader." % loader)
func = TemplateLoader
if not func.is_usable:
warnings.warn(
"Your TEMPLATE_LOADERS setting includes %r, but your Python "
"installation doesn't support that type of template loading. "
"Consider removing that line from TEMPLATE_LOADERS." % loader)
return None
else:
return func
else:
raise ImproperlyConfigured(
"Invalid value in TEMPLATE_LOADERS: %r" % loader)

View File

@ -87,8 +87,8 @@ def clear_context_processors_cache(**kwargs):
@receiver(setting_changed) @receiver(setting_changed)
def clear_template_loaders_cache(**kwargs): def clear_template_loaders_cache(**kwargs):
if kwargs['setting'] == 'TEMPLATE_LOADERS': if kwargs['setting'] == 'TEMPLATE_LOADERS':
from django.template import loader from django.template.loaders.utils import get_template_loaders
loader.template_source_loaders = None get_template_loaders.cache_clear()
@receiver(setting_changed) @receiver(setting_changed)

View File

@ -12,6 +12,7 @@ from django.http import (HttpResponse, HttpResponseNotFound, HttpRequest,
build_request_repr) build_request_repr)
from django.template import Template, Context, TemplateDoesNotExist from django.template import Template, Context, TemplateDoesNotExist
from django.template.defaultfilters import force_escape, pprint from django.template.defaultfilters import force_escape, pprint
from django.template.loaders.utils import get_template_loaders
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from django.utils.html import escape from django.utils.html import escape
from django.utils.encoding import force_bytes, smart_text from django.utils.encoding import force_bytes, smart_text
@ -279,14 +280,15 @@ class ExceptionReporter(object):
"""Return a dictionary containing traceback information.""" """Return a dictionary containing traceback information."""
if self.exc_type and issubclass(self.exc_type, TemplateDoesNotExist): if self.exc_type and issubclass(self.exc_type, TemplateDoesNotExist):
from django.template.loader import template_source_loaders
self.template_does_not_exist = True self.template_does_not_exist = True
self.loader_debug_info = [] self.loader_debug_info = []
# If the template_source_loaders haven't been populated yet, you need # If Django fails in get_template_loaders, provide an empty list
# to provide an empty list for this for loop to not fail. # for the following loop to not fail.
if template_source_loaders is None: try:
template_source_loaders = [] template_loaders = get_template_loaders()
for loader in template_source_loaders: except Exception:
template_loaders = []
for loader in template_loaders:
try: try:
source_list_func = loader.get_template_sources source_list_func = loader.get_template_sources
# NOTE: This assumes exc_value is the name of the template that # NOTE: This assumes exc_value is the name of the template that

View File

@ -22,6 +22,7 @@ except ImportError:
from django.template import TemplateDoesNotExist, Context from django.template import TemplateDoesNotExist, Context
from django.template.loaders.eggs import Loader as EggLoader from django.template.loaders.eggs import Loader as EggLoader
from django.template.loaders.utils import find_template_loader
from django.template import loader from django.template import loader
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.utils import six from django.utils import six
@ -127,7 +128,7 @@ class CachedLoader(TestCase):
def test_missing_template_is_cached(self): def test_missing_template_is_cached(self):
"#19949 -- Check that the missing template is cached." "#19949 -- Check that the missing template is cached."
template_loader = loader.find_template_loader(settings.TEMPLATE_LOADERS[0]) template_loader = find_template_loader(settings.TEMPLATE_LOADERS[0])
# Empty cache, which may be filled from previous tests. # Empty cache, which may be filled from previous tests.
template_loader.reset() template_loader.reset()
# Check that 'missing.html' isn't already in cache before 'missing.html' is loaded # Check that 'missing.html' isn't already in cache before 'missing.html' is loaded

View File

@ -14,7 +14,8 @@ from django.contrib.auth.models import Group
from django.core import urlresolvers from django.core import urlresolvers
from django.template import (base as template_base, loader, Context, from django.template import (base as template_base, loader, Context,
RequestContext, Template, TemplateSyntaxError) RequestContext, Template, TemplateSyntaxError)
from django.template.loaders import app_directories, filesystem, cached from django.template.loaders import app_directories, filesystem
from django.template.loaders.utils import get_template_loaders
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
from django.test.utils import override_settings, extend_sys_path from django.test.utils import override_settings, extend_sys_path
from django.utils.deprecation import RemovedInDjango19Warning, RemovedInDjango20Warning from django.utils.deprecation import RemovedInDjango19Warning, RemovedInDjango20Warning
@ -218,21 +219,27 @@ class TemplateLoaderTests(TestCase):
@override_settings(TEMPLATE_DEBUG=True) @override_settings(TEMPLATE_DEBUG=True)
def test_loader_debug_origin(self): def test_loader_debug_origin(self):
# We rely on the fact that runtests.py sets up TEMPLATE_DIRS to # We rely on the fact that runtests.py sets up TEMPLATE_DIRS to
# point to a directory containing a login.html file. Also that # point to a directory containing a login.html file.
# the file system and app directories loaders both inherit the
# load_template method from the base Loader class, so we only need
# to test one of them.
load_name = 'login.html' load_name = 'login.html'
# We also rely on the fact the file system and app directories loaders
# both inherit the load_template method from the base Loader class, so
# we only need to test one of them.
template = loader.get_template(load_name) template = loader.get_template(load_name)
template_name = template.nodelist[0].source[0].name template_name = template.nodelist[0].source[0].name
self.assertTrue(template_name.endswith(load_name), self.assertTrue(template_name.endswith(load_name),
'Template loaded by filesystem loader has incorrect name for debug page: %s' % template_name) 'Template loaded by filesystem loader has incorrect name for debug page: %s' % template_name)
# Also test the cached loader, since it overrides load_template @override_settings(TEMPLATE_LOADERS=[
cache_loader = cached.Loader(('',)) ('django.template.loaders.cached.Loader',
cache_loader._cached_loaders = loader.template_source_loaders ['django.template.loaders.filesystem.Loader']),
loader.template_source_loaders = (cache_loader,) ])
@override_settings(TEMPLATE_DEBUG=True)
def test_cached_loader_debug_origin(self):
# Same comment as in test_loader_debug_origin.
load_name = 'login.html'
# Test the cached loader separately since it overrides load_template.
template = loader.get_template(load_name) template = loader.get_template(load_name)
template_name = template.nodelist[0].source[0].name template_name = template.nodelist[0].source[0].name
self.assertTrue(template_name.endswith(load_name), self.assertTrue(template_name.endswith(load_name),
@ -243,15 +250,15 @@ class TemplateLoaderTests(TestCase):
self.assertTrue(template_name.endswith(load_name), self.assertTrue(template_name.endswith(load_name),
'Cached template loaded through cached loader has incorrect name for debug page: %s' % template_name) 'Cached template loaded through cached loader has incorrect name for debug page: %s' % template_name)
@override_settings(TEMPLATE_DEBUG=True)
def test_loader_origin(self): def test_loader_origin(self):
with self.settings(TEMPLATE_DEBUG=True): template = loader.get_template('login.html')
template = loader.get_template('login.html') self.assertEqual(template.origin.loadname, 'login.html')
self.assertEqual(template.origin.loadname, 'login.html')
@override_settings(TEMPLATE_DEBUG=True)
def test_string_origin(self): def test_string_origin(self):
with self.settings(TEMPLATE_DEBUG=True): template = Template('string template')
template = Template('string template') self.assertEqual(template.origin.source, 'string template')
self.assertEqual(template.origin.source, 'string template')
def test_debug_false_origin(self): def test_debug_false_origin(self):
template = loader.get_template('login.html') template = loader.get_template('login.html')
@ -613,7 +620,9 @@ class TemplateTests(TestCase):
if output != result: if output != result:
failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s', TEMPLATE_DEBUG=%s): %s -- FAILED. Expected %r, got %r" % (is_cached, invalid_str, template_debug, name, result, output)) failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s', TEMPLATE_DEBUG=%s): %s -- FAILED. Expected %r, got %r" % (is_cached, invalid_str, template_debug, name, result, output))
loader.template_source_loaders[0].reset() # This relies on get_template_loaders() memoizing its
# result. All callers get the same iterable of loaders.
get_template_loaders()[0].reset()
if template_base.invalid_var_format_string: if template_base.invalid_var_format_string:
expected_invalid_str = 'INVALID' expected_invalid_str = 'INVALID'

View File

@ -8,8 +8,7 @@ import os
import itertools import itertools
from django.core.urlresolvers import reverse, NoReverseMatch from django.core.urlresolvers import reverse, NoReverseMatch
from django.template import (TemplateSyntaxError, from django.template import TemplateSyntaxError, Context, Template
Context, Template, loader)
import django.template.context import django.template.context
from django.test import Client, TestCase, override_settings from django.test import Client, TestCase, override_settings
from django.test.client import encode_file, RequestFactory from django.test.client import encode_file, RequestFactory
@ -902,13 +901,6 @@ class ExceptionTests(TestCase):
@override_settings(ROOT_URLCONF='test_client_regress.urls') @override_settings(ROOT_URLCONF='test_client_regress.urls')
class TemplateExceptionTests(TestCase): class TemplateExceptionTests(TestCase):
def setUp(self):
# Reset the loaders so they don't try to render cached templates.
if loader.template_source_loaders is not None:
for template_loader in loader.template_source_loaders:
if hasattr(template_loader, 'reset'):
template_loader.reset()
@override_settings( @override_settings(
TEMPLATE_DIRS=(os.path.join(os.path.dirname(upath(__file__)), 'bad_templates'),) TEMPLATE_DIRS=(os.path.join(os.path.dirname(upath(__file__)), 'bad_templates'),)
) )
@ -916,9 +908,10 @@ class TemplateExceptionTests(TestCase):
"Errors found when rendering 404 error templates are re-raised" "Errors found when rendering 404 error templates are re-raised"
try: try:
self.client.get("/no_such_view/") self.client.get("/no_such_view/")
self.fail("Should get error about syntax error in template")
except TemplateSyntaxError: except TemplateSyntaxError:
pass pass
else:
self.fail("Should get error about syntax error in template")
# We need two different tests to check URLconf substitution - one to check # We need two different tests to check URLconf substitution - one to check