From 4dc4d12e27e4e0337568651136eee5b6f2171204 Mon Sep 17 00:00:00 2001 From: Unai Zalakain Date: Thu, 12 Dec 2013 10:59:05 +0100 Subject: [PATCH] Fixed #21598 -- cleaned up template loader overrides in tests - Template loader overriding is managed with contexts. - The test loader is a class (function based loaders entered deprecation timeline in 1.4). - Template loader overrider that overrides with test loader added. --- django/test/utils.py | 115 ++++++++--- tests/template_tests/tests.py | 263 +++++++++++------------- tests/view_tests/tests/test_debug.py | 16 +- tests/view_tests/tests/test_defaults.py | 13 +- 4 files changed, 214 insertions(+), 193 deletions(-) diff --git a/django/test/utils.py b/django/test/utils.py index 48524244cc..fdbf920868 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -146,42 +146,107 @@ def get_runner(settings, test_runner_class=None): return test_runner -def setup_test_template_loader(templates_dict, use_cached_loader=False): +class override_template_loaders(object): """ - Changes Django to only find templates from within a dictionary (where each - key is the template name and each value is the corresponding template - content to return). + Acts as a function decorator, context manager or start/end manager and + override the template loaders. It could be used in the following ways: - Use meth:`restore_template_loaders` to restore the original loaders. + @override_template_loaders(SomeLoader()) + def test_function(self): + ... + + with override_template_loaders(SomeLoader(), OtherLoader()) as loaders: + ... + + loaders = override_template_loaders.override(SomeLoader()) + ... + override_template_loaders.restore() """ - if hasattr(loader, RESTORE_LOADERS_ATTR): - raise Exception("loader.%s already exists" % RESTORE_LOADERS_ATTR) + def __init__(self, *loaders): + self.loaders = loaders + self.old_loaders = [] - def test_template_loader(template_name, template_dirs=None): - "A custom template loader that loads templates from a dictionary." + def __enter__(self): + self.old_loaders = loader.template_source_loaders + loader.template_source_loaders = self.loaders + return self.loaders + + def __exit__(self, type, value, traceback): + loader.template_source_loaders = self.old_loaders + + def __call__(self, test_func): + @wraps(test_func) + def inner(*args, **kwargs): + with self: + return test_func(*args, **kwargs) + return inner + + @classmethod + def override(cls, *loaders): + if hasattr(loader, RESTORE_LOADERS_ATTR): + raise Exception("loader.%s already exists" % RESTORE_LOADERS_ATTR) + setattr(loader, RESTORE_LOADERS_ATTR, loader.template_source_loaders) + loader.template_source_loaders = loaders + return loaders + + @classmethod + def restore(cls): + loader.template_source_loaders = getattr(loader, RESTORE_LOADERS_ATTR) + delattr(loader, RESTORE_LOADERS_ATTR) + + +class TestTemplateLoader(loader.BaseLoader): + "A custom template loader that loads templates from a dictionary." + is_usable = True + + def __init__(self, templates_dict): + self.templates_dict = templates_dict + + def load_template_source(self, template_name, template_dirs=None, + skip_template=None): try: - return (templates_dict[template_name], "test:%s" % template_name) + return (self.templates_dict[template_name], + "test:%s" % template_name) except KeyError: raise TemplateDoesNotExist(template_name) - if use_cached_loader: - template_loader = cached.Loader(('test_template_loader',)) - template_loader._cached_loaders = (test_template_loader,) - else: - template_loader = test_template_loader - setattr(loader, RESTORE_LOADERS_ATTR, loader.template_source_loaders) - loader.template_source_loaders = (template_loader,) - return template_loader - - -def restore_template_loaders(): +class override_with_test_loader(override_template_loaders): """ - Restores the original template loaders after - :meth:`setup_test_template_loader` has been run. + Acts as a function decorator, context manager or start/end manager and + override the template loaders with the test loader. It could be used in the + following ways: + + @override_with_test_loader(templates_dict, use_cached_loader=True) + def test_function(self): + ... + + with override_with_test_loader(templates_dict) as test_loader: + ... + + test_loader = override_with_test_loader.override(templates_dict) + ... + override_with_test_loader.restore() """ - loader.template_source_loaders = getattr(loader, RESTORE_LOADERS_ATTR) - delattr(loader, RESTORE_LOADERS_ATTR) + + def __init__(self, templates_dict, use_cached_loader=False): + self.loader = self._get_loader(templates_dict, use_cached_loader) + super(override_with_test_loader, self).__init__(self.loader) + + def __enter__(self): + return super(override_with_test_loader, self).__enter__()[0] + + @classmethod + def override(cls, templates_dict, use_cached_loader=False): + loader = cls._get_loader(templates_dict, use_cached_loader) + return super(override_with_test_loader, cls).override(loader)[0] + + @classmethod + def _get_loader(cls, templates_dict, use_cached_loader=False): + if use_cached_loader: + loader = cached.Loader(('TestTemplateLoader',)) + loader._cached_loaders = TestTemplateLoader(templates_dict) + return TestTemplateLoader(templates_dict) class override_settings(object): diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py index 90279265ad..b81e63545d 100644 --- a/tests/template_tests/tests.py +++ b/tests/template_tests/tests.py @@ -15,8 +15,8 @@ from django.template import (base as template_base, loader, Context, RequestContext, Template, TemplateSyntaxError) from django.template.loaders import app_directories, filesystem, cached from django.test import RequestFactory, TestCase -from django.test.utils import (setup_test_template_loader, - restore_template_loaders, override_settings, extend_sys_path) +from django.test.utils import (override_settings, override_template_loaders, + override_with_test_loader, extend_sys_path) from django.utils.deprecation import RemovedInDjango19Warning, RemovedInDjango20Warning from django.utils.encoding import python_2_unicode_compatible from django.utils.formats import date_format @@ -208,42 +208,36 @@ class TemplateLoaderTests(TestCase): test_template_sources('/DIR1/index.HTML', template_dirs, ['/DIR1/index.HTML']) + @override_template_loaders(filesystem.Loader()) # Turn TEMPLATE_DEBUG on, so that the origin file name will be kept with # the compiled templates. @override_settings(TEMPLATE_DEBUG=True) def test_loader_debug_origin(self): - old_loaders = loader.template_source_loaders + # 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 BaseLoader class, so we only need + # to test one of them. + load_name = 'login.html' + 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) - try: - loader.template_source_loaders = (filesystem.Loader(),) + # 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,) - # 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 BaseLoader class, so we only need - # to test one of them. - load_name = 'login.html' - 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) + template = loader.get_template(load_name) + template_name = template.nodelist[0].source[0].name + self.assertTrue(template_name.endswith(load_name), + 'Template loaded through cached 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,) - - template = loader.get_template(load_name) - template_name = template.nodelist[0].source[0].name - self.assertTrue(template_name.endswith(load_name), - 'Template loaded through cached loader has incorrect name for debug page: %s' % template_name) - - template = loader.get_template(load_name) - template_name = template.nodelist[0].source[0].name - self.assertTrue(template_name.endswith(load_name), - 'Cached template loaded through cached loader has incorrect name for debug page: %s' % template_name) - finally: - loader.template_source_loaders = old_loaders + template = loader.get_template(load_name) + template_name = template.nodelist[0].source[0].name + self.assertTrue(template_name.endswith(load_name), + 'Cached template loaded through cached loader has incorrect name for debug page: %s' % template_name) def test_loader_origin(self): with self.settings(TEMPLATE_DEBUG=True): @@ -262,33 +256,31 @@ class TemplateLoaderTests(TestCase): # TEMPLATE_DEBUG must be true, otherwise the exception raised # during {% include %} processing will be suppressed. @override_settings(TEMPLATE_DEBUG=True) + # Test the base loader class via the app loader. load_template + # from base is used by all shipped loaders excepting cached, + # which has its own test. + @override_template_loaders(app_directories.Loader()) def test_include_missing_template(self): """ Tests that the correct template is identified as not existing when {% include %} specifies a template that does not exist. """ - old_loaders = loader.template_source_loaders - + load_name = 'test_include_error.html' + r = None try: - # Test the base loader class via the app loader. load_template - # from base is used by all shipped loaders excepting cached, - # which has its own test. - loader.template_source_loaders = (app_directories.Loader(),) - - load_name = 'test_include_error.html' - r = None - try: - tmpl = loader.select_template([load_name]) - r = tmpl.render(template.Context({})) - except template.TemplateDoesNotExist as e: - self.assertEqual(e.args[0], 'missing.html') - self.assertEqual(r, None, 'Template rendering unexpectedly succeeded, produced: ->%r<-' % r) - finally: - loader.template_source_loaders = old_loaders + tmpl = loader.select_template([load_name]) + r = tmpl.render(template.Context({})) + except template.TemplateDoesNotExist as e: + self.assertEqual(e.args[0], 'missing.html') + self.assertEqual(r, None, 'Template rendering unexpectedly succeeded, produced: ->%r<-' % r) # TEMPLATE_DEBUG must be true, otherwise the exception raised # during {% include %} processing will be suppressed. @override_settings(TEMPLATE_DEBUG=True) + # Test the base loader class via the app loader. load_template + # from base is used by all shipped loaders excepting cached, + # which has its own test. + @override_template_loaders(app_directories.Loader()) def test_extends_include_missing_baseloader(self): """ Tests that the correct template is identified as not existing @@ -296,24 +288,14 @@ class TemplateLoaderTests(TestCase): that template has an {% include %} of something that does not exist. See #12787. """ - old_loaders = loader.template_source_loaders - + load_name = 'test_extends_error.html' + tmpl = loader.get_template(load_name) + r = None try: - # Test the base loader class via the app loader. load_template - # from base is used by all shipped loaders excepting cached, - # which has its own test. - loader.template_source_loaders = (app_directories.Loader(),) - - load_name = 'test_extends_error.html' - tmpl = loader.get_template(load_name) - r = None - try: - r = tmpl.render(template.Context({})) - except template.TemplateDoesNotExist as e: - self.assertEqual(e.args[0], 'missing.html') - self.assertEqual(r, None, 'Template rendering unexpectedly succeeded, produced: ->%r<-' % r) - finally: - loader.template_source_loaders = old_loaders + r = tmpl.render(template.Context({})) + except template.TemplateDoesNotExist as e: + self.assertEqual(e.args[0], 'missing.html') + self.assertEqual(r, None, 'Template rendering unexpectedly succeeded, produced: ->%r<-' % r) @override_settings(TEMPLATE_DEBUG=True) def test_extends_include_missing_cachedloader(self): @@ -321,14 +303,9 @@ class TemplateLoaderTests(TestCase): Same as test_extends_include_missing_baseloader, only tests behavior of the cached loader instead of BaseLoader. """ - - old_loaders = loader.template_source_loaders - - try: - cache_loader = cached.Loader(('',)) - cache_loader._cached_loaders = (app_directories.Loader(),) - loader.template_source_loaders = (cache_loader,) - + cache_loader = cached.Loader(('',)) + cache_loader._cached_loaders = (app_directories.Loader(),) + with override_template_loaders(cache_loader,): load_name = 'test_extends_error.html' tmpl = loader.get_template(load_name) r = None @@ -346,8 +323,6 @@ class TemplateLoaderTests(TestCase): except template.TemplateDoesNotExist as e: self.assertEqual(e.args[0], 'missing.html') self.assertEqual(r, None, 'Template rendering unexpectedly succeeded, produced: ->%r<-' % r) - finally: - loader.template_source_loaders = old_loaders def test_include_template_argument(self): """ @@ -544,89 +519,83 @@ class TemplateTests(TestCase): template_tests.update(filter_tests) - cache_loader = setup_test_template_loader( - dict((name, t[0]) for name, t in six.iteritems(template_tests)), - use_cached_loader=True, - ) + templates = dict((name, t[0]) for name, t in six.iteritems(template_tests)) + with override_with_test_loader(templates, use_cached_loader=True) as cache_loader: + failures = [] + tests = sorted(template_tests.items()) - failures = [] - tests = sorted(template_tests.items()) + # Set TEMPLATE_STRING_IF_INVALID to a known string. + expected_invalid_str = 'INVALID' - # Set TEMPLATE_STRING_IF_INVALID to a known string. - expected_invalid_str = 'INVALID' + # Warm the URL reversing cache. This ensures we don't pay the cost + # warming the cache during one of the tests. + urlresolvers.reverse('template_tests.views.client_action', + kwargs={'id': 0, 'action': "update"}) - # Warm the URL reversing cache. This ensures we don't pay the cost - # warming the cache during one of the tests. - urlresolvers.reverse('template_tests.views.client_action', - kwargs={'id': 0, 'action': "update"}) + for name, vals in tests: + if isinstance(vals[2], tuple): + normal_string_result = vals[2][0] + invalid_string_result = vals[2][1] - for name, vals in tests: - if isinstance(vals[2], tuple): - normal_string_result = vals[2][0] - invalid_string_result = vals[2][1] + if isinstance(invalid_string_result, tuple): + expected_invalid_str = 'INVALID %s' + invalid_string_result = invalid_string_result[0] % invalid_string_result[1] + template_base.invalid_var_format_string = True - if isinstance(invalid_string_result, tuple): - expected_invalid_str = 'INVALID %s' - invalid_string_result = invalid_string_result[0] % invalid_string_result[1] - template_base.invalid_var_format_string = True + try: + template_debug_result = vals[2][2] + except IndexError: + template_debug_result = normal_string_result - try: - template_debug_result = vals[2][2] - except IndexError: - template_debug_result = normal_string_result + else: + normal_string_result = vals[2] + invalid_string_result = vals[2] + template_debug_result = vals[2] - else: - normal_string_result = vals[2] - invalid_string_result = vals[2] - template_debug_result = vals[2] - - with translation.override(vals[1].get('LANGUAGE_CODE', 'en-us')): - - for invalid_str, template_debug, result in [ - ('', False, normal_string_result), - (expected_invalid_str, False, invalid_string_result), - ('', True, template_debug_result) - ]: - with override_settings(TEMPLATE_STRING_IF_INVALID=invalid_str, - TEMPLATE_DEBUG=template_debug): - for is_cached in (False, True): - try: + with translation.override(vals[1].get('LANGUAGE_CODE', 'en-us')): + for invalid_str, template_debug, result in [ + ('', False, normal_string_result), + (expected_invalid_str, False, invalid_string_result), + ('', True, template_debug_result) + ]: + with override_settings(TEMPLATE_STRING_IF_INVALID=invalid_str, + TEMPLATE_DEBUG=template_debug): + for is_cached in (False, True): try: - with warnings.catch_warnings(): - # Ignore pending deprecations of loading 'ssi' and 'url' tags from future. - warnings.filterwarnings("ignore", category=RemovedInDjango19Warning, module='django.templatetags.future') - # Ignore deprecations of loading 'cycle' and 'firstof' tags from future. - warnings.filterwarnings("ignore", category=RemovedInDjango20Warning, module="django.templatetags.future") - test_template = loader.get_template(name) - except ShouldNotExecuteException: - failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s', TEMPLATE_DEBUG=%s): %s -- FAILED. Template loading invoked method that shouldn't have been invoked." % (is_cached, invalid_str, template_debug, name)) + try: + with warnings.catch_warnings(): + # Ignore pending deprecations of loading 'ssi' and 'url' tags from future. + warnings.filterwarnings("ignore", category=RemovedInDjango19Warning, module='django.templatetags.future') + # Ignore deprecations of loading 'cycle' and 'firstof' tags from future. + warnings.filterwarnings("ignore", category=RemovedInDjango20Warning, module="django.templatetags.future") + test_template = loader.get_template(name) + except ShouldNotExecuteException: + failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s', TEMPLATE_DEBUG=%s): %s -- FAILED. Template loading invoked method that shouldn't have been invoked." % (is_cached, invalid_str, template_debug, name)) - try: - with warnings.catch_warnings(): - # Ignore deprecations of using the wrong number of variables with the 'for' tag. - warnings.filterwarnings("ignore", category=RemovedInDjango20Warning, module="django.template.defaulttags") - output = self.render(test_template, vals) - except ShouldNotExecuteException: - failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s', TEMPLATE_DEBUG=%s): %s -- FAILED. Template rendering invoked method that shouldn't have been invoked." % (is_cached, invalid_str, template_debug, name)) - except ContextStackException: - failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s', TEMPLATE_DEBUG=%s): %s -- FAILED. Context stack was left imbalanced" % (is_cached, invalid_str, template_debug, name)) - continue - except Exception: - exc_type, exc_value, exc_tb = sys.exc_info() - if exc_type != result: - tb = '\n'.join(traceback.format_exception(exc_type, exc_value, exc_tb)) - failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s', TEMPLATE_DEBUG=%s): %s -- FAILED. Got %s, exception: %s\n%s" % (is_cached, invalid_str, template_debug, name, exc_type, exc_value, tb)) - continue - 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)) - cache_loader.reset() + try: + with warnings.catch_warnings(): + # Ignore deprecations of using the wrong number of variables with the 'for' tag. + warnings.filterwarnings("ignore", category=RemovedInDjango20Warning, module="django.template.defaulttags") + output = self.render(test_template, vals) + except ShouldNotExecuteException: + failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s', TEMPLATE_DEBUG=%s): %s -- FAILED. Template rendering invoked method that shouldn't have been invoked." % (is_cached, invalid_str, template_debug, name)) + except ContextStackException: + failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s', TEMPLATE_DEBUG=%s): %s -- FAILED. Context stack was left imbalanced" % (is_cached, invalid_str, template_debug, name)) + continue + except Exception: + exc_type, exc_value, exc_tb = sys.exc_info() + if exc_type != result: + tb = '\n'.join(traceback.format_exception(exc_type, exc_value, exc_tb)) + failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s', TEMPLATE_DEBUG=%s): %s -- FAILED. Got %s, exception: %s\n%s" % (is_cached, invalid_str, template_debug, name, exc_type, exc_value, tb)) + continue + 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)) + cache_loader.reset() if template_base.invalid_var_format_string: expected_invalid_str = 'INVALID' template_base.invalid_var_format_string = False - restore_template_loaders() - self.assertEqual(failures, [], "Tests failed:\n%s\n%s" % ('-' * 70, ("\n%s\n" % ('-' * 70)).join(failures))) @@ -1887,13 +1856,13 @@ class RequestContextTests(unittest.TestCase): def setUp(self): templates = { - 'child': Template('{{ var|default:"none" }}'), + 'child': '{{ var|default:"none" }}', } - setup_test_template_loader(templates) + override_with_test_loader.override(templates) self.fake_request = RequestFactory().get('/') def tearDown(self): - restore_template_loaders() + override_with_test_loader.restore() def test_include_only(self): """ diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index 8b56cb583b..55a8ded31b 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/tests/view_tests/tests/test_debug.py @@ -17,8 +17,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.core.urlresolvers import reverse from django.template.base import TemplateDoesNotExist from django.test import TestCase, RequestFactory, override_settings -from django.test.utils import ( - setup_test_template_loader, restore_template_loaders) +from django.test.utils import override_with_test_loader from django.utils.encoding import force_text, force_bytes from django.utils import six from django.views.debug import ExceptionReporter @@ -47,23 +46,16 @@ class DebugViewTests(TestCase): def test_403(self): # Ensure no 403.html template exists to test the default case. - setup_test_template_loader({}) - try: + with override_with_test_loader({}): response = self.client.get('/raises403/') self.assertContains(response, '

403 Forbidden

', status_code=403) - finally: - restore_template_loaders() def test_403_template(self): # Set up a test 403.html template. - setup_test_template_loader( - {'403.html': 'This is a test template for a 403 Forbidden error.'} - ) - try: + with override_with_test_loader({'403.html': 'This is a test template ' + 'for a 403 Forbidden error.'}): response = self.client.get('/raises403/') self.assertContains(response, 'test template', status_code=403) - finally: - restore_template_loaders() def test_404(self): response = self.client.get('/raises404/') diff --git a/tests/view_tests/tests/test_defaults.py b/tests/view_tests/tests/test_defaults.py index 4d08dbb579..1f6a554c19 100644 --- a/tests/view_tests/tests/test_defaults.py +++ b/tests/view_tests/tests/test_defaults.py @@ -1,8 +1,7 @@ from __future__ import unicode_literals from django.test import TestCase -from django.test.utils import (setup_test_template_loader, - restore_template_loaders, override_settings) +from django.test.utils import override_settings, override_with_test_loader from ..models import UrlArticle @@ -41,17 +40,13 @@ class DefaultsTests(TestCase): Test that 404.html and 500.html templates are picked by their respective handler. """ - setup_test_template_loader( - {'404.html': 'This is a test template for a 404 error.', - '500.html': 'This is a test template for a 500 error.'} - ) - try: + with override_with_test_loader({ + '404.html': 'This is a test template for a 404 error.', + '500.html': 'This is a test template for a 500 error.'}): for code, url in ((404, '/non_existing_url/'), (500, '/server_error/')): response = self.client.get(url) self.assertContains(response, "test template for a %d error" % code, status_code=code) - finally: - restore_template_loaders() def test_get_absolute_url_attributes(self): "A model can set attributes on the get_absolute_url method"