diff --git a/django/template/loader.py b/django/template/loader.py index c62c3833228..093d7d172b5 100644 --- a/django/template/loader.py +++ b/django/template/loader.py @@ -1,13 +1,9 @@ import warnings -from django.core.exceptions import ImproperlyConfigured -from django.template.base import Origin, Template, Context, TemplateDoesNotExist 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.module_loading import import_string -from django.utils import six - -template_source_loaders = None class LoaderOrigin(Origin): @@ -26,58 +22,8 @@ def make_origin(display_name, loader, name, dirs): 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): - # Calculate template_source_loaders the first time the function is executed - # because putting this logic in the module-level namespace may cause - # circular import errors. See Django ticket #1292. - global template_source_loaders - if template_source_loaders is None: - loaders = [] - for 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: + for loader in get_template_loaders(): try: source, display_name = loader(name, dirs) return (source, make_origin(display_name, loader, name, dirs)) diff --git a/django/template/loaders/cached.py b/django/template/loaders/cached.py index e217efc93ff..180ed9900db 100644 --- a/django/template/loaders/cached.py +++ b/django/template/loaders/cached.py @@ -5,7 +5,8 @@ to load templates from them in order, caching the result. import hashlib 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 .base import Loader as BaseLoader diff --git a/django/template/loaders/utils.py b/django/template/loaders/utils.py new file mode 100644 index 00000000000..4850c9f1aa5 --- /dev/null +++ b/django/template/loaders/utils.py @@ -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) diff --git a/django/test/signals.py b/django/test/signals.py index e080d976ee9..f065e155b8a 100644 --- a/django/test/signals.py +++ b/django/test/signals.py @@ -87,8 +87,8 @@ def clear_context_processors_cache(**kwargs): @receiver(setting_changed) def clear_template_loaders_cache(**kwargs): if kwargs['setting'] == 'TEMPLATE_LOADERS': - from django.template import loader - loader.template_source_loaders = None + from django.template.loaders.utils import get_template_loaders + get_template_loaders.cache_clear() @receiver(setting_changed) diff --git a/django/views/debug.py b/django/views/debug.py index 95264afbd5d..59136ce90c3 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -12,6 +12,7 @@ from django.http import (HttpResponse, HttpResponseNotFound, HttpRequest, build_request_repr) from django.template import Template, Context, TemplateDoesNotExist 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.html import escape from django.utils.encoding import force_bytes, smart_text @@ -279,14 +280,15 @@ class ExceptionReporter(object): """Return a dictionary containing traceback information.""" 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.loader_debug_info = [] - # If the template_source_loaders haven't been populated yet, you need - # to provide an empty list for this for loop to not fail. - if template_source_loaders is None: - template_source_loaders = [] - for loader in template_source_loaders: + # If Django fails in get_template_loaders, provide an empty list + # for the following loop to not fail. + try: + template_loaders = get_template_loaders() + except Exception: + template_loaders = [] + for loader in template_loaders: try: source_list_func = loader.get_template_sources # NOTE: This assumes exc_value is the name of the template that diff --git a/tests/template_tests/test_loaders.py b/tests/template_tests/test_loaders.py index e83b8060b1f..c3c086f3811 100644 --- a/tests/template_tests/test_loaders.py +++ b/tests/template_tests/test_loaders.py @@ -22,6 +22,7 @@ except ImportError: from django.template import TemplateDoesNotExist, Context 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.test import TestCase, override_settings from django.utils import six @@ -127,7 +128,7 @@ class CachedLoader(TestCase): def test_missing_template_is_cached(self): "#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. template_loader.reset() # Check that 'missing.html' isn't already in cache before 'missing.html' is loaded diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py index 334dd31f7ba..b8097fbde10 100644 --- a/tests/template_tests/tests.py +++ b/tests/template_tests/tests.py @@ -14,7 +14,8 @@ from django.contrib.auth.models import Group from django.core import urlresolvers from django.template import (base as template_base, loader, Context, 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.utils import override_settings, extend_sys_path from django.utils.deprecation import RemovedInDjango19Warning, RemovedInDjango20Warning @@ -218,21 +219,27 @@ class TemplateLoaderTests(TestCase): @override_settings(TEMPLATE_DEBUG=True) def test_loader_debug_origin(self): # We rely on the fact that runtests.py sets up TEMPLATE_DIRS to - # point to a directory containing a login.html file. Also that - # 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. + # point to a directory containing a login.html file. 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_name = template.nodelist[0].source[0].name self.assertTrue(template_name.endswith(load_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 - cache_loader = cached.Loader(('',)) - cache_loader._cached_loaders = loader.template_source_loaders - loader.template_source_loaders = (cache_loader,) + @override_settings(TEMPLATE_LOADERS=[ + ('django.template.loaders.cached.Loader', + ['django.template.loaders.filesystem.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_name = template.nodelist[0].source[0].name self.assertTrue(template_name.endswith(load_name), @@ -243,15 +250,15 @@ class TemplateLoaderTests(TestCase): self.assertTrue(template_name.endswith(load_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): - with self.settings(TEMPLATE_DEBUG=True): - template = loader.get_template('login.html') - self.assertEqual(template.origin.loadname, 'login.html') + template = loader.get_template('login.html') + self.assertEqual(template.origin.loadname, 'login.html') + @override_settings(TEMPLATE_DEBUG=True) def test_string_origin(self): - with self.settings(TEMPLATE_DEBUG=True): - template = Template('string template') - self.assertEqual(template.origin.source, 'string template') + template = Template('string template') + self.assertEqual(template.origin.source, 'string template') def test_debug_false_origin(self): template = loader.get_template('login.html') @@ -613,7 +620,9 @@ class TemplateTests(TestCase): 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)) - 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: expected_invalid_str = 'INVALID' diff --git a/tests/test_client_regress/tests.py b/tests/test_client_regress/tests.py index 9ceba4992f9..f7dcae15cf2 100644 --- a/tests/test_client_regress/tests.py +++ b/tests/test_client_regress/tests.py @@ -8,8 +8,7 @@ import os import itertools from django.core.urlresolvers import reverse, NoReverseMatch -from django.template import (TemplateSyntaxError, - Context, Template, loader) +from django.template import TemplateSyntaxError, Context, Template import django.template.context from django.test import Client, TestCase, override_settings from django.test.client import encode_file, RequestFactory @@ -902,13 +901,6 @@ class ExceptionTests(TestCase): @override_settings(ROOT_URLCONF='test_client_regress.urls') 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( 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" try: self.client.get("/no_such_view/") - self.fail("Should get error about syntax error in template") except TemplateSyntaxError: pass + else: + self.fail("Should get error about syntax error in template") # We need two different tests to check URLconf substitution - one to check