Commit 096db060 authored by Brett Cannon's avatar Brett Cannon

Issue #15767: Introduce ModuleNotFoundError, a subclass of

ImportError.

The exception is raised by import when a module could not be found.
Technically this is defined as no viable loader could be found for the
specified module. This includes ``from ... import`` statements so that
the module usage is consistent for all situations where import
couldn't find what was requested.

This should allow for the common idiom of::

  try:
    import something
  except ImportError:
    pass

to be updated to using ModuleNotFoundError and not accidentally mask
ImportError messages that should propagate (e.g. issues with a
loader).

This work was driven by the fact that the ``from ... import``
statement needed to be able to tell the difference between an
ImportError that simply couldn't find a module (and thus silence the
exception so that ceval can raise it) and an ImportError that
represented an actual problem.
parent f9179378
...@@ -686,6 +686,8 @@ the variables: ...@@ -686,6 +686,8 @@ the variables:
+-----------------------------------------+---------------------------------+----------+ +-----------------------------------------+---------------------------------+----------+
| :c:data:`PyExc_ImportError` | :exc:`ImportError` | | | :c:data:`PyExc_ImportError` | :exc:`ImportError` | |
+-----------------------------------------+---------------------------------+----------+ +-----------------------------------------+---------------------------------+----------+
| :c:data:`PyExc_ModuleNotFoundError` | :exc:`ModuleNotFoundError` | |
+-----------------------------------------+---------------------------------+----------+
| :c:data:`PyExc_IndexError` | :exc:`IndexError` | | | :c:data:`PyExc_IndexError` | :exc:`IndexError` | |
+-----------------------------------------+---------------------------------+----------+ +-----------------------------------------+---------------------------------+----------+
| :c:data:`PyExc_InterruptedError` | :exc:`InterruptedError` | | | :c:data:`PyExc_InterruptedError` | :exc:`InterruptedError` | |
......
...@@ -169,8 +169,8 @@ The following exceptions are the exceptions that are usually raised. ...@@ -169,8 +169,8 @@ The following exceptions are the exceptions that are usually raised.
.. exception:: ImportError .. exception:: ImportError
Raised when an :keyword:`import` statement fails to find the module definition Raised when the :keyword:`import` statement has troubles trying to load a
or when a ``from ... import`` fails to find a name that is to be imported. module.
The :attr:`name` and :attr:`path` attributes can be set using keyword-only The :attr:`name` and :attr:`path` attributes can be set using keyword-only
arguments to the constructor. When set they represent the name of the module arguments to the constructor. When set they represent the name of the module
...@@ -180,6 +180,15 @@ The following exceptions are the exceptions that are usually raised. ...@@ -180,6 +180,15 @@ The following exceptions are the exceptions that are usually raised.
.. versionchanged:: 3.3 .. versionchanged:: 3.3
Added the :attr:`name` and :attr:`path` attributes. Added the :attr:`name` and :attr:`path` attributes.
.. exception:: ModuleNotFoundError
A subclass of :exc:`ImportError` which is raised by :keyword:`import` when a
module could not be located. This includes ``from ... import`` statements as
the specific attribute being requested cannot be known a priori to be a module
or some other type of object.
.. versionadded:: 3.4
.. exception:: IndexError .. exception:: IndexError
......
...@@ -137,6 +137,9 @@ Some smaller changes made to the core Python language are: ...@@ -137,6 +137,9 @@ Some smaller changes made to the core Python language are:
* Unicode database updated to UCD version 6.2. * Unicode database updated to UCD version 6.2.
* Import now raises the new exception :exc:`ModuleNotFoundError` (subclass of
:exc:`ImportError`) when it cannot find something.
New Modules New Modules
......
...@@ -152,6 +152,7 @@ PyAPI_DATA(PyObject *) PyExc_EOFError; ...@@ -152,6 +152,7 @@ PyAPI_DATA(PyObject *) PyExc_EOFError;
PyAPI_DATA(PyObject *) PyExc_FloatingPointError; PyAPI_DATA(PyObject *) PyExc_FloatingPointError;
PyAPI_DATA(PyObject *) PyExc_OSError; PyAPI_DATA(PyObject *) PyExc_OSError;
PyAPI_DATA(PyObject *) PyExc_ImportError; PyAPI_DATA(PyObject *) PyExc_ImportError;
PyAPI_DATA(PyObject *) PyExc_ModuleNotFoundError;
PyAPI_DATA(PyObject *) PyExc_IndexError; PyAPI_DATA(PyObject *) PyExc_IndexError;
PyAPI_DATA(PyObject *) PyExc_KeyError; PyAPI_DATA(PyObject *) PyExc_KeyError;
PyAPI_DATA(PyObject *) PyExc_KeyboardInterrupt; PyAPI_DATA(PyObject *) PyExc_KeyboardInterrupt;
......
...@@ -1553,11 +1553,7 @@ def _find_and_load_unlocked(name, import_): ...@@ -1553,11 +1553,7 @@ def _find_and_load_unlocked(name, import_):
raise ImportError(msg, name=name) raise ImportError(msg, name=name)
loader = _find_module(name, path) loader = _find_module(name, path)
if loader is None: if loader is None:
exc = ImportError(_ERR_MSG.format(name), name=name) raise ModuleNotFoundError(_ERR_MSG.format(name), name=name)
# TODO(brett): switch to a proper ModuleNotFound exception in Python
# 3.4.
exc._not_found = True
raise exc
elif name not in sys.modules: elif name not in sys.modules:
# The parent import may have already imported this module. # The parent import may have already imported this module.
loader.load_module(name) loader.load_module(name)
...@@ -1643,13 +1639,10 @@ def _handle_fromlist(module, fromlist, import_): ...@@ -1643,13 +1639,10 @@ def _handle_fromlist(module, fromlist, import_):
from_name = '{}.{}'.format(module.__name__, x) from_name = '{}.{}'.format(module.__name__, x)
try: try:
_call_with_frames_removed(import_, from_name) _call_with_frames_removed(import_, from_name)
except ImportError as exc: except ModuleNotFoundError as exc:
# Backwards-compatibility dictates we ignore failed # Backwards-compatibility dictates we ignore failed
# imports triggered by fromlist for modules that don't # imports triggered by fromlist for modules that don't
# exist. # exist.
# TODO(brett): In Python 3.4, have import raise
# ModuleNotFound and catch that.
if getattr(exc, '_not_found', False):
if exc.name == from_name: if exc.name == from_name:
continue continue
raise raise
......
...@@ -317,7 +317,7 @@ def safeimport(path, forceload=0, cache={}): ...@@ -317,7 +317,7 @@ def safeimport(path, forceload=0, cache={}):
elif exc is SyntaxError: elif exc is SyntaxError:
# A SyntaxError occurred before we could execute the module. # A SyntaxError occurred before we could execute the module.
raise ErrorDuringImport(value.filename, info) raise ErrorDuringImport(value.filename, info)
elif exc is ImportError and value.name == path: elif issubclass(exc, ImportError) and value.name == path:
# No such module in the path. # No such module in the path.
return None return None
else: else:
......
...@@ -13,6 +13,7 @@ BaseException ...@@ -13,6 +13,7 @@ BaseException
+-- BufferError +-- BufferError
+-- EOFError +-- EOFError
+-- ImportError +-- ImportError
+-- ModuleNotFoundError
+-- LookupError +-- LookupError
| +-- IndexError | +-- IndexError
| +-- KeyError | +-- KeyError
......
...@@ -953,8 +953,5 @@ class ImportErrorTests(unittest.TestCase): ...@@ -953,8 +953,5 @@ class ImportErrorTests(unittest.TestCase):
self.assertEqual(str(arg), str(exc)) self.assertEqual(str(arg), str(exc))
def test_main():
run_unittest(ExceptionTests, ImportErrorTests)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -68,7 +68,15 @@ class ImportTests(unittest.TestCase): ...@@ -68,7 +68,15 @@ class ImportTests(unittest.TestCase):
def tearDown(self): def tearDown(self):
unload(TESTFN) unload(TESTFN)
setUp = tearDown def test_import_raises_ModuleNotFoundError(self):
with self.assertRaises(ModuleNotFoundError):
import something_that_should_not_exist_anywhere
def test_from_import_raises_ModuleNotFoundError(self):
with self.assertRaises(ModuleNotFoundError):
from something_that_should_not_exist_anywhere import blah
with self.assertRaises(ModuleNotFoundError):
from importlib import something_that_should_not_exist_anywhere
def test_case_sensitivity(self): def test_case_sensitivity(self):
# Brief digression to test that import is case-sensitive: if we got # Brief digression to test that import is case-sensitive: if we got
...@@ -487,7 +495,7 @@ func_filename = func.__code__.co_filename ...@@ -487,7 +495,7 @@ func_filename = func.__code__.co_filename
header = f.read(12) header = f.read(12)
code = marshal.load(f) code = marshal.load(f)
constants = list(code.co_consts) constants = list(code.co_consts)
foreign_code = test_main.__code__ foreign_code = importlib.import_module.__code__
pos = constants.index(1) pos = constants.index(1)
constants[pos] = foreign_code constants[pos] = foreign_code
code = type(code)(code.co_argcount, code.co_kwonlyargcount, code = type(code)(code.co_argcount, code.co_kwonlyargcount,
...@@ -1014,16 +1022,5 @@ class ImportTracebackTests(unittest.TestCase): ...@@ -1014,16 +1022,5 @@ class ImportTracebackTests(unittest.TestCase):
importlib.SourceLoader.load_module = old_load_module importlib.SourceLoader.load_module = old_load_module
def test_main(verbose=None):
run_unittest(ImportTests, PycacheTests, FilePermissionTests,
PycRewritingTests, PathsTests, RelativeImportTests,
OverridingImportBuiltinTests,
ImportlibBootstrapTests,
TestSymbolicallyLinkedPackage,
ImportTracebackTests)
if __name__ == '__main__': if __name__ == '__main__':
# Test needs to be a package, so we can do relative imports. unittest.main()
from test.test_import import test_main
test_main()
...@@ -22,6 +22,10 @@ class APITest(unittest.TestCase): ...@@ -22,6 +22,10 @@ class APITest(unittest.TestCase):
"""Test API-specific details for __import__ (e.g. raising the right """Test API-specific details for __import__ (e.g. raising the right
exception when passing in an int for the module name).""" exception when passing in an int for the module name)."""
def test_raises_ModuleNotFoundError(self):
with self.assertRaises(ModuleNotFoundError):
util.import_('some module that does not exist')
def test_name_requires_rparition(self): def test_name_requires_rparition(self):
# Raise TypeError if a non-string is passed in for the module name. # Raise TypeError if a non-string is passed in for the module name.
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
......
...@@ -69,16 +69,16 @@ class HandlingFromlist(unittest.TestCase): ...@@ -69,16 +69,16 @@ class HandlingFromlist(unittest.TestCase):
self.assertTrue(hasattr(module, 'module')) self.assertTrue(hasattr(module, 'module'))
self.assertEqual(module.module.__name__, 'pkg.module') self.assertEqual(module.module.__name__, 'pkg.module')
def test_module_from_package_triggers_ImportError(self): def test_module_from_package_triggers_ModuleNotFoundError(self):
# If a submodule causes an ImportError because it tries to import # If a submodule causes an ModuleNotFoundError because it tries to import
# a module which doesn't exist, that should let the ImportError # a module which doesn't exist, that should let the ModuleNotFoundError
# propagate. # propagate.
def module_code(): def module_code():
import i_do_not_exist import i_do_not_exist
with util.mock_modules('pkg.__init__', 'pkg.mod', with util.mock_modules('pkg.__init__', 'pkg.mod',
module_code={'pkg.mod': module_code}) as importer: module_code={'pkg.mod': module_code}) as importer:
with util.import_state(meta_path=[importer]): with util.import_state(meta_path=[importer]):
with self.assertRaises(ImportError) as exc: with self.assertRaises(ModuleNotFoundError) as exc:
import_util.import_('pkg', fromlist=['mod']) import_util.import_('pkg', fromlist=['mod'])
self.assertEqual('i_do_not_exist', exc.exception.name) self.assertEqual('i_do_not_exist', exc.exception.name)
......
...@@ -206,7 +206,7 @@ expected_html_data_docstrings = tuple(s.replace(' ', ' ') ...@@ -206,7 +206,7 @@ expected_html_data_docstrings = tuple(s.replace(' ', ' ')
missing_pattern = "no Python documentation found for '%s'" missing_pattern = "no Python documentation found for '%s'"
# output pattern for module with bad imports # output pattern for module with bad imports
badimport_pattern = "problem in %s - ImportError: No module named %r" badimport_pattern = "problem in %s - ModuleNotFoundError: No module named %r"
def run_pydoc(module_name, *args, **env): def run_pydoc(module_name, *args, **env):
""" """
......
...@@ -131,7 +131,7 @@ class HelperFunctionsTests(unittest.TestCase): ...@@ -131,7 +131,7 @@ class HelperFunctionsTests(unittest.TestCase):
re.escape(os.path.join(pth_dir, pth_fn))) re.escape(os.path.join(pth_dir, pth_fn)))
# XXX: ditto previous XXX comment. # XXX: ditto previous XXX comment.
self.assertRegex(err_out.getvalue(), 'Traceback') self.assertRegex(err_out.getvalue(), 'Traceback')
self.assertRegex(err_out.getvalue(), 'ImportError') self.assertRegex(err_out.getvalue(), 'ModuleNotFoundError')
@unittest.skipIf(sys.platform == "win32", "Windows does not raise an " @unittest.skipIf(sys.platform == "win32", "Windows does not raise an "
"error for file paths containing null characters") "error for file paths containing null characters")
......
...@@ -10,6 +10,9 @@ What's New in Python 3.4.0 Alpha 1? ...@@ -10,6 +10,9 @@ What's New in Python 3.4.0 Alpha 1?
Core and Builtins Core and Builtins
----------------- -----------------
- Issue #15767: Introduce ModuleNotFoundError which is raised when a module
could not be found.
- Issue #18183: Fix various unicode operations on strings with large unicode - Issue #18183: Fix various unicode operations on strings with large unicode
codepoints. codepoints.
......
...@@ -709,6 +709,13 @@ ComplexExtendsException(PyExc_Exception, ImportError, ...@@ -709,6 +709,13 @@ ComplexExtendsException(PyExc_Exception, ImportError,
"Import can't find module, or can't find name in " "Import can't find module, or can't find name in "
"module."); "module.");
/*
* ModuleNotFoundError extends ImportError
*/
MiddlingExtendsException(PyExc_ImportError, ModuleNotFoundError, ImportError,
"Module not found.");
/* /*
* OSError extends Exception * OSError extends Exception
*/ */
...@@ -2395,6 +2402,7 @@ _PyExc_Init(PyObject *bltinmod) ...@@ -2395,6 +2402,7 @@ _PyExc_Init(PyObject *bltinmod)
PRE_INIT(SystemExit) PRE_INIT(SystemExit)
PRE_INIT(KeyboardInterrupt) PRE_INIT(KeyboardInterrupt)
PRE_INIT(ImportError) PRE_INIT(ImportError)
PRE_INIT(ModuleNotFoundError)
PRE_INIT(OSError) PRE_INIT(OSError)
PRE_INIT(EOFError) PRE_INIT(EOFError)
PRE_INIT(RuntimeError) PRE_INIT(RuntimeError)
...@@ -2465,6 +2473,7 @@ _PyExc_Init(PyObject *bltinmod) ...@@ -2465,6 +2473,7 @@ _PyExc_Init(PyObject *bltinmod)
POST_INIT(SystemExit) POST_INIT(SystemExit)
POST_INIT(KeyboardInterrupt) POST_INIT(KeyboardInterrupt)
POST_INIT(ImportError) POST_INIT(ImportError)
POST_INIT(ModuleNotFoundError)
POST_INIT(OSError) POST_INIT(OSError)
INIT_ALIAS(EnvironmentError, OSError) INIT_ALIAS(EnvironmentError, OSError)
INIT_ALIAS(IOError, OSError) INIT_ALIAS(IOError, OSError)
......
...@@ -4588,7 +4588,7 @@ import_from(PyObject *v, PyObject *name) ...@@ -4588,7 +4588,7 @@ import_from(PyObject *v, PyObject *name)
x = PyObject_GetAttr(v, name); x = PyObject_GetAttr(v, name);
if (x == NULL && PyErr_ExceptionMatches(PyExc_AttributeError)) { if (x == NULL && PyErr_ExceptionMatches(PyExc_AttributeError)) {
PyErr_Format(PyExc_ImportError, "cannot import name %S", name); PyErr_Format(PyExc_ModuleNotFoundError, "cannot import name %S", name);
} }
return x; return x;
} }
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment