Index: Misc/NEWS =================================================================== --- Misc/NEWS (revision 79760) +++ Misc/NEWS (working copy) @@ -176,6 +176,10 @@ - Issue #1220212: Added os.kill support for Windows, including support for sending CTRL+C and CTRL+BREAK events to console subprocesses. +- Issue #7559: unittest: TestLoader.loadTestsFromName() now lets ImportErrors + bubble up if a bad import statement is encountered while loading a nested + module. Before the method raised an AttributeError. + Extension Modules ----------------- Index: Lib/unittest/test/test_loader.py =================================================================== --- Lib/unittest/test/test_loader.py (revision 79760) +++ Lib/unittest/test/test_loader.py (working copy) @@ -1,6 +1,9 @@ +import os import sys import types +from contextlib import contextmanager +from test import test_support import unittest @@ -259,6 +262,174 @@ else: self.fail("TestLoader.loadTestsFromName failed to raise AttributeError") + ## Test methods related to ImportErrors + #### + + # For this group of test methods, the specifier name is a ``dotted name'' + # that may resolve to either a nested module (or nested module not found). + # + # By nested module, we mean a module package_foo.bar, for example, + # where package_foo is found. + # + # What happens if a bad import statement is encountered while loading + # a nested module? + # + # In these cases we want the ImportError to bubble up instead of an + # AttributeError being raised. An AttributeError should be raised only + # if the nested module were not found and there were no intervening + # ImportErrors (i.e. somewhere in the __init__.py files leading up to + # the nested module). + # + # To test various import scenarios, we require some helper methods to set + # up temporary package directories, etc. + + @contextmanager + def _dir_in_path(self, dir): + os.mkdir(dir) + sys.path.insert(0, dir) + try: + yield + finally: + sys.path.pop(0) + test_support.rmtree(dir) + + def _create_file(self, path, text=''): + """Create a file with given contents.""" + f = open(path, 'w') + f.write(text) + f.close() + + def _create_package(self, package_dir, init_text=''): + """Create a package with given __init__ contents.""" + os.mkdir(package_dir) + init_file_name = '__init__' + os.extsep + 'py' + init_file_path = os.path.join(package_dir, init_file_name) + self._create_file(init_file_path, init_text) + + def _test_in_context(self, test_function): + test_dir = test_support.TESTFN + with self._dir_in_path(test_dir), \ + test_support.EnvironmentVarGuard() as env: + #env['PYTHONPATH'] = test_support.TESTFN + package_name = 'package_foo' + package_dir = os.path.join(test_dir, package_name) + self._create_package(package_dir) + test_function(package_dir) + + # Parameter: package_foo.mod_with_bad_import. + # + # package_foo/mod_with_bad_import.py: + # import no_exist + # + # This is the standard case. This should raise an ImportError since + # the module exists but contains a bad import statement. + def test_loadTestsFromName__with_bad_import(self): + def do_test(package_dir): + file_name = 'mod_with_bad_import' + os.extsep + 'py' + path = os.path.join(package_dir, file_name) + self._create_file(path, 'import no_exist') + + loader = unittest.TestLoader() + + try: + loader.loadTestsFromName('package_foo.mod_with_bad_import') + except ImportError, e: + self.assertEqual(str(e), "No module named no_exist") + else: + self.fail("TestLoader.loadTestsFromName failed to raise an " + "ImportError") + + self._test_in_context(do_test) + + # Parameter: package_foo.subpackage.no_exist_xxx + # + # package/subpackage/__init__.py: + # import no_exist + # + # This should raise an ImportError despite the module not existing since + # an intervening __init__.py has a bad import statement. + def test_loadTestsFromName__with_bad_import_in_init(self): + def do_test(package_dir): + sub_name = 'subpackage' + sub_dir = os.path.join(package_dir, sub_name) + self._create_package(sub_dir, 'import no_exist') + + loader = unittest.TestLoader() + + try: + loader.loadTestsFromName('package_foo.subpackage.no_exist_xxx') + except ImportError, e: + self.assertEqual(str(e), "No module named no_exist") + else: + self.fail("TestLoader.loadTestsFromName failed to raise an " + "ImportError") + + self._test_in_context(do_test) + + # Parameter: package_foo.subpackage.good + # + # package_foo/subpackage/__init__.py: + # import package_foo.subpackage.good + # import no_exist + # + # This should raise an ImportError. This case is unusual because the + # nested module has already been processed by the time the bad import + # import statement is reached. This is because of the module's + # explicit appearance in an intervening __init__.py. + def test_loadTestsFromName__with_bad_import_after_import(self): + def do_test(package_dir): + sub_name = 'subpackage' + sub_dir = os.path.join(package_dir, sub_name) + self._create_package(sub_dir, 'import package.subpackage.good\n' + 'import no_exist') + + file_name = 'good' + os.extsep + 'py' + path = os.path.join(sub_dir, file_name) + self._create_file(path) # empty module + + loader = unittest.TestLoader() + + try: + loader.loadTestsFromName('package_foo.subpackage.good') + except ImportError, e: + self.assertEqual(str(e), "No module named no_exist") + else: + self.fail("TestLoader.loadTestsFromName failed to raise an " + "ImportError") + + self._test_in_context(do_test) + + # Parameter: package_foo.subpackage.no_exist + # + # package/subpackage/__init__.py: + # import package_foo.subpackage.no_exist + # + # This is another unusual case. This should raise an ImportError despite + # the module not existing since the intervening __init__.py has a bad + # import statement (which happens to be for the same module being + # explicitly loaded). + def test_loadTestsFromName__with_same_no_exist_import_in_init(self): + def do_test(package_dir): + sub_name = 'subpackage' + sub_dir = os.path.join(package_dir, sub_name) + self._create_package(sub_dir, + 'import package_foo.subpackage.no_exist') + + loader = unittest.TestLoader() + + try: + loader.loadTestsFromName('package_foo.subpackage.no_exist') + except ImportError, e: + self.assertEqual(str(e), "No module named no_exist") + else: + self.fail("TestLoader.loadTestsFromName failed to raise an " + "ImportError") + + self._test_in_context(do_test) + + #### + ## /Test methods related to ImportErrors + # "The specifier name is a ``dotted name'' that may resolve either to # a module, a test case class, a TestSuite instance, a test method # within a test case class, or a callable object which returns a