Skip to content

bpo-33237: Improve AttributeError message for partially initialized module. #6398

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Oct 30, 2018
1 change: 1 addition & 0 deletions Include/moduleobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ PyAPI_FUNC(PyObject *) PyModule_GetFilenameObject(PyObject *);
#ifndef Py_LIMITED_API
PyAPI_FUNC(void) _PyModule_Clear(PyObject *);
PyAPI_FUNC(void) _PyModule_ClearDict(PyObject *);
PyAPI_FUNC(int) _PyModuleSpec_IsInitializing(PyObject *);
#endif
PyAPI_FUNC(struct PyModuleDef*) PyModule_GetDef(PyObject*);
PyAPI_FUNC(void*) PyModule_GetState(PyObject*);
Expand Down
13 changes: 13 additions & 0 deletions Lib/test/test_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1271,6 +1271,19 @@ def test_binding(self):
except ImportError:
self.fail('circular import with binding a submodule to a name failed')

def test_crossreference1(self):
import test.test_import.data.circular_imports.use
import test.test_import.data.circular_imports.source

def test_crossreference2(self):
with self.assertRaises(AttributeError) as cm:
import test.test_import.data.circular_imports.source
errmsg = str(cm.exception)
self.assertIn('test.test_import.data.circular_imports.source', errmsg)
self.assertIn('spam', errmsg)
self.assertIn('partially initialized module', errmsg)
self.assertIn('circular import', errmsg)


if __name__ == '__main__':
# Test needs to be a package, so we can do relative imports.
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_import/data/circular_imports/source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import use
spam = 1
2 changes: 2 additions & 0 deletions Lib/test/test_import/data/circular_imports/use.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import source
source.spam
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improved :exc:`AttributeError` message for partially initialized module.
41 changes: 39 additions & 2 deletions Objects/moduleobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,27 @@ module_repr(PyModuleObject *m)
return PyObject_CallMethod(interp->importlib, "_module_repr", "O", m);
}

/* Check if the "_initializing" attribute of the module spec is set to true.
Clear the exception and return 0 if spec is NULL.
*/
int
_PyModuleSpec_IsInitializing(PyObject *spec)
{
if (spec != NULL) {
_Py_IDENTIFIER(_initializing);
PyObject *value = _PyObject_GetAttrId(spec, &PyId__initializing);
if (value != NULL) {
int initializing = PyObject_IsTrue(value);
Py_DECREF(value);
if (initializing >= 0) {
return initializing;
}
}
}
PyErr_Clear();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you always clear the current exception? What if the function is called with an exception set? Maybe start with a "assert(!PyErr_Occurred());".

I prefer the old code which only clears the exception on error.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not wrong to call this function with NULL and an exception set. Checking this here saves several lines of code and/or indentation level at caller places.

The exception is cleared on error in most cases. The only exception from this rule is for _PyDict_GetItemId() returned NULL.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, it makes sense.

return 0;
}

static PyObject*
module_getattro(PyModuleObject *m, PyObject *name)
{
Expand All @@ -717,8 +738,24 @@ module_getattro(PyModuleObject *m, PyObject *name)
_Py_IDENTIFIER(__name__);
mod_name = _PyDict_GetItemId(m->md_dict, &PyId___name__);
if (mod_name && PyUnicode_Check(mod_name)) {
PyErr_Format(PyExc_AttributeError,
"module '%U' has no attribute '%U'", mod_name, name);
_Py_IDENTIFIER(__spec__);
Py_INCREF(mod_name);
PyObject *spec = _PyDict_GetItemId(m->md_dict, &PyId___spec__);
Py_XINCREF(spec);
if (_PyModuleSpec_IsInitializing(spec)) {
PyErr_Format(PyExc_AttributeError,
"partially initialized "
"module '%U' has no attribute '%U' "
"(most likely due to a circular import)",
mod_name, name);
}
else {
PyErr_Format(PyExc_AttributeError,
"module '%U' has no attribute '%U'",
mod_name, name);
}
Py_XDECREF(spec);
Py_DECREF(mod_name);
return NULL;
}
}
Expand Down
30 changes: 9 additions & 21 deletions Python/import.c
Original file line number Diff line number Diff line change
Expand Up @@ -1721,38 +1721,26 @@ PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
mod = PyImport_GetModule(abs_name);
if (mod != NULL && mod != Py_None) {
_Py_IDENTIFIER(__spec__);
_Py_IDENTIFIER(_initializing);
_Py_IDENTIFIER(_lock_unlock_module);
PyObject *value = NULL;
PyObject *spec;
int initializing = 0;

/* Optimization: only call _bootstrap._lock_unlock_module() if
__spec__._initializing is true.
NOTE: because of this, initializing must be set *before*
stuffing the new module in sys.modules.
*/
spec = _PyObject_GetAttrId(mod, &PyId___spec__);
if (spec != NULL) {
value = _PyObject_GetAttrId(spec, &PyId__initializing);
Py_DECREF(spec);
}
if (value == NULL)
PyErr_Clear();
else {
initializing = PyObject_IsTrue(value);
Py_DECREF(value);
if (initializing == -1)
PyErr_Clear();
if (initializing > 0) {
value = _PyObject_CallMethodIdObjArgs(interp->importlib,
&PyId__lock_unlock_module, abs_name,
NULL);
if (value == NULL)
goto error;
Py_DECREF(value);
if (_PyModuleSpec_IsInitializing(spec)) {
PyObject *value = _PyObject_CallMethodIdObjArgs(interp->importlib,
&PyId__lock_unlock_module, abs_name,
NULL);
if (value == NULL) {
Py_DECREF(spec);
goto error;
}
Py_DECREF(value);
}
Py_XDECREF(spec);
}
else {
Py_XDECREF(mod);
Expand Down