From 4c8455b8f300c3dd8df048287b9a80bbb3b2e094 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 19 Jul 2026 22:38:41 +0200 Subject: [PATCH 1/6] Specialize lazy import exceptions --- Objects/lazyimportobject.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index fa1eb25047d9617..5c4b768e875ed72 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -81,6 +81,27 @@ lazy_import_dealloc(PyObject *op) Py_TYPE(op)->tp_free(op); } +static PyObject * +lazy_import_getattro(PyObject *op, PyObject *name) { + /* Suppress to override the error message. */ + PyObject *value = _PyObject_GenericGetAttrWithDict(op, name, NULL, /* suppress */1); + if (value == NULL) { + if (PyErr_Occurred()) { + return NULL; + } + PyObject *lz_name = _PyLazyImport_GetName(op); + if (lz_name == NULL) { + return NULL; + } + PyErr_Format(PyExc_AttributeError, + "lazy import '%U' has no attribute '%U'", + lz_name, name); + Py_DECREF(lz_name); + return NULL; + } + return value; +} + static PyObject * lazy_import_name(PyLazyImportObject *m) { @@ -149,6 +170,7 @@ PyTypeObject PyLazyImport_Type = { .tp_repr = lazy_import_repr, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, .tp_doc = lazy_import_doc, + .tp_getattro = lazy_import_getattro, .tp_traverse = lazy_import_traverse, .tp_clear = lazy_import_clear, .tp_methods = lazy_import_methods, From 0f43042aa92a7b1a1ebb36e0acbdf805397dd2cb Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 19 Jul 2026 23:27:58 +0200 Subject: [PATCH 2/6] Better wording, perchance. --- Objects/lazyimportobject.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index 5c4b768e875ed72..41d8dc925b218e4 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -94,8 +94,8 @@ lazy_import_getattro(PyObject *op, PyObject *name) { return NULL; } PyErr_Format(PyExc_AttributeError, - "lazy import '%U' has no attribute '%U'", - lz_name, name); + "cannot access attribute '%U' on unresolved lazy import '%U'", + name, lz_name); Py_DECREF(lz_name); return NULL; } From a88d715658ee31a9ca80b0b0fe9162e562eabf8e Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 25 Jul 2026 12:28:11 +0200 Subject: [PATCH 3/6] Move comment up, use `%R` in the error message --- Objects/lazyimportobject.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index 41d8dc925b218e4..c3573de25b93b9a 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -81,9 +81,9 @@ lazy_import_dealloc(PyObject *op) Py_TYPE(op)->tp_free(op); } +/* Specialize the error message for failed attribute lookups. */ static PyObject * lazy_import_getattro(PyObject *op, PyObject *name) { - /* Suppress to override the error message. */ PyObject *value = _PyObject_GenericGetAttrWithDict(op, name, NULL, /* suppress */1); if (value == NULL) { if (PyErr_Occurred()) { @@ -94,7 +94,7 @@ lazy_import_getattro(PyObject *op, PyObject *name) { return NULL; } PyErr_Format(PyExc_AttributeError, - "cannot access attribute '%U' on unresolved lazy import '%U'", + "cannot access attribute %R on unresolved lazy import %R", name, lz_name); Py_DECREF(lz_name); return NULL; From dd527a64039df1cd75fe504fcdff8a2ff19be95c Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 25 Jul 2026 12:44:28 +0200 Subject: [PATCH 4/6] Add news entry --- .../2026-07-25-12-43-42.gh-issue-154196.0rAdob.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-25-12-43-42.gh-issue-154196.0rAdob.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-25-12-43-42.gh-issue-154196.0rAdob.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-25-12-43-42.gh-issue-154196.0rAdob.rst new file mode 100644 index 000000000000000..8f55e76a5c0dcd4 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-25-12-43-42.gh-issue-154196.0rAdob.rst @@ -0,0 +1,2 @@ +Improve :exc:`AttributeError` messages from unresolved lazy imports. Patch +by Bartosz Sławecki. From 2ee42266bb50248abe351d8ac4ce07571ef6ce58 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 25 Jul 2026 13:07:58 +0200 Subject: [PATCH 5/6] Add test --- Lib/test/test_lazy_import/__init__.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 899ecec1ba2afa4..c90f9fd8847bd98 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -277,6 +277,22 @@ def test_lazy_import_type_attributes_accessible(self): proc = assert_python_ok("-c", code) self.assertIn(b" Date: Sat, 25 Jul 2026 13:29:29 +0200 Subject: [PATCH 6/6] Add a clarifying comment why `PyErr_Occurred()` check --- Objects/lazyimportobject.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Objects/lazyimportobject.c b/Objects/lazyimportobject.c index c3573de25b93b9a..9d645e194dae1da 100644 --- a/Objects/lazyimportobject.c +++ b/Objects/lazyimportobject.c @@ -87,6 +87,7 @@ lazy_import_getattro(PyObject *op, PyObject *name) { PyObject *value = _PyObject_GenericGetAttrWithDict(op, name, NULL, /* suppress */1); if (value == NULL) { if (PyErr_Occurred()) { + /* Bubble up earlier unrelated exception */ return NULL; } PyObject *lz_name = _PyLazyImport_GetName(op);