Skip to content

Commit 61e8184

Browse files
gh-95754: Better AttributeError on partially initialised module (#112577)
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent 2d91409 commit 61e8184

File tree

4 files changed

+34
-2
lines changed

4 files changed

+34
-2
lines changed

Diff for: Lib/test/test_import/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -1632,6 +1632,14 @@ def test_circular_from_import(self):
16321632
str(cm.exception),
16331633
)
16341634

1635+
def test_circular_import(self):
1636+
with self.assertRaisesRegex(
1637+
AttributeError,
1638+
r"partially initialized module 'test.test_import.data.circular_imports.import_cycle' "
1639+
r"from '.*' has no attribute 'some_attribute' \(most likely due to a circular import\)"
1640+
):
1641+
import test.test_import.data.circular_imports.import_cycle
1642+
16351643
def test_absolute_circular_submodule(self):
16361644
with self.assertRaises(AttributeError) as cm:
16371645
import test.test_import.data.circular_imports.subpkg2.parent
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import test.test_import.data.circular_imports.import_cycle as m
2+
3+
m.some_attribute
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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 for: Objects/moduleobject.c

+22-2
Original file line numberDiff line numberDiff line change
@@ -788,7 +788,7 @@ PyObject*
788788
_Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
789789
{
790790
// When suppress=1, this function suppresses AttributeError.
791-
PyObject *attr, *mod_name, *getattr;
791+
PyObject *attr, *mod_name, *getattr, *origin;
792792
attr = _PyObject_GenericGetAttrWithDict((PyObject *)m, name, NULL, suppress);
793793
if (attr) {
794794
return attr;
@@ -831,11 +831,31 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
831831
if (suppress != 1) {
832832
int rc = _PyModuleSpec_IsInitializing(spec);
833833
if (rc > 0) {
834-
PyErr_Format(PyExc_AttributeError,
834+
int valid_spec = PyObject_GetOptionalAttr(spec, &_Py_ID(origin), &origin);
835+
if (valid_spec == -1) {
836+
Py_XDECREF(spec);
837+
Py_DECREF(mod_name);
838+
return NULL;
839+
}
840+
if (valid_spec == 1 && !PyUnicode_Check(origin)) {
841+
valid_spec = 0;
842+
Py_DECREF(origin);
843+
}
844+
if (valid_spec == 1) {
845+
PyErr_Format(PyExc_AttributeError,
846+
"partially initialized "
847+
"module '%U' from '%U' has no attribute '%U' "
848+
"(most likely due to a circular import)",
849+
mod_name, origin, name);
850+
Py_DECREF(origin);
851+
}
852+
else {
853+
PyErr_Format(PyExc_AttributeError,
835854
"partially initialized "
836855
"module '%U' has no attribute '%U' "
837856
"(most likely due to a circular import)",
838857
mod_name, name);
858+
}
839859
}
840860
else if (rc == 0) {
841861
rc = _PyModuleSpec_IsUninitializedSubmodule(spec, name);

0 commit comments

Comments
 (0)