diff --git a/django/utils/module_loading.py b/django/utils/module_loading.py index e8bc41ca31..f251035387 100644 --- a/django/utils/module_loading.py +++ b/django/utils/module_loading.py @@ -1,26 +1,60 @@ -import os import imp +import os +import sys -def module_has_submodule(mod, submod_name): - # If the module was loaded from an egg, __loader__ will be set and - # its find_module must be used to search for submodules. - loader = getattr(mod, '__loader__', None) - if loader: - mod_path = "%s.%s" % (mod.__name__[mod.__name__.rfind('.')+1:], submod_name) - x = loader.find_module(mod_path) - if x is None: - # zipimport.zipimporter.find_module is documented to take - # dotted paths but in fact through Python 2.7 is observed - # to require os.sep in place of dots...so try using os.sep - # if the dotted path version failed to find the requested - # submodule. - x = loader.find_module(mod_path.replace('.', os.sep)) - return x is not None - try: - imp.find_module(submod_name, mod.__path__) +def module_has_submodule(package, module_name): + """See if 'module' is in 'package'.""" + name = ".".join([package.__name__, module_name]) + if name in sys.modules: return True - except ImportError: + for finder in sys.meta_path: + if finder.find_module(name): + return True + for entry in package.__path__: # No __path__, then not a package. + try: + # Try the cached finder. + finder = sys.path_importer_cache[entry] + if finder is None: + # Implicit import machinery should be used. + try: + file_, _, _ = imp.find_module(module_name, [entry]) + if file_: + file_.close() + return True + except ImportError: + continue + # Else see if the finder knows of a loader. + elif finder.find_module(name): + return True + else: + continue + except KeyError: + # No cached finder, so try and make one. + for hook in sys.path_hooks: + try: + finder = hook(entry) + # XXX Could cache in sys.path_importer_cache + if finder.find_module(name): + return True + else: + # Once a finder is found, stop the search. + break + except ImportError: + # Continue the search for a finder. + continue + else: + # No finder found. + # Try the implicit import machinery if searching a directory. + if os.path.isdir(entry): + try: + file_, _, _ = imp.find_module(module_name, [entry]) + if file_: + file_.close() + return True + except ImportError: + pass + # XXX Could insert None or NullImporter + else: + # Exhausted the search, so the module cannot be found. return False - - diff --git a/tests/regressiontests/utils/eggs/test_egg.egg b/tests/regressiontests/utils/eggs/test_egg.egg new file mode 100644 index 0000000000..9b08cc10ef Binary files /dev/null and b/tests/regressiontests/utils/eggs/test_egg.egg differ diff --git a/tests/regressiontests/utils/module_loading.py b/tests/regressiontests/utils/module_loading.py new file mode 100644 index 0000000000..7b233f8e14 --- /dev/null +++ b/tests/regressiontests/utils/module_loading.py @@ -0,0 +1,139 @@ +import os +import sys +from unittest import TestCase +from zipimport import zipimporter + +from django.utils.importlib import import_module +from django.utils.module_loading import module_has_submodule + +class DefaultLoader(TestCase): + def test_loader(self): + "Normal module existence can be tested" + test_module = import_module('regressiontests.utils.test_module') + + # An importable child + self.assertTrue(module_has_submodule(test_module, 'good_module')) + mod = import_module('regressiontests.utils.test_module.good_module') + self.assertEqual(mod.content, 'Good Module') + + # A child that exists, but will generate an import error if loaded + self.assertTrue(module_has_submodule(test_module, 'bad_module')) + self.assertRaises(ImportError, import_module, 'regressiontests.utils.test_module.bad_module') + + # A child that doesn't exist + self.assertFalse(module_has_submodule(test_module, 'no_such_module')) + self.assertRaises(ImportError, import_module, 'regressiontests.utils.test_module.no_such_module') + +class EggLoader(TestCase): + def setUp(self): + self.old_path = sys.path + self.egg_dir = '%s/eggs' % os.path.dirname(__file__) + + def tearDown(self): + sys.path = self.old_path + + def test_shallow_loader(self): + "Module existence can be tested inside eggs" + egg_name = '%s/test_egg.egg' % self.egg_dir + sys.path.append(egg_name) + egg_module = import_module('egg_module') + + # An importable child + self.assertTrue(module_has_submodule(egg_module, 'good_module')) + mod = import_module('egg_module.good_module') + self.assertEqual(mod.content, 'Good Module') + + # A child that exists, but will generate an import error if loaded + self.assertTrue(module_has_submodule(egg_module, 'bad_module')) + self.assertRaises(ImportError, import_module, 'egg_module.bad_module') + + # A child that doesn't exist + self.assertFalse(module_has_submodule(egg_module, 'no_such_module')) + self.assertRaises(ImportError, import_module, 'egg_module.no_such_module') + + def test_deep_loader(self): + "Modules deep inside an egg can still be tested for existence" + egg_name = '%s/test_egg.egg' % self.egg_dir + sys.path.append(egg_name) + egg_module = import_module('egg_module.sub1.sub2') + + # An importable child + self.assertTrue(module_has_submodule(egg_module, 'good_module')) + mod = import_module('egg_module.sub1.sub2.good_module') + self.assertEqual(mod.content, 'Deep Good Module') + + # A child that exists, but will generate an import error if loaded + self.assertTrue(module_has_submodule(egg_module, 'bad_module')) + self.assertRaises(ImportError, import_module, 'egg_module.sub1.sub2.bad_module') + + # A child that doesn't exist + self.assertFalse(module_has_submodule(egg_module, 'no_such_module')) + self.assertRaises(ImportError, import_module, 'egg_module.sub1.sub2.no_such_module') + +class TestFinder(object): + def __init__(self, *args, **kwargs): + self.importer = zipimporter(*args, **kwargs) + + def find_module(self, path): + importer = self.importer.find_module(path) + if importer is None: + return + return TestLoader(importer) + +class TestLoader(object): + def __init__(self, importer): + self.importer = importer + + def load_module(self, name): + mod = self.importer.load_module(name) + mod.__loader__ = self + return mod + +class CustomLoader(TestCase): + def setUp(self): + self.egg_dir = '%s/eggs' % os.path.dirname(__file__) + self.old_path = sys.path + sys.path_hooks.insert(0, TestFinder) + sys.path_importer_cache.clear() + + def tearDown(self): + sys.path = self.old_path + sys.path_hooks.pop(0) + + def test_shallow_loader(self): + "Module existence can be tested with a custom loader" + egg_name = '%s/test_egg.egg' % self.egg_dir + sys.path.append(egg_name) + egg_module = import_module('egg_module') + + # An importable child + self.assertTrue(module_has_submodule(egg_module, 'good_module')) + mod = import_module('egg_module.good_module') + self.assertEqual(mod.content, 'Good Module') + + # A child that exists, but will generate an import error if loaded + self.assertTrue(module_has_submodule(egg_module, 'bad_module')) + self.assertRaises(ImportError, import_module, 'egg_module.bad_module') + + # A child that doesn't exist + self.assertFalse(module_has_submodule(egg_module, 'no_such_module')) + self.assertRaises(ImportError, import_module, 'egg_module.no_such_module') + + def test_deep_loader(self): + "Modules existence can be tested deep inside a custom loader" + egg_name = '%s/test_egg.egg' % self.egg_dir + sys.path.append(egg_name) + egg_module = import_module('egg_module.sub1.sub2') + + # An importable child + self.assertTrue(module_has_submodule(egg_module, 'good_module')) + mod = import_module('egg_module.sub1.sub2.good_module') + self.assertEqual(mod.content, 'Deep Good Module') + + # A child that exists, but will generate an import error if loaded + self.assertTrue(module_has_submodule(egg_module, 'bad_module')) + self.assertRaises(ImportError, import_module, 'egg_module.sub1.sub2.bad_module') + + # A child that doesn't exist + self.assertFalse(module_has_submodule(egg_module, 'no_such_module')) + self.assertRaises(ImportError, import_module, 'egg_module.sub1.sub2.no_such_module') diff --git a/tests/regressiontests/utils/test_module/__init__.py b/tests/regressiontests/utils/test_module/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/utils/test_module/bad_module.py b/tests/regressiontests/utils/test_module/bad_module.py new file mode 100644 index 0000000000..cc0cd168ce --- /dev/null +++ b/tests/regressiontests/utils/test_module/bad_module.py @@ -0,0 +1,3 @@ +import a_package_name_that_does_not_exist + +content = 'Bad Module' \ No newline at end of file diff --git a/tests/regressiontests/utils/test_module/good_module.py b/tests/regressiontests/utils/test_module/good_module.py new file mode 100644 index 0000000000..0ca689832a --- /dev/null +++ b/tests/regressiontests/utils/test_module/good_module.py @@ -0,0 +1 @@ +content = 'Good Module' \ No newline at end of file diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py index 812276b554..edbc2792bf 100644 --- a/tests/regressiontests/utils/tests.py +++ b/tests/regressiontests/utils/tests.py @@ -32,6 +32,7 @@ __test__ = { from dateformat import * from feedgenerator import * +from module_loading import * class TestUtilsHtml(TestCase):