summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/test/test_import/__init__.py8
-rw-r--r--Lib/test/test_import/data/circular_imports/import_cycle.py3
-rw-r--r--Misc/NEWS.d/next/Core and Builtins/2023-12-01-08-16-10.gh-issue-95754.ae4gwy.rst1
-rw-r--r--Objects/moduleobject.c24
4 files changed, 34 insertions, 2 deletions
diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py
index 48c0a43..7b01262 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 0000000..cd9507b
--- /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 0000000..0884bc4
--- /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 e2741fe..3a1c516 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);