Skip to content

Commit 0a8ae8a

Browse files
authored
bpo-44717: improve AttributeError on circular imports of submodules (pythonGH-27338)
1 parent 717f608 commit 0a8ae8a

File tree

9 files changed

+1809
-1734
lines changed

9 files changed

+1809
-1734
lines changed

Lib/importlib/_bootstrap.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ def __init__(self, name, loader, *, origin=None, loader_state=None,
361361
self.origin = origin
362362
self.loader_state = loader_state
363363
self.submodule_search_locations = [] if is_package else None
364+
self._uninitialized_submodules = []
364365

365366
# file-location attributes
366367
self._set_fileattr = False
@@ -987,6 +988,7 @@ def _sanity_check(name, package, level):
987988
def _find_and_load_unlocked(name, import_):
988989
path = None
989990
parent = name.rpartition('.')[0]
991+
parent_spec = None
990992
if parent:
991993
if parent not in sys.modules:
992994
_call_with_frames_removed(import_, parent)
@@ -999,15 +1001,24 @@ def _find_and_load_unlocked(name, import_):
9991001
except AttributeError:
10001002
msg = (_ERR_MSG + '; {!r} is not a package').format(name, parent)
10011003
raise ModuleNotFoundError(msg, name=name) from None
1004+
parent_spec = parent_module.__spec__
1005+
child = name.rpartition('.')[2]
10021006
spec = _find_spec(name, path)
10031007
if spec is None:
10041008
raise ModuleNotFoundError(_ERR_MSG.format(name), name=name)
10051009
else:
1006-
module = _load_unlocked(spec)
1010+
if parent_spec:
1011+
# Temporarily add child we are currently importing to parent's
1012+
# _uninitialized_submodules for circular import tracking.
1013+
parent_spec._uninitialized_submodules.append(child)
1014+
try:
1015+
module = _load_unlocked(spec)
1016+
finally:
1017+
if parent_spec:
1018+
parent_spec._uninitialized_submodules.pop()
10071019
if parent:
10081020
# Set the module as an attribute on its parent.
10091021
parent_module = sys.modules[parent]
1010-
child = name.rpartition('.')[2]
10111022
try:
10121023
setattr(parent_module, child, module)
10131024
except AttributeError:

Lib/test/test_import/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,6 +1350,16 @@ def test_circular_from_import(self):
13501350
str(cm.exception),
13511351
)
13521352

1353+
def test_absolute_circular_submodule(self):
1354+
with self.assertRaises(AttributeError) as cm:
1355+
import test.test_import.data.circular_imports.subpkg2.parent
1356+
self.assertIn(
1357+
"cannot access submodule 'parent' of module "
1358+
"'test.test_import.data.circular_imports.subpkg2' "
1359+
"(most likely due to a circular import)",
1360+
str(cm.exception),
1361+
)
1362+
13531363
def test_unwritable_module(self):
13541364
self.addCleanup(unload, "test.test_import.data.unwritable")
13551365
self.addCleanup(unload, "test.test_import.data.unwritable.x")

Lib/test/test_import/data/circular_imports/subpkg2/__init__.py

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import test.test_import.data.circular_imports.subpkg2.parent.child
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.subpkg2.parent
2+
3+
test.test_import.data.circular_imports.subpkg2.parent

Makefile.pre.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1473,6 +1473,8 @@ TESTSUBDIRS= ctypes/test \
14731473
test/test_import/data \
14741474
test/test_import/data/circular_imports \
14751475
test/test_import/data/circular_imports/subpkg \
1476+
test/test_import/data/circular_imports/subpkg2 \
1477+
test/test_import/data/circular_imports/subpkg2/parent \
14761478
test/test_import/data/package \
14771479
test/test_import/data/package2 \
14781480
test/test_import/data/unwritable \
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve AttributeError on circular imports of submodules.

Objects/moduleobject.c

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,30 @@ _PyModuleSpec_IsInitializing(PyObject *spec)
739739
return 0;
740740
}
741741

742+
/* Check if the submodule name is in the "_uninitialized_submodules" attribute
743+
of the module spec.
744+
*/
745+
int
746+
_PyModuleSpec_IsUninitializedSubmodule(PyObject *spec, PyObject *name)
747+
{
748+
if (spec == NULL) {
749+
return 0;
750+
}
751+
752+
_Py_IDENTIFIER(_uninitialized_submodules);
753+
PyObject *value = _PyObject_GetAttrId(spec, &PyId__uninitialized_submodules);
754+
if (value == NULL) {
755+
return 0;
756+
}
757+
758+
int is_uninitialized = PySequence_Contains(value, name);
759+
Py_DECREF(value);
760+
if (is_uninitialized == -1) {
761+
return 0;
762+
}
763+
return is_uninitialized;
764+
}
765+
742766
static PyObject*
743767
module_getattro(PyModuleObject *m, PyObject *name)
744768
{
@@ -773,6 +797,12 @@ module_getattro(PyModuleObject *m, PyObject *name)
773797
"(most likely due to a circular import)",
774798
mod_name, name);
775799
}
800+
else if (_PyModuleSpec_IsUninitializedSubmodule(spec, name)) {
801+
PyErr_Format(PyExc_AttributeError,
802+
"cannot access submodule '%U' of module '%U' "
803+
"(most likely due to a circular import)",
804+
name, mod_name);
805+
}
776806
else {
777807
PyErr_Format(PyExc_AttributeError,
778808
"module '%U' has no attribute '%U'",

0 commit comments

Comments
 (0)