]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-74185: repr() of ImportError now contains attributes name and path (#136770)
authorYoav Nir <11yoav.nir@gmail.com>
Thu, 14 Aug 2025 13:14:00 +0000 (14:14 +0100)
committerGitHub <noreply@github.com>
Thu, 14 Aug 2025 13:14:00 +0000 (15:14 +0200)
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Co-authored-by: Oleg Iarygin <oleg@arhadthedev.net>
Co-authored-by: ynir3 <ynir3@bloomberg.net>
Doc/whatsnew/3.15.rst
Lib/test/test_exceptions.py
Misc/NEWS.d/next/Core_and_Builtins/2025-07-19-10-35-31.gh-issue-74185.7hPCA5.rst [new file with mode: 0644]
Objects/exceptions.c

index 9f01b52f1aff3b4a04e32649d117b94d3a391a55..6c5ab1bb1a1078bc2a30464831b21096f8129ce1 100644 (file)
@@ -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
 ===========
index 57d0656487d4db642d6879daa4db4ea5d07fa140..59f77f91d85e5c4adfda7be1140ad4d5d2be513e 100644 (file)
@@ -2079,6 +2079,50 @@ class ImportErrorTests(unittest.TestCase):
                 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 (file)
index 0000000..d149e7b
--- /dev/null
@@ -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
index b17cac83551670792128e382825ff9191155afa6..531ee48eaf8a2452f40ebc804fcc65929f4e485f 100644 (file)
@@ -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