diff --git a/Lib/packaging/tests/test_util.py b/Lib/packaging/tests/test_util.py --- a/Lib/packaging/tests/test_util.py +++ b/Lib/packaging/tests/test_util.py @@ -388,6 +388,18 @@ class UtilTestCase(support.EnvironRestor def baz(self): pass """)) + self.write_file((tmpdir, 'syntaxerror.py'), 'mport unittest') + self.write_file((tmpdir, 'a', 'syntaxerror.py'), 'mport unittest') + self.write_file((tmpdir, 'importerror.py'), 'import MAGIC') + self.write_file((tmpdir, 'a', 'importerror.py'), 'import MAGIC') + customerror = textwrap.dedent("""\ + class Error(Exception): + pass + + raise Error('very useful error message') + """) + self.write_file((tmpdir, 'customerror.py'), customerror) + self.write_file((tmpdir, 'a', 'customerror.py'), customerror) # check Python, C and built-in module self.assertEqual(resolve_name('hello').__name__, 'hello') @@ -415,6 +427,21 @@ class UtilTestCase(support.EnvironRestor self.assertRaises(ImportError, resolve_name, 'a.b.Spam') self.assertRaises(ImportError, resolve_name, 'a.b.c.Spam') + # make sure exceptions pass through instead of being wrapped into + # ImportErrors which would lose useful information + for name, exc, msg in ( + ('syntaxerror', SyntaxError, 'invalid syntax.*line 1'), + ('a.syntaxerror', SyntaxError, 'invalid syntax.*line 1'), + ('importerror', ImportError, "No module named '?MAGIC'?"), + # see docstring of resolve_name and the XXX comment in the source + # for an explanation of why the following error message is an + # exception to the rule + ('a.importerror', ImportError, "has no attribute 'importerror'"), + ('customerror', Exception, 'very useful error message'), + ('a.customerror', Exception, 'very useful error message'), + ): + self.assertRaisesRegex(exc, msg, resolve_name, name) + def test_run_2to3_on_code(self): content = "print 'test'" converted_content = "print('test')" diff --git a/Lib/packaging/util.py b/Lib/packaging/util.py --- a/Lib/packaging/util.py +++ b/Lib/packaging/util.py @@ -630,16 +630,20 @@ def find_packages(paths=(os.curdir,), ex def resolve_name(name): """Resolve a name like ``module.object`` to an object and return it. - This functions supports packages and attributes without depth limitation: + This functions supports packages and attributes without depth limit: ``package.package.module.class.class.function.attr`` is valid input. However, looking up builtins is not directly supported: use ``builtins.name``. Raises ImportError if importing the module fails or if one requested - attribute is not found. + attribute is not found. May also raise other types of exceptions + caused by importing a module, such as SyntaxError. Due to an + implementation detail, if a submodule raises an ImportError the error + message will mention a missing attribute instead of giving the name of + the module that wasn't found. """ + # fast path for simple module names if '.' not in name: - # shortcut __import__(name) return sys.modules[name] @@ -649,17 +653,29 @@ def resolve_name(name): module_name = parts[:cursor] ret = '' + # try to import as much as possible while cursor > 0: + name = '.'.join(module_name) try: - ret = __import__('.'.join(module_name)) + ret = __import__(name) break - except ImportError: + except ImportError as exc: + # XXX ImportError may mean (1) all modules are imported, now get + # attributes or (2) one of the modules raised an ImportError which + # should propagate immediately, if we could make the difference cursor -= 1 module_name = parts[:cursor] + # if importing the module causes an exception, let it + # propagate immediately + except Exception: + raise + # if we did not even import one part, raise now if ret == '': raise ImportError(parts[0]) + # modules and submodules are imported, now use getattr to go + # through attributes for part in parts[1:]: try: ret = getattr(ret, part)