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:
Bruno Oliveira 2024-04-13 11:39:54 -03:00 committed by Bruno Oliveira
parent 70dd4b0880
commit f4289181cb
2 changed files with 49 additions and 15 deletions

View File

@ -636,31 +636,30 @@ def _import_module_using_spec(
if spec_matches_module_path(spec, module_path):
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)
sys.modules[module_name] = mod
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:
insert_missing_modules(sys.modules, module_name)
_set_name_in_parent(mod)
return mod
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(
module_spec: Optional[ModuleSpec], module_path: Path
) -> bool:

View File

@ -1127,6 +1127,41 @@ def test_safe_exists(tmp_path: Path) -> 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."""
pytester.path.joinpath("foo/bar/baz").mkdir(parents=True)
pytester.path.joinpath("foo/__init__.py").touch()