https://github.com/python/cpython/commit/c87b66bc7c21fca637a3f5c6fd3aa14316a05bc6
commit: c87b66bc7c21fca637a3f5c6fd3aa14316a05bc6
branch: main
author: Yoav Nir <[email protected]>
committer: vstinner <[email protected]>
date: 2025-08-14T15:14:00+02:00
summary:

gh-74185: repr() of ImportError now contains attributes name and path (#136770)

Co-authored-by: Serhiy Storchaka <[email protected]>
Co-authored-by: Oleg Iarygin <[email protected]>
Co-authored-by: ynir3 <[email protected]>

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2025-07-19-10-35-31.gh-issue-74185.7hPCA5.rst
M Doc/whatsnew/3.15.rst
M Lib/test/test_exceptions.py
M Objects/exceptions.c

diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 9f01b52f1aff3b..6c5ab1bb1a1078 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -204,6 +204,10 @@ Other language changes
   controlled by :ref:`environment variables <using-on-controlling-color>`.
   (Contributed by Peter Bierma in :gh:`134170`.)
 
+* The :meth:`~object.__repr__` of :class:`ImportError` and 
:class:`ModuleNotFoundError`
+  now shows "name" and "path" as ``name=<name>`` and ``path=<path>`` if they 
were given
+  as keyword arguments at construction time.
+  (Contributed by Serhiy Storchaka, Oleg Iarygin, and Yoav Nir in :gh:`74185`.)
 
 New modules
 ===========
diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py
index 57d0656487d4db..59f77f91d85e5c 100644
--- a/Lib/test/test_exceptions.py
+++ b/Lib/test/test_exceptions.py
@@ -2079,6 +2079,50 @@ def test_copy_pickle(self):
                 self.assertEqual(exc.name, orig.name)
                 self.assertEqual(exc.path, orig.path)
 
+    def test_repr(self):
+        exc = ImportError()
+        self.assertEqual(repr(exc), "ImportError()")
+
+        exc = ImportError('test')
+        self.assertEqual(repr(exc), "ImportError('test')")
+
+        exc = ImportError('test', 'case')
+        self.assertEqual(repr(exc), "ImportError('test', 'case')")
+
+        exc = ImportError(name='somemodule')
+        self.assertEqual(repr(exc), "ImportError(name='somemodule')")
+
+        exc = ImportError('test', name='somemodule')
+        self.assertEqual(repr(exc), "ImportError('test', name='somemodule')")
+
+        exc = ImportError(path='somepath')
+        self.assertEqual(repr(exc), "ImportError(path='somepath')")
+
+        exc = ImportError('test', path='somepath')
+        self.assertEqual(repr(exc), "ImportError('test', path='somepath')")
+
+        exc = ImportError(name='somename', path='somepath')
+        self.assertEqual(repr(exc),
+                "ImportError(name='somename', path='somepath')")
+
+        exc = ImportError('test', name='somename', path='somepath')
+        self.assertEqual(repr(exc),
+                "ImportError('test', name='somename', path='somepath')")
+
+        exc = ModuleNotFoundError('test', name='somename', path='somepath')
+        self.assertEqual(repr(exc),
+                "ModuleNotFoundError('test', name='somename', 
path='somepath')")
+
+    def test_ModuleNotFoundError_repr_with_failed_import(self):
+        with self.assertRaises(ModuleNotFoundError) as cm:
+            import does_not_exist  # type: ignore[import] # noqa: F401
+
+        self.assertEqual(cm.exception.name, "does_not_exist")
+        self.assertIsNone(cm.exception.path)
+
+        self.assertEqual(repr(cm.exception),
+            "ModuleNotFoundError(\"No module named 'does_not_exist'\", 
name='does_not_exist')")
+
 
 def run_script(source):
     if isinstance(source, str):
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2025-07-19-10-35-31.gh-issue-74185.7hPCA5.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2025-07-19-10-35-31.gh-issue-74185.7hPCA5.rst
new file mode 100644
index 00000000000000..d149e7b2878574
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2025-07-19-10-35-31.gh-issue-74185.7hPCA5.rst
@@ -0,0 +1,4 @@
+The :meth:`~object.__repr__` of :class:`ImportError` and 
:class:`ModuleNotFoundError`
+now shows "name" and "path" as ``name=<name>`` and ``path=<path>`` if they 
were given
+as keyword arguments at construction time.
+Patch by Serhiy Storchaka, Oleg Iarygin, and Yoav Nir
diff --git a/Objects/exceptions.c b/Objects/exceptions.c
index b17cac83551670..531ee48eaf8a24 100644
--- a/Objects/exceptions.c
+++ b/Objects/exceptions.c
@@ -1864,6 +1864,62 @@ ImportError_reduce(PyObject *self, PyObject 
*Py_UNUSED(ignored))
     return res;
 }
 
+static PyObject *
+ImportError_repr(PyObject *self)
+{
+    int hasargs = PyTuple_GET_SIZE(((PyBaseExceptionObject *)self)->args) != 0;
+    PyImportErrorObject *exc = PyImportErrorObject_CAST(self);
+    if (exc->name == NULL && exc->path == NULL) {
+        return BaseException_repr(self);
+    }
+    PyUnicodeWriter *writer = PyUnicodeWriter_Create(0);
+    if (writer == NULL) {
+        goto error;
+    }
+    PyObject *r = BaseException_repr(self);
+    if (r == NULL) {
+        goto error;
+    }
+    if (PyUnicodeWriter_WriteSubstring(
+        writer, r, 0, PyUnicode_GET_LENGTH(r) - 1) < 0)
+    {
+        Py_DECREF(r);
+        goto error;
+    }
+    Py_DECREF(r);
+    if (exc->name) {
+        if (hasargs) {
+            if (PyUnicodeWriter_WriteASCII(writer, ", ", 2) < 0) {
+                goto error;
+            }
+        }
+        if (PyUnicodeWriter_Format(writer, "name=%R", exc->name) < 0) {
+            goto error;
+        }
+        hasargs = 1;
+    }
+    if (exc->path) {
+        if (hasargs) {
+            if (PyUnicodeWriter_WriteASCII(writer, ", ", 2) < 0) {
+                goto error;
+            }
+        }
+        if (PyUnicodeWriter_Format(writer, "path=%R", exc->path) < 0) {
+            goto error;
+        }
+    }
+
+    if (PyUnicodeWriter_WriteChar(writer, ')') < 0) {
+        goto error;
+    }
+
+    return PyUnicodeWriter_Finish(writer);
+
+error:
+    PyUnicodeWriter_Discard(writer);
+    return NULL;
+}
+
 static PyMemberDef ImportError_members[] = {
     {"msg", _Py_T_OBJECT, offsetof(PyImportErrorObject, msg), 0,
         PyDoc_STR("exception message")},
@@ -1881,12 +1937,26 @@ 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.");
+static PyTypeObject _PyExc_ImportError = {
+    PyVarObject_HEAD_INIT(NULL, 0)
+    .tp_name = "ImportError",
+    .tp_basicsize = sizeof(PyImportErrorObject),
+    .tp_dealloc = ImportError_dealloc,
+    .tp_repr = ImportError_repr,
+    .tp_str = ImportError_str,
+    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
+    .tp_doc = PyDoc_STR(
+        "Import can't find module, "
+        "or can't find name in module."),
+    .tp_traverse = ImportError_traverse,
+    .tp_clear = ImportError_clear,
+    .tp_methods = ImportError_methods,
+    .tp_members = ImportError_members,
+    .tp_base = &_PyExc_Exception,
+    .tp_dictoffset = offsetof(PyImportErrorObject, dict),
+    .tp_init = ImportError_init,
+};
+PyObject *PyExc_ImportError = (PyObject *)&_PyExc_ImportError;
 
 /*
  *    ModuleNotFoundError extends ImportError

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to