Skip to content

Commit 3e429dc

Browse files
bpo-33237: Improve AttributeError message for partially initialized module. (GH-6398)
1 parent 95b6acf commit 3e429dc

File tree

7 files changed

+67
-23
lines changed

7 files changed

+67
-23
lines changed

Include/moduleobject.h

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ PyAPI_FUNC(PyObject *) PyModule_GetFilenameObject(PyObject *);
3030
#ifndef Py_LIMITED_API
3131
PyAPI_FUNC(void) _PyModule_Clear(PyObject *);
3232
PyAPI_FUNC(void) _PyModule_ClearDict(PyObject *);
33+
PyAPI_FUNC(int) _PyModuleSpec_IsInitializing(PyObject *);
3334
#endif
3435
PyAPI_FUNC(struct PyModuleDef*) PyModule_GetDef(PyObject*);
3536
PyAPI_FUNC(void*) PyModule_GetState(PyObject*);

Lib/test/test_import/__init__.py

+13
Original file line numberDiff line numberDiff line change
@@ -1271,6 +1271,19 @@ def test_binding(self):
12711271
except ImportError:
12721272
self.fail('circular import with binding a submodule to a name failed')
12731273

1274+
def test_crossreference1(self):
1275+
import test.test_import.data.circular_imports.use
1276+
import test.test_import.data.circular_imports.source
1277+
1278+
def test_crossreference2(self):
1279+
with self.assertRaises(AttributeError) as cm:
1280+
import test.test_import.data.circular_imports.source
1281+
errmsg = str(cm.exception)
1282+
self.assertIn('test.test_import.data.circular_imports.source', errmsg)
1283+
self.assertIn('spam', errmsg)
1284+
self.assertIn('partially initialized module', errmsg)
1285+
self.assertIn('circular import', errmsg)
1286+
12741287

12751288
if __name__ == '__main__':
12761289
# Test needs to be a package, so we can do relative imports.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import use
2+
spam = 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import source
2+
source.spam
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improved :exc:`AttributeError` message for partially initialized module.

Objects/moduleobject.c

+39-2
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,27 @@ module_repr(PyModuleObject *m)
698698
return PyObject_CallMethod(interp->importlib, "_module_repr", "O", m);
699699
}
700700

701+
/* Check if the "_initializing" attribute of the module spec is set to true.
702+
Clear the exception and return 0 if spec is NULL.
703+
*/
704+
int
705+
_PyModuleSpec_IsInitializing(PyObject *spec)
706+
{
707+
if (spec != NULL) {
708+
_Py_IDENTIFIER(_initializing);
709+
PyObject *value = _PyObject_GetAttrId(spec, &PyId__initializing);
710+
if (value != NULL) {
711+
int initializing = PyObject_IsTrue(value);
712+
Py_DECREF(value);
713+
if (initializing >= 0) {
714+
return initializing;
715+
}
716+
}
717+
}
718+
PyErr_Clear();
719+
return 0;
720+
}
721+
701722
static PyObject*
702723
module_getattro(PyModuleObject *m, PyObject *name)
703724
{
@@ -717,8 +738,24 @@ module_getattro(PyModuleObject *m, PyObject *name)
717738
_Py_IDENTIFIER(__name__);
718739
mod_name = _PyDict_GetItemId(m->md_dict, &PyId___name__);
719740
if (mod_name && PyUnicode_Check(mod_name)) {
720-
PyErr_Format(PyExc_AttributeError,
721-
"module '%U' has no attribute '%U'", mod_name, name);
741+
_Py_IDENTIFIER(__spec__);
742+
Py_INCREF(mod_name);
743+
PyObject *spec = _PyDict_GetItemId(m->md_dict, &PyId___spec__);
744+
Py_XINCREF(spec);
745+
if (_PyModuleSpec_IsInitializing(spec)) {
746+
PyErr_Format(PyExc_AttributeError,
747+
"partially initialized "
748+
"module '%U' has no attribute '%U' "
749+
"(most likely due to a circular import)",
750+
mod_name, name);
751+
}
752+
else {
753+
PyErr_Format(PyExc_AttributeError,
754+
"module '%U' has no attribute '%U'",
755+
mod_name, name);
756+
}
757+
Py_XDECREF(spec);
758+
Py_DECREF(mod_name);
722759
return NULL;
723760
}
724761
}

Python/import.c

+9-21
Original file line numberDiff line numberDiff line change
@@ -1721,38 +1721,26 @@ PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
17211721
mod = PyImport_GetModule(abs_name);
17221722
if (mod != NULL && mod != Py_None) {
17231723
_Py_IDENTIFIER(__spec__);
1724-
_Py_IDENTIFIER(_initializing);
17251724
_Py_IDENTIFIER(_lock_unlock_module);
1726-
PyObject *value = NULL;
17271725
PyObject *spec;
1728-
int initializing = 0;
17291726

17301727
/* Optimization: only call _bootstrap._lock_unlock_module() if
17311728
__spec__._initializing is true.
17321729
NOTE: because of this, initializing must be set *before*
17331730
stuffing the new module in sys.modules.
17341731
*/
17351732
spec = _PyObject_GetAttrId(mod, &PyId___spec__);
1736-
if (spec != NULL) {
1737-
value = _PyObject_GetAttrId(spec, &PyId__initializing);
1738-
Py_DECREF(spec);
1739-
}
1740-
if (value == NULL)
1741-
PyErr_Clear();
1742-
else {
1743-
initializing = PyObject_IsTrue(value);
1744-
Py_DECREF(value);
1745-
if (initializing == -1)
1746-
PyErr_Clear();
1747-
if (initializing > 0) {
1748-
value = _PyObject_CallMethodIdObjArgs(interp->importlib,
1749-
&PyId__lock_unlock_module, abs_name,
1750-
NULL);
1751-
if (value == NULL)
1752-
goto error;
1753-
Py_DECREF(value);
1733+
if (_PyModuleSpec_IsInitializing(spec)) {
1734+
PyObject *value = _PyObject_CallMethodIdObjArgs(interp->importlib,
1735+
&PyId__lock_unlock_module, abs_name,
1736+
NULL);
1737+
if (value == NULL) {
1738+
Py_DECREF(spec);
1739+
goto error;
17541740
}
1741+
Py_DECREF(value);
17551742
}
1743+
Py_XDECREF(spec);
17561744
}
17571745
else {
17581746
Py_XDECREF(mod);

0 commit comments

Comments
 (0)