diff --git a/Lib/unittest/loader.py b/Lib/unittest/loader.py --- a/Lib/unittest/loader.py +++ b/Lib/unittest/loader.py @@ -6,6 +6,7 @@ import traceback import types import functools +import importlib from fnmatch import fnmatch @@ -92,14 +93,17 @@ if module is None: parts_copy = parts[:] while parts_copy: + aname = '.'.join(parts_copy) try: - module = __import__('.'.join(parts_copy)) + module = importlib.import_module(aname) break - except ImportError: + except ImportError as e: + if e.name != aname and not str(e).endswith('not a package'): + raise del parts_copy[-1] if not parts_copy: raise - parts = parts[1:] + parts = parts[len(parts_copy):] obj = module for part in parts: parent, obj = obj, getattr(obj, part) diff --git a/Lib/unittest/test/test_loader.py b/Lib/unittest/test/test_loader.py --- a/Lib/unittest/test/test_loader.py +++ b/Lib/unittest/test/test_loader.py @@ -1,6 +1,9 @@ +import os import sys import types +from contextlib import contextmanager +from test import support import unittest @@ -264,6 +267,208 @@ # within a test case class, or a callable object which returns a # TestCase or TestSuite instance." # + # What happens when the module is found, but the attribute can't -- + # when a custom __import__ is being used? + # + # Replacing __import__ alters the stack trace of ImportErrors when a + # module is not found. This method helps ensure that code is not + # relying on the form of the stack trace in certain ways. + def test_loadTestsFromName__unknown_attr_name_and_new__import__(self): + @contextmanager + def replaced_import(): + old__import__ = __builtins__['__import__'] + + # This is a minimal __import__ wrapper to alter the stack trace. + def my__import__(*args, **kwargs): + """Import a module.""" + return old__import__(*args, **kwargs) + + __builtins__['__import__'] = my__import__ + try: + yield + finally: + __builtins__['__import__'] = old__import__ + + loader = unittest.TestLoader() + + try: + with replaced_import(): + loader.loadTestsFromName('unittest.sdasfasfasdf') + except AttributeError as e: + self.assertEqual(str(e), "'module' object has no attribute " + "'sdasfasfasdf'") + else: + self.fail("TestLoader.loadTestsFromName failed to raise " + "AttributeError") + + ## Test methods related to ImportErrors (see issue 7559) + + # 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. + + 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 _do_test_in_context(self, do_test, package_name): + """Call test code in the appropriate context managers. + + The context managers create a temp directory, add it to sys.path, + and create a package in the temp directory with the given name. + + """ + with support.temp_cwd(support.TESTFN) as cwd: + with support.DirsOnSysPath(cwd): + self._create_package(package_name) + do_test() + + # 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): + package_name = 'package_foo' + def do_test(): + file_name = 'mod_with_bad_import' + os.extsep + 'py' + path = os.path.join(package_name, file_name) + self._create_file(path, 'import no_exist') + + loader = unittest.TestLoader() + + try: + loader.loadTestsFromName('package_foo.mod_with_bad_import') + except ImportError as e: + self.assertEqual(str(e), "No module named 'no_exist'") + else: + self.fail("TestLoader.loadTestsFromName failed to raise an " + "ImportError") + + self._do_test_in_context(do_test, package_name) + + # 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): + package_name = 'package_foo' + def do_test(): + sub_name = 'subpackage' + sub_dir = os.path.join(package_name, sub_name) + self._create_package(sub_dir, 'import no_exist') + + loader = unittest.TestLoader() + + try: + loader.loadTestsFromName('package_foo.subpackage.no_exist_xxx') + except ImportError as e: + self.assertEqual(str(e), "No module named 'no_exist'") + else: + self.fail("TestLoader.loadTestsFromName failed to raise an " + "ImportError") + + self._do_test_in_context(do_test, package_name) + + # 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_same_init_import(self): + package_name = 'package_foo' + def do_test(): + sub_name = 'subpackage' + sub_dir = os.path.join(package_name, sub_name) + self._create_package(sub_dir, 'import package_foo.subpackage.good\n' + 'import no_exist') + + file_name = 'good' + os.extsep + 'py' + path = os.path.join(sub_dir, file_name) + self._create_file(path, 'pass') # empty module + + loader = unittest.TestLoader() + + try: + loader.loadTestsFromName('package_foo.subpackage.good') + except ImportError as e: + self.assertEqual(str(e), "No module named 'no_exist'") + else: + self.fail("TestLoader.loadTestsFromName failed to raise an " + "ImportError") + + self._do_test_in_context(do_test, package_name) + + # 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_init_import(self): + package_name = 'package_foo' + def do_test(): + sub_name = 'subpackage' + sub_dir = os.path.join(package_name, 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 as e: + self.assertEqual(str(e), "No module named " + "'package_foo.subpackage.no_exist'") + else: + self.fail("TestLoader.loadTestsFromName failed to raise an " + "ImportError") + + self._do_test_in_context(do_test, package_name) + + ## End Test methods related to ImportErrors (see issue 7559) + + # "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 + # TestCase or TestSuite instance." + # # What happens when we provide the module, but the attribute can't be # found? def test_loadTestsFromName__relative_unknown_name(self):