diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index 8c5a9602e43..33bc3b06dee 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -159,6 +159,14 @@ The following exceptions are the exceptions that are usually raised. 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. + 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 + that was attempted to be imported and the path to any file which triggered + the exception, respectively. + + .. versionchanged:: 3.3 + Added the :attr:`name` and :attr:`path` attributes. + .. exception:: IndexError diff --git a/Include/pyerrors.h b/Include/pyerrors.h index 1e42ebbb877..a5507059c59 100644 --- a/Include/pyerrors.h +++ b/Include/pyerrors.h @@ -231,6 +231,13 @@ PyAPI_FUNC(PyObject *) PyErr_Format( ... ); +typedef struct { + PyException_HEAD + PyObject *msg; + PyObject *name; + PyObject *path; +} PyImportErrorObject; + #ifdef MS_WINDOWS PyAPI_FUNC(PyObject *) PyErr_SetFromWindowsErrWithFilename( int ierr, @@ -256,6 +263,12 @@ PyAPI_FUNC(PyObject *) PyErr_SetExcFromWindowsErrWithUnicodeFilename( PyAPI_FUNC(PyObject *) PyErr_SetExcFromWindowsErr(PyObject *, int); #endif /* MS_WINDOWS */ +PyAPI_FUNC(PyObject *) PyErr_SetExcWithArgsKwargs(PyObject *, PyObject *, + PyObject *); +PyAPI_FUNC(PyObject *) PyErr_SetFromImportErrorWithNameAndPath(PyObject *, + PyObject *, PyObject *); +PyAPI_FUNC(PyObject *) PyErr_SetFromImportErrorWithName(PyObject *, PyObject *); + /* Export the old function so that the existing API remains available: */ PyAPI_FUNC(void) PyErr_BadInternalCall(void); PyAPI_FUNC(void) _PyErr_BadInternalCall(const char *filename, int lineno); diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 42536d3b7ca..39ff85fc190 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -902,8 +902,30 @@ class ExceptionTests(unittest.TestCase): self.assertEqual(cm.exception.errno, errno.ENOTDIR, cm.exception) +class ImportErrorTests(unittest.TestCase): + + def test_attributes(self): + # Setting 'name' and 'path' should not be a problem. + exc = ImportError('test') + self.assertIsNone(exc.name) + self.assertIsNone(exc.path) + + exc = ImportError('test', name='somemodule') + self.assertEqual(exc.name, 'somemodule') + self.assertIsNone(exc.path) + + exc = ImportError('test', path='somepath') + self.assertEqual(exc.path, 'somepath') + self.assertIsNone(exc.name) + + exc = ImportError('test', path='somepath', name='somename') + self.assertEqual(exc.name, 'somename') + self.assertEqual(exc.path, 'somepath') + + + def test_main(): - run_unittest(ExceptionTests) + run_unittest(ExceptionTests, ImportErrorTests) if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS b/Misc/NEWS index c899f12a9cc..3dab768e998 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -10,6 +10,10 @@ What's New in Python 3.3.0 Alpha 3? Core and Builtins ----------------- +- Issue #1559549: ImportError now has 'name' and 'path' attributes that are set + using keyword arguments to its constructor. They are currently not set by + import as they are meant for use by importlib. + - Issue #14474: Save and restore exception state in thread.start_new_thread() while writing error message if the thread leaves a unhandled exception. diff --git a/Objects/exceptions.c b/Objects/exceptions.c index bc4379982ea..f3dde1121aa 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -605,9 +605,104 @@ SimpleExtendsException(PyExc_BaseException, KeyboardInterrupt, /* * ImportError extends Exception */ -SimpleExtendsException(PyExc_Exception, ImportError, - "Import can't find module, or can't find name in module."); +static int +ImportError_init(PyImportErrorObject *self, PyObject *args, PyObject *kwds) +{ + PyObject *msg = NULL; + PyObject *name = NULL; + PyObject *path = NULL; + +/* Macro replacement doesn't allow ## to start the first line of a macro, + so we move the assignment and NULL check into the if-statement. */ +#define GET_KWD(kwd) { \ + kwd = PyDict_GetItemString(kwds, #kwd); \ + if (kwd) { \ + Py_CLEAR(self->kwd); \ + self->kwd = kwd; \ + Py_INCREF(self->kwd);\ + if (PyDict_DelItemString(kwds, #kwd)) \ + return -1; \ + } \ + } + + if (kwds) { + GET_KWD(name); + GET_KWD(path); + } + + if (BaseException_init((PyBaseExceptionObject *)self, args, kwds) == -1) + return -1; + if (PyTuple_GET_SIZE(args) != 1) + return 0; + if (!PyArg_UnpackTuple(args, "ImportError", 1, 1, &msg)) + return -1; + + Py_CLEAR(self->msg); /* replacing */ + self->msg = msg; + Py_INCREF(self->msg); + + return 0; +} + +static int +ImportError_clear(PyImportErrorObject *self) +{ + Py_CLEAR(self->msg); + Py_CLEAR(self->name); + Py_CLEAR(self->path); + return BaseException_clear((PyBaseExceptionObject *)self); +} + +static void +ImportError_dealloc(PyImportErrorObject *self) +{ + _PyObject_GC_UNTRACK(self); + ImportError_clear(self); + Py_TYPE(self)->tp_free((PyObject *)self); +} + +static int +ImportError_traverse(PyImportErrorObject *self, visitproc visit, void *arg) +{ + Py_VISIT(self->msg); + Py_VISIT(self->name); + Py_VISIT(self->path); + return BaseException_traverse((PyBaseExceptionObject *)self, visit, arg); +} + +static PyObject * +ImportError_str(PyImportErrorObject *self) +{ + if (self->msg) { + Py_INCREF(self->msg); + return self->msg; + } + else { + return BaseException_str((PyBaseExceptionObject *)self); + } +} + +static PyMemberDef ImportError_members[] = { + {"msg", T_OBJECT, offsetof(PyImportErrorObject, msg), 0, + PyDoc_STR("exception message")}, + {"name", T_OBJECT, offsetof(PyImportErrorObject, name), 0, + PyDoc_STR("module name")}, + {"path", T_OBJECT, offsetof(PyImportErrorObject, path), 0, + PyDoc_STR("module path")}, + {NULL} /* Sentinel */ +}; + +static PyMethodDef ImportError_methods[] = { + {NULL} +}; + +ComplexExtendsException(PyExc_Exception, ImportError, + ImportError, 0 /* new */, + ImportError_methods, ImportError_members, + 0 /* getset */, ImportError_str, + "Import can't find module, or can't find name in " + "module."); /* * OSError extends Exception diff --git a/Python/errors.c b/Python/errors.c index 31fa9e29556..345a345afe4 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -585,6 +585,53 @@ PyObject *PyErr_SetFromWindowsErrWithUnicodeFilename( } #endif /* MS_WINDOWS */ +PyObject * +PyErr_SetExcWithArgsKwargs(PyObject *exc, PyObject *args, PyObject *kwargs) +{ + PyObject *val; + + /* args must at least be an empty tuple */ + if (args == NULL) + args = PyTuple_New(0); + + val = PyObject_Call(exc, args, kwargs); + if (val != NULL) { + PyErr_SetObject((PyObject *) Py_TYPE(val), val); + Py_DECREF(val); + } + + return NULL; +} + +PyObject * +PyErr_SetFromImportErrorWithNameAndPath(PyObject *msg, + PyObject *name, PyObject *path) +{ + PyObject *args = PyTuple_New(1); + PyObject *kwargs = PyDict_New(); + PyObject *result; + + if (path == NULL) + path = Py_None; + + PyTuple_SetItem(args, 0, msg); + PyDict_SetItemString(kwargs, "name", name); + PyDict_SetItemString(kwargs, "path", path); + + result = PyErr_SetExcWithArgsKwargs(PyExc_ImportError, args, kwargs); + + Py_DECREF(args); + Py_DECREF(kwargs); + + return result; +} + +PyObject * +PyErr_SetFromImportErrorWithName(PyObject *msg, PyObject *name) +{ + return PyErr_SetFromImportErrorWithNameAndPath(msg, name, NULL); +} + void _PyErr_BadInternalCall(const char *filename, int lineno) {