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:
+-----------------------------------------+---------------------------------+----------+
| :c:data:`PyExc_ImportError` | :exc:`ImportError` | |
+-----------------------------------------+---------------------------------+----------+
| :c:data:`PyExc_ModuleNotFoundError` | :exc:`ModuleNotFoundError` | |
+-----------------------------------------+---------------------------------+----------+
| :c:data:`PyExc_IndexError` | :exc:`IndexError` | |
+-----------------------------------------+---------------------------------+----------+
| :c:data:`PyExc_InterruptedError` | :exc:`InterruptedError` | |
......
......@@ -169,8 +169,8 @@ The following exceptions are the exceptions that are usually raised.
.. exception:: ImportError
Raised when an :keyword:`import` statement fails to find the module definition
or when a ``from ... import`` fails to find a name that is to be imported.
Raised when the :keyword:`import` statement has troubles trying to load a
module.
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
......@@ -180,6 +180,15 @@ The following exceptions are the exceptions that are usually raised.
.. versionchanged:: 3.3
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
......
......@@ -137,6 +137,9 @@ Some smaller changes made to the core Python language are:
* 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
......
......@@ -152,6 +152,7 @@ PyAPI_DATA(PyObject *) PyExc_EOFError;
PyAPI_DATA(PyObject *) PyExc_FloatingPointError;
PyAPI_DATA(PyObject *) PyExc_OSError;
PyAPI_DATA(PyObject *) PyExc_ImportError;
PyAPI_DATA(PyObject *) PyExc_ModuleNotFoundError;
PyAPI_DATA(PyObject *) PyExc_IndexError;
PyAPI_DATA(PyObject *) PyExc_KeyError;
PyAPI_DATA(PyObject *) PyExc_KeyboardInterrupt;
......
......@@ -1553,11 +1553,7 @@ def _find_and_load_unlocked(name, import_):
raise ImportError(msg, name=name)
loader = _find_module(name, path)
if loader is None:
exc = ImportError(_ERR_MSG.format(name), name=name)
# TODO(brett): switch to a proper ModuleNotFound exception in Python
# 3.4.
exc._not_found = True
raise exc
raise ModuleNotFoundError(_ERR_MSG.format(name), name=name)
elif name not in sys.modules:
# The parent import may have already imported this module.
loader.load_module(name)
......@@ -1643,15 +1639,12 @@ def _handle_fromlist(module, fromlist, import_):
from_name = '{}.{}'.format(module.__name__, x)
try:
_call_with_frames_removed(import_, from_name)
except ImportError as exc:
except ModuleNotFoundError as exc:
# Backwards-compatibility dictates we ignore failed
# imports triggered by fromlist for modules that don't
# 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:
continue
if exc.name == from_name:
continue
raise
return module
......
......@@ -317,7 +317,7 @@ def safeimport(path, forceload=0, cache={}):
elif exc is SyntaxError:
# A SyntaxError occurred before we could execute the module.
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.
return None
else:
......
......@@ -13,6 +13,7 @@ BaseException
+-- BufferError
+-- EOFError
+-- ImportError
+-- ModuleNotFoundError
+-- LookupError
| +-- IndexError
| +-- KeyError
......
......@@ -953,8 +953,5 @@ class ImportErrorTests(unittest.TestCase):
self.assertEqual(str(arg), str(exc))
def test_main():
run_unittest(ExceptionTests, ImportErrorTests)
if __name__ == '__main__':
unittest.main()
......@@ -68,7 +68,15 @@ class ImportTests(unittest.TestCase):
def tearDown(self):
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):
# Brief digression to test that import is case-sensitive: if we got
......@@ -487,7 +495,7 @@ func_filename = func.__code__.co_filename
header = f.read(12)
code = marshal.load(f)
constants = list(code.co_consts)
foreign_code = test_main.__code__
foreign_code = importlib.import_module.__code__
pos = constants.index(1)
constants[pos] = foreign_code
code = type(code)(code.co_argcount, code.co_kwonlyargcount,
......@@ -1014,16 +1022,5 @@ class ImportTracebackTests(unittest.TestCase):
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__':
# Test needs to be a package, so we can do relative imports.
from test.test_import import test_main
test_main()
unittest.main()
......@@ -22,6 +22,10 @@ class APITest(unittest.TestCase):
"""Test API-specific details for __import__ (e.g. raising the right
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):
# Raise TypeError if a non-string is passed in for the module name.
with self.assertRaises(TypeError):
......
......@@ -69,16 +69,16 @@ class HandlingFromlist(unittest.TestCase):
self.assertTrue(hasattr(module, 'module'))
self.assertEqual(module.module.__name__, 'pkg.module')
def test_module_from_package_triggers_ImportError(self):
# If a submodule causes an ImportError because it tries to import
# a module which doesn't exist, that should let the ImportError
def test_module_from_package_triggers_ModuleNotFoundError(self):
# If a submodule causes an ModuleNotFoundError because it tries to import
# a module which doesn't exist, that should let the ModuleNotFoundError
# propagate.
def module_code():
import i_do_not_exist
with util.mock_modules('pkg.__init__', 'pkg.mod',
module_code={'pkg.mod': module_code}) as 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'])
self.assertEqual('i_do_not_exist', exc.exception.name)
......
......@@ -206,7 +206,7 @@ expected_html_data_docstrings = tuple(s.replace(' ', ' ')
missing_pattern = "no Python documentation found for '%s'"
# 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):
"""
......
......@@ -131,7 +131,7 @@ class HelperFunctionsTests(unittest.TestCase):
re.escape(os.path.join(pth_dir, pth_fn)))
# XXX: ditto previous XXX comment.
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 "
"error for file paths containing null characters")
......
......@@ -10,6 +10,9 @@ What's New in Python 3.4.0 Alpha 1?
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
codepoints.
......
......@@ -709,6 +709,13 @@ ComplexExtendsException(PyExc_Exception, ImportError,
"Import can't find module, or can't find name in "
"module.");
/*
* ModuleNotFoundError extends ImportError
*/
MiddlingExtendsException(PyExc_ImportError, ModuleNotFoundError, ImportError,
"Module not found.");
/*
* OSError extends Exception
*/
......@@ -2395,6 +2402,7 @@ _PyExc_Init(PyObject *bltinmod)
PRE_INIT(SystemExit)
PRE_INIT(KeyboardInterrupt)
PRE_INIT(ImportError)
PRE_INIT(ModuleNotFoundError)
PRE_INIT(OSError)
PRE_INIT(EOFError)
PRE_INIT(RuntimeError)
......@@ -2465,6 +2473,7 @@ _PyExc_Init(PyObject *bltinmod)
POST_INIT(SystemExit)
POST_INIT(KeyboardInterrupt)
POST_INIT(ImportError)
POST_INIT(ModuleNotFoundError)
POST_INIT(OSError)
INIT_ALIAS(EnvironmentError, OSError)
INIT_ALIAS(IOError, OSError)
......
......@@ -4588,7 +4588,7 @@ import_from(PyObject *v, PyObject *name)
x = PyObject_GetAttr(v, name);
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;
}
......
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