importlib: _import_module_using_spec also imports parent modules
According to the Python import implementation:
73906d5c90/Lib/importlib/_bootstrap.py (L1308-L1311)
When we import a module we should also import its parents, and set the child as attribute of its parent.
This commit is contained in:
parent
70dd4b0880
commit
f4289181cb
|
@ -636,31 +636,30 @@ def _import_module_using_spec(
|
||||||
|
|
||||||
if spec_matches_module_path(spec, module_path):
|
if spec_matches_module_path(spec, module_path):
|
||||||
assert spec is not None
|
assert spec is not None
|
||||||
|
# Attempt to import the parent module, seems is our responsibility:
|
||||||
|
# https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311
|
||||||
|
parent_module_name, _, name = module_name.rpartition(".")
|
||||||
|
parent_module: Optional[ModuleType] = sys.modules.get(parent_module_name)
|
||||||
|
if parent_module is None and parent_module_name:
|
||||||
|
with contextlib.suppress(ModuleNotFoundError, ImportWarning):
|
||||||
|
parent_module = importlib.import_module(parent_module_name)
|
||||||
|
|
||||||
|
# Find spec and import this module.
|
||||||
mod = importlib.util.module_from_spec(spec)
|
mod = importlib.util.module_from_spec(spec)
|
||||||
sys.modules[module_name] = mod
|
sys.modules[module_name] = mod
|
||||||
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
||||||
|
|
||||||
|
# Set this module as an attribute of the parent module (#12194).
|
||||||
|
if parent_module is not None:
|
||||||
|
setattr(parent_module, name, mod)
|
||||||
|
|
||||||
if insert_modules:
|
if insert_modules:
|
||||||
insert_missing_modules(sys.modules, module_name)
|
insert_missing_modules(sys.modules, module_name)
|
||||||
_set_name_in_parent(mod)
|
|
||||||
return mod
|
return mod
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _set_name_in_parent(module: ModuleType) -> None:
|
|
||||||
"""
|
|
||||||
Sets an attribute in the module's parent pointing to the module itself (#12194).
|
|
||||||
|
|
||||||
Based on https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1335-L1342.
|
|
||||||
"""
|
|
||||||
parent, _, name = module.__name__.rpartition(".")
|
|
||||||
if not parent:
|
|
||||||
return
|
|
||||||
parent_module = sys.modules.get(parent)
|
|
||||||
if parent_module is not None:
|
|
||||||
setattr(sys.modules[parent], name, module)
|
|
||||||
|
|
||||||
|
|
||||||
def spec_matches_module_path(
|
def spec_matches_module_path(
|
||||||
module_spec: Optional[ModuleSpec], module_path: Path
|
module_spec: Optional[ModuleSpec], module_path: Path
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
|
@ -1127,6 +1127,41 @@ def test_safe_exists(tmp_path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_import_sets_module_as_attribute(pytester: Pytester) -> None:
|
def test_import_sets_module_as_attribute(pytester: Pytester) -> None:
|
||||||
|
"""Unittest test for #12194."""
|
||||||
|
pytester.path.joinpath("foo/bar/baz").mkdir(parents=True)
|
||||||
|
pytester.path.joinpath("foo/__init__.py").touch()
|
||||||
|
pytester.path.joinpath("foo/bar/__init__.py").touch()
|
||||||
|
pytester.path.joinpath("foo/bar/baz/__init__.py").touch()
|
||||||
|
pytester.syspathinsert()
|
||||||
|
|
||||||
|
# Import foo.bar.baz and ensure parent modules also ended up imported.
|
||||||
|
baz = import_path(
|
||||||
|
pytester.path.joinpath("foo/bar/baz/__init__.py"),
|
||||||
|
mode=ImportMode.importlib,
|
||||||
|
root=pytester.path,
|
||||||
|
consider_namespace_packages=False,
|
||||||
|
)
|
||||||
|
assert baz.__name__ == "foo.bar.baz"
|
||||||
|
foo = sys.modules["foo"]
|
||||||
|
assert foo.__name__ == "foo"
|
||||||
|
bar = sys.modules["foo.bar"]
|
||||||
|
assert bar.__name__ == "foo.bar"
|
||||||
|
|
||||||
|
# Check parent modules have an attribute pointing to their children.
|
||||||
|
assert bar.baz is baz
|
||||||
|
assert foo.bar is bar
|
||||||
|
|
||||||
|
# Ensure we returned the "foo.bar" module cached in sys.modules.
|
||||||
|
bar_2 = import_path(
|
||||||
|
pytester.path.joinpath("foo/bar/__init__.py"),
|
||||||
|
mode=ImportMode.importlib,
|
||||||
|
root=pytester.path,
|
||||||
|
consider_namespace_packages=False,
|
||||||
|
)
|
||||||
|
assert bar_2 is bar
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_sets_module_as_attribute_regression(pytester: Pytester) -> None:
|
||||||
"""Regression test for #12194."""
|
"""Regression test for #12194."""
|
||||||
pytester.path.joinpath("foo/bar/baz").mkdir(parents=True)
|
pytester.path.joinpath("foo/bar/baz").mkdir(parents=True)
|
||||||
pytester.path.joinpath("foo/__init__.py").touch()
|
pytester.path.joinpath("foo/__init__.py").touch()
|
||||||
|
|
Loading…
Reference in New Issue