Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
gh-95754: Better AttributeError on partially initialised module
  • Loading branch information
hauntsaninja committed Dec 1, 2023
commit 3e339aed2bc84e97b038aa07b1a2bdf5e06c5cc3
8 changes: 8 additions & 0 deletions Lib/test/test_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1629,6 +1629,14 @@ def test_circular_from_import(self):
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
Expand Down
3 changes: 3 additions & 0 deletions Lib/test/test_import/data/circular_imports/import_cycle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import test.test_import.data.circular_imports.import_cycle as m

m.some_attribute
15 changes: 13 additions & 2 deletions Objects/moduleobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -799,7 +799,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;
Expand Down Expand Up @@ -841,11 +841,22 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
}
if (suppress != 1) {
if (_PyModuleSpec_IsInitializing(spec)) {
PyErr_Format(PyExc_AttributeError,
origin = PyObject_GetAttr(spec, &_Py_ID(origin));
Copy link
Member

Choose a reason for hiding this comment

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

PyObject_GetAttr() can set not only AttributeError. If it sets a different error, it should not be overridden by AttributeError.

I suggest to use PyObject_GetOptionalAttr().

Copy link
Member

Choose a reason for hiding this comment

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

Why not chain the original error to a new exception (raise from)?

Copy link
Member

Choose a reason for hiding this comment

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

Or maybe this is a use case for adding a note to the original exception and letting it propagate on (if we don't want to change the type of the exception being raised).

if (origin == NULL || origin == Py_None) {
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,
"partially initialized "
"module '%U' from '%U' has no attribute '%U' "
"(most likely due to a circular import)",
mod_name, origin, name);
}
Py_XDECREF(origin);
}
else if (_PyModuleSpec_IsUninitializedSubmodule(spec, name)) {
PyErr_Format(PyExc_AttributeError,
Expand Down