From 61e818409567ce452af60605937cdedf582f6293 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Thu, 21 Dec 2023 13:24:10 -0800 Subject: [PATCH] gh-95754: Better AttributeError on partially initialised module (#112577) Co-authored-by: Serhiy Storchaka --- Lib/test/test_import/__init__.py | 8 +++++++ .../data/circular_imports/import_cycle.py | 3 +++ ...3-12-01-08-16-10.gh-issue-95754.ae4gwy.rst | 1 + Objects/moduleobject.c | 24 +++++++++++++++++-- 4 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 Lib/test/test_import/data/circular_imports/import_cycle.py create mode 100644 Misc/NEWS.d/next/Core and Builtins/2023-12-01-08-16-10.gh-issue-95754.ae4gwy.rst diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py index 48c0a43f29e..7b0126226c4 100644 --- a/Lib/test/test_import/__init__.py +++ b/Lib/test/test_import/__init__.py @@ -1632,6 +1632,14 @@ class CircularImportTests(unittest.TestCase): str(cm.exception), ) + def test_circular_import(self): + with self.assertRaisesRegex( + AttributeError, + r"partially initialized module 'test.test_import.data.circular_imports.import_cycle' " + r"from '.*' has no attribute 'some_attribute' \(most likely due to a circular import\)" + ): + import test.test_import.data.circular_imports.import_cycle + def test_absolute_circular_submodule(self): with self.assertRaises(AttributeError) as cm: import test.test_import.data.circular_imports.subpkg2.parent diff --git a/Lib/test/test_import/data/circular_imports/import_cycle.py b/Lib/test/test_import/data/circular_imports/import_cycle.py new file mode 100644 index 00000000000..cd9507b5f69 --- /dev/null +++ b/Lib/test/test_import/data/circular_imports/import_cycle.py @@ -0,0 +1,3 @@ +import test.test_import.data.circular_imports.import_cycle as m + +m.some_attribute diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-12-01-08-16-10.gh-issue-95754.ae4gwy.rst b/Misc/NEWS.d/next/Core and Builtins/2023-12-01-08-16-10.gh-issue-95754.ae4gwy.rst new file mode 100644 index 00000000000..0884bc4a4be --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2023-12-01-08-16-10.gh-issue-95754.ae4gwy.rst @@ -0,0 +1 @@ +Provide a better error message when accessing invalid attributes on partially initialized modules. The origin of the module being accessed is now included in the message to help with the common issue of shadowing other modules. diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index e2741fef6de..3a1c516658d 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -788,7 +788,7 @@ PyObject* _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) { // When suppress=1, this function suppresses AttributeError. - PyObject *attr, *mod_name, *getattr; + PyObject *attr, *mod_name, *getattr, *origin; attr = _PyObject_GenericGetAttrWithDict((PyObject *)m, name, NULL, suppress); if (attr) { return attr; @@ -831,11 +831,31 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) if (suppress != 1) { int rc = _PyModuleSpec_IsInitializing(spec); if (rc > 0) { - PyErr_Format(PyExc_AttributeError, + int valid_spec = PyObject_GetOptionalAttr(spec, &_Py_ID(origin), &origin); + if (valid_spec == -1) { + Py_XDECREF(spec); + Py_DECREF(mod_name); + return NULL; + } + if (valid_spec == 1 && !PyUnicode_Check(origin)) { + valid_spec = 0; + Py_DECREF(origin); + } + if (valid_spec == 1) { + PyErr_Format(PyExc_AttributeError, + "partially initialized " + "module '%U' from '%U' has no attribute '%U' " + "(most likely due to a circular import)", + mod_name, origin, name); + Py_DECREF(origin); + } + else { + PyErr_Format(PyExc_AttributeError, "partially initialized " "module '%U' has no attribute '%U' " "(most likely due to a circular import)", mod_name, name); + } } else if (rc == 0) { rc = _PyModuleSpec_IsUninitializedSubmodule(spec, name);