diff --git a/README.rst b/README.rst index 6025da8..208e4e7 100644 --- a/README.rst +++ b/README.rst @@ -24,6 +24,7 @@ Features * Hygienic TransactionTestCases, which can save you a DB flush per test * Support for various databases. Tested with MySQL, PostgreSQL, and SQLite. Others should work as well. +* Support for automatically validating Django templates by compiling them. .. _nose: http://somethingaboutorange.com/mrl/projects/nose/ .. _nose plugins: http://nose-plugins.jottit.com/ @@ -321,6 +322,9 @@ django-nose does not support Django 1.0. Recent Version History ---------------------- +1.3 (Unreleased) + * Support validating Django templates by compiling them (rpatterson) + 1.2 (2013-07-23) * Python 3 support (melinath and jonashaag) * Django 1.5 compat (fabiosantoscode) diff --git a/django_nose/runner.py b/django_nose/runner.py index 5e126f5..d93e96b 100644 --- a/django_nose/runner.py +++ b/django_nose/runner.py @@ -27,6 +27,7 @@ from django_nose.plugin import DjangoSetUpPlugin, ResultPlugin, TestReorderer from django_nose.utils import uses_mysql +from django_nose.templates import DjangoTemplates try: any @@ -68,7 +69,8 @@ def _get_test_db_name(self): def _get_plugins_from_settings(): plugins = (list(getattr(settings, 'NOSE_PLUGINS', [])) + - ['django_nose.plugin.TestReorderer']) + ['django_nose.plugin.TestReorderer', + 'django_nose.templates.DjangoTemplates']) for plug_path in plugins: try: dot = plug_path.rindex('.') @@ -138,7 +140,8 @@ def run_suite(self, nose_argv): result_plugin = ResultPlugin() plugins_to_add = [DjangoSetUpPlugin(self), result_plugin, - TestReorderer()] + TestReorderer(), + DjangoTemplates()] for plugin in _get_plugins_from_settings(): plugins_to_add.append(plugin) diff --git a/django_nose/templates.py b/django_nose/templates.py new file mode 100644 index 0000000..06d5e2a --- /dev/null +++ b/django_nose/templates.py @@ -0,0 +1,81 @@ +""" +Generate tests for compiling each Django template to validate them. +""" + +import os +import re +import unittest + +from nose.plugins import base + + +def is_descendant(ancestor, descendant): + return os.path.join(descendant, '').startswith(os.path.join(ancestor, '')) + + +class DjangoTemplates(base.Plugin): + __doc__ + + name = 'django-templates' + test_name_pattern = re.compile(r'\W') + + def configure(self, options, config): + """Make sure the Django template loader cache is populated.""" + base.Plugin.configure(self, options, config) + from django.template import loader + try: + loader.find_template('') + except loader.TemplateDoesNotExist: + pass + + self.sources = set() + self.loader = unittest.TestLoader() + + def wantDirectory(self, path): + """Select all files in a Django templates directory if enabled.""" + from django.template import loader + for source in self.sources: + if is_descendant(source, path): + return None + for source_loader in loader.template_source_loaders: + try: + sources = source_loader.get_template_sources('.') + except loader.TemplateDoesNotExist: + continue + for source in sources: + if is_descendant(source, path): + self.sources.add(source) + return True + return None + + def loadTestsFromDir(self, path): + """Construct tests for all files in a Django templates directory.""" + if path not in self.sources: + return None + + from django.template import loader + from django import test + + tests = {} + for root, dirs, files in os.walk(path): + for filename in files: + file_path = os.path.join(root, filename) + if not os.path.isfile(file_path): + # self.wantDirectory will handle directories + continue + + def generatedDjangoTemplateTest(test, file_path=file_path): + template = loader.get_template(file_path[len(path) + 1:]) + test.assertTrue( + callable(getattr(template, 'render', None))) + + generatedDjangoTemplateTest.__doc__ = ( + "Template compiles: {0!r}".format( + os.path.relpath(file_path))) + test_name = 'test_django_template_{0}'.format( + self.test_name_pattern.sub('_', filename)) + generatedDjangoTemplateTest.func_name = test_name + tests[test_name] = generatedDjangoTemplateTest + + case = type('DjangoTemplateTestCase', (test.TestCase, ), tests) + return self.loader.loadTestsFromTestCase(case) diff --git a/setup.py b/setup.py index 329eedb..2077b4d 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='django-nose', - version='1.2', + version='1.3', description='Makes your Django tests simple and snappy', long_description=open(os.path.join(ROOT, 'README.rst')).read(), author='Jeff Balogh', @@ -29,6 +29,7 @@ #entry_points=""" # [nose.plugins.0.10] # fixture_bundler = django_nose.fixture_bundling:FixtureBundlingPlugin + # django_templates = django_nose.templates:DjangoTemplates # """, classifiers=[ 'Development Status :: 5 - Production/Stable',