Skip to content

Commit 4bb2407

Browse files
hauntsaninjaserhiy-storchaka
authored andcommitted
pythongh-95754: Better AttributeError on partially initialised module (python#112577)
Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 8fa3c93 commit 4bb2407

File tree

4 files changed

+34
-2
lines changed

4 files changed

+34
-2
lines changed

Lib/test/test_import/__init__.py

Lines changed: 8 additions & 0 deletions
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
Lines changed: 3 additions & 0 deletions
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
Lines changed: 1 addition & 0 deletions
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.

Objects/moduleobject.c

Lines changed: 22 additions & 2 deletions
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)