8000 bpo-44717: improve AttributeError on circular imports of submodules (… · python/cpython@0a8ae8a · GitHub
[go: up one dir, main page]

Skip to content

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 0a8ae8a

Browse files
authored
bpo-44717: improve AttributeError on circular imports of submodules (GH-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)
0