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 when importing from a module with a bad import statement. 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,39 @@ else: self.fail("TestLoader.loadTestsFromName failed to raise AttributeError") + # "The specifier name is a ``dotted name'' that may resolve either to a + # module...." + # + # What happens when a module is found but it has a bad import statement? + def test_loadTestsFromName__bad_import(self): + + @contextmanager + def newdirinpath(dir): + os.mkdir(dir) + sys.path.insert(0, dir) + yield + sys.path.pop(0) + test_support.rmtree(dir) + + with newdirinpath(test_support.TESTFN), \ + test_support.EnvironmentVarGuard() as env: + env['PYTHONPATH'] = test_support.TESTFN + file_name = 'foofoo' + os.extsep + "py" + file_path = os.path.join(test_support.TESTFN, file_name) + f = open(file_path, 'w') + f.write('import barbar') + f.close() + + loader = unittest.TestLoader() + + try: + loader.loadTestsFromName('foofoo') + except ImportError, e: + self.assertEqual(str(e), "No module named barbar") + else: + self.fail("TestLoader.loadTestsFromName failed to raise an " + "ImportError") + # "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 Index: Lib/unittest/loader.py =================================================================== --- Lib/unittest/loader.py (revision 79760) +++ Lib/unittest/loader.py (working copy) @@ -74,6 +74,34 @@ self.suiteClass) return tests + # The stack-walking code of this method was taken from-- + # + # http://twistedmatrix.com/trac/browser/trunk/twisted/python/reflect.py#L382 + def _importWithExceptionCheck(self, module_name, module_not_found): + """Import the given module while checking for a module not found. + + An empty list should be passed in for the module_not_found parameter. + + If this method raises an exception due to a module not found, then + this method adds an element to module_not_found so the list evaluates + to True. This lets the caller distinguish between an ImportError due + to a bad import statements and an ImportError due to a module not + found -- while allowing the original exception to bubble up untouched + in the latter case. + + """ + try: + return __import__(module_name) + except ImportError: + excType, excValue, excTraceback = sys.exc_info() + while excTraceback: + execName = excTraceback.tb_frame.f_globals["__name__"] + if execName is None: # python 2.4+ cleans up execName. + raise excType, excValue, excTraceback + excTraceback = excTraceback.tb_next + module_not_found.append(1) + raise + def loadTestsFromName(self, name, module=None): """Return a suite of all tests cases given a string specifier. @@ -88,15 +116,22 @@ parts_copy = parts[:] while parts_copy: try: - module = __import__('.'.join(parts_copy)) + import_name = '.'.join(parts_copy) + module_not_found = [] + module = self._importWithExceptionCheck(import_name, + module_not_found) break except ImportError: del parts_copy[-1] - if not parts_copy: + if not parts_copy or not module_not_found: raise parts = parts[1:] obj = module for part in parts: + # If the module was not found above, then the following line + # raises an AttributeError. We keep this behavior rather than + # allowing the original ImportError to bubble up so as not to + # break code that is currently catching AttributeError. parent, obj = obj, getattr(obj, part) if isinstance(obj, types.ModuleType):