mirror of
https://github.com/python/cpython.git
synced 2025-09-26 10:19:53 +00:00
bpo-44717: improve AttributeError on circular imports of submodules (GH-27299)
Signed-off-by: Filipe Laíns <lains@riseup.net> Co-authored-by: Łukasz Langa <lukasz@langa.pl>
This commit is contained in:
parent
5370f0a82a
commit
8072a1181d
8 changed files with 1807 additions and 1734 deletions
|
@ -361,6 +361,7 @@ class ModuleSpec:
|
||||||
self.origin = origin
|
self.origin = origin
|
||||||
self.loader_state = loader_state
|
self.loader_state = loader_state
|
||||||
self.submodule_search_locations = [] if is_package else None
|
self.submodule_search_locations = [] if is_package else None
|
||||||
|
self._uninitialized_submodules = []
|
||||||
|
|
||||||
# file-location attributes
|
# file-location attributes
|
||||||
self._set_fileattr = False
|
self._set_fileattr = False
|
||||||
|
@ -987,6 +988,7 @@ _ERR_MSG = _ERR_MSG_PREFIX + '{!r}'
|
||||||
def _find_and_load_unlocked(name, import_):
|
def _find_and_load_unlocked(name, import_):
|
||||||
path = None
|
path = None
|
||||||
parent = name.rpartition('.')[0]
|
parent = name.rpartition('.')[0]
|
||||||
|
parent_spec = None
|
||||||
if parent:
|
if parent:
|
||||||
if parent not in sys.modules:
|
if parent not in sys.modules:
|
||||||
_call_with_frames_removed(import_, parent)
|
_call_with_frames_removed(import_, parent)
|
||||||
|
@ -999,15 +1001,24 @@ def _find_and_load_unlocked(name, import_):
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
msg = (_ERR_MSG + '; {!r} is not a package').format(name, parent)
|
msg = (_ERR_MSG + '; {!r} is not a package').format(name, parent)
|
||||||
raise ModuleNotFoundError(msg, name=name) from None
|
raise ModuleNotFoundError(msg, name=name) from None
|
||||||
|
parent_spec = parent_module.__spec__
|
||||||
|
child = name.rpartition('.')[2]
|
||||||
spec = _find_spec(name, path)
|
spec = _find_spec(name, path)
|
||||||
if spec is None:
|
if spec is None:
|
||||||
raise ModuleNotFoundError(_ERR_MSG.format(name), name=name)
|
raise ModuleNotFoundError(_ERR_MSG.format(name), name=name)
|
||||||
else:
|
else:
|
||||||
module = _load_unlocked(spec)
|
if parent_spec:
|
||||||
|
# Temporarily add child we are currently importing to parent's
|
||||||
|
# _uninitialized_submodules for circular import tracking.
|
||||||
|
parent_spec._uninitialized_submodules.append(child)
|
||||||
|
try:
|
||||||
|
module = _load_unlocked(spec)
|
||||||
|
finally:
|
||||||
|
if parent_spec:
|
||||||
|
parent_spec._uninitialized_submodules.pop()
|
||||||
if parent:
|
if parent:
|
||||||
# Set the module as an attribute on its parent.
|
# Set the module as an attribute on its parent.
|
||||||
parent_module = sys.modules[parent]
|
parent_module = sys.modules[parent]
|
||||||
child = name.rpartition('.')[2]
|
|
||||||
try:
|
try:
|
||||||
setattr(parent_module, child, module)
|
setattr(parent_module, child, module)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
|
|
@ -1350,6 +1350,16 @@ class CircularImportTests(unittest.TestCase):
|
||||||
str(cm.exception),
|
str(cm.exception),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_absolute_circular_submodule(self):
|
||||||
|
with self.assertRaises(AttributeError) as cm:
|
||||||
|
import test.test_import.data.circular_imports.subpkg2.parent
|
||||||
|
self.assertIn(
|
||||||
|
"cannot access submodule 'parent' of module "
|
||||||
|
"'test.test_import.data.circular_imports.subpkg2' "
|
||||||
|
"(most likely due to a circular import)",
|
||||||
|
str(cm.exception),
|
||||||
|
)
|
||||||
|
|
||||||
def test_unwritable_module(self):
|
def test_unwritable_module(self):
|
||||||
self.addCleanup(unload, "test.test_import.data.unwritable")
|
self.addCleanup(unload, "test.test_import.data.unwritable")
|
||||||
self.addCleanup(unload, "test.test_import.data.unwritable.x")
|
self.addCleanup(unload, "test.test_import.data.unwritable.x")
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
import test.test_import.data.circular_imports.subpkg2.parent.child
|
|
@ -0,0 +1,3 @@
|
||||||
|
import test.test_import.data.circular_imports.subpkg2.parent
|
||||||
|
|
||||||
|
test.test_import.data.circular_imports.subpkg2.parent
|
|
@ -0,0 +1 @@
|
||||||
|
Improve AttributeError on circular imports of submodules.
|
|
@ -739,6 +739,30 @@ _PyModuleSpec_IsInitializing(PyObject *spec)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Check if the submodule name is in the "_uninitialized_submodules" attribute
|
||||||
|
of the module spec.
|
||||||
|
*/
|
||||||
|
int
|
||||||
|
_PyModuleSpec_IsUninitializedSubmodule(PyObject *spec, PyObject *name)
|
||||||
|
{
|
||||||
|
if (spec == NULL) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_Py_IDENTIFIER(_uninitialized_submodules);
|
||||||
|
PyObject *value = _PyObject_GetAttrId(spec, &PyId__uninitialized_submodules);
|
||||||
|
if (value == NULL) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int is_uninitialized = PySequence_Contains(value, name);
|
||||||
|
Py_DECREF(value);
|
||||||
|
if (is_uninitialized == -1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return is_uninitialized;
|
||||||
|
}
|
||||||
|
|
||||||
static PyObject*
|
static PyObject*
|
||||||
module_getattro(PyModuleObject *m, PyObject *name)
|
module_getattro(PyModuleObject *m, PyObject *name)
|
||||||
{
|
{
|
||||||
|
@ -773,6 +797,12 @@ module_getattro(PyModuleObject *m, PyObject *name)
|
||||||
"(most likely due to a circular import)",
|
"(most likely due to a circular import)",
|
||||||
mod_name, name);
|
mod_name, name);
|
||||||
}
|
}
|
||||||
|
else if (_PyModuleSpec_IsUninitializedSubmodule(spec, name)) {
|
||||||
|
PyErr_Format(PyExc_AttributeError,
|
||||||
|
"cannot access submodule '%U' of module '%U' "
|
||||||
|
"(most likely due to a circular import)",
|
||||||
|
name, mod_name);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
PyErr_Format(PyExc_AttributeError,
|
PyErr_Format(PyExc_AttributeError,
|
||||||
"module '%U' has no attribute '%U'",
|
"module '%U' has no attribute '%U'",
|
||||||
|
|
3481
Python/importlib.h
generated
3481
Python/importlib.h
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue