Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cb764d9
bpo-29999: repr() of ImportError now contains attributes name and path.
serhiy-storchaka Apr 5, 2017
db41c7d
Use GitHub Actions version of Bedevere
arhadthedev Feb 12, 2023
1caeb91
Add the news entry back
arhadthedev Feb 13, 2023
7f29270
Remove an accidentally included untracked file
arhadthedev Feb 13, 2023
c80e056
first commit
ynir3 Jul 19, 2025
126be20
add test_ModuleNotFoundError_repr_with_failed_import
ynir3 Jul 19, 2025
4a97f5c
add test_ModuleNotFoundError_repr_with_failed_import
ynir3 Jul 19, 2025
852da8c
add blurb
ynir3 Jul 19, 2025
6ab7362
add noqa:F401
ynir3 Jul 19, 2025
12c2293
remove whitespace
ynir3 Jul 19, 2025
a53da37
fix trailing-whitespace
ynir3 Jul 19, 2025
5b6d626
use ComplexExtendsException
ynir3 Jul 19, 2025
951e0a1
return to non-macro style
ynir3 Jul 19, 2025
8fbfd18
use modern field initializers
ynir3 Jul 19, 2025
458bcd0
remove un-necessary casts
ynir3 Jul 19, 2025
fa2f884
use PyUnicodeWriter
ynir3 Jul 19, 2025
35a6a29
fix trailing whitespace
ynir3 Jul 19, 2025
6e416c6
reduce nesting
ynir3 Jul 19, 2025
aa75f6a
use PyUnicdodeWriter_Format and if statement braces
ynir3 Jul 19, 2025
8b5e4ed
indentation
ynir3 Jul 19, 2025
28545e5
move decref
ynir3 Jul 19, 2025
4f63193
check baseException_repr failure, add 3.15rst
ynir3 Jul 20, 2025
62a479b
Merge branch 'main' into importerror-repr
Yoav11 Jul 20, 2025
25f3d42
fix chr c input
ynir3 Jul 20, 2025
a45ed75
move tests, edit rst wording
ynir3 Jul 20, 2025
69e7305
Merge branch 'main' into importerror-repr
Yoav11 Jul 20, 2025
9c09cd0
edit blurb
ynir3 Jul 20, 2025
ca0e2a4
edit blurb
ynir3 Jul 20, 2025
b0d7f4a
Merge branch 'main' into importerror-repr
Yoav11 Jul 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ Other language changes
* Several error messages incorrectly using the term "argument" have been corrected.
(Contributed by Stan Ulbrych in :gh:`133382`.)

* 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
===========
Expand Down
44 changes: 44 additions & 0 deletions Lib/test/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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
79 changes: 73 additions & 6 deletions Objects/exceptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -1864,6 +1864,59 @@ ImportError_reduce(PyObject *self, PyObject *Py_UNUSED(ignored))
return res;
}

static PyObject *
ImportError_repr(PyObject *self)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there other exceptions that will benefit from having keywords in their reprs? AFAICT, the keywords are only rendered if they were passed to the constructor as keyword arguments. In particular, we could make a generic repr helper for that which takes into account them, something like:

static PyObject *
repr_with_keywords(PyObject *exc, const char * const *kwlist)
{
	/* format using kwlist[i]=getattr(exc, kwlist[i]) 
     * with kwlist NULL-terminated */
}

Its usage would be

static char *kwlist[] = {"name", "path", NULL};
repr_with_keywords(exc, kwlist)

Copy link
Member

@picnixz picnixz Jul 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, let's hold off this idea as I don't know if it can be useful. But this can be worth investigating instead of storing the given keywords explicitly.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes - AFAIK only ImportError and ModuleNotFoundError (which hinnerits this functionality as per the tests) have optional arguments ? but makes sense to me that if other exception types in the future have optionals then we could extract this out.

Copy link
Member

@picnixz picnixz Jul 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but let's do it in a follow-up PR so that we can revert the helper commit more easily. Also it depends on whether we would store the keywords passed to the constructor (this is #11580)

{
int hasargs = PyTuple_GET_SIZE(((PyBaseExceptionObject *)self)->args) != 0;
PyImportErrorObject *exc = PyImportErrorObject_CAST(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_XDECREF(r);
goto error;
}
Py_XDECREF(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")},
Expand All @@ -1881,12 +1934,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
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy