From f4289181cb0ed651c8778eb247a7c15b1f3b20ad Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 13 Apr 2024 11:39:54 -0300 Subject: [PATCH] importlib: _import_module_using_spec also imports parent modules According to the Python import implementation: https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/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. --- src/_pytest/pathlib.py | 29 ++++++++++++++--------------- testing/test_pathlib.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 3cdaa0b6e..e1a336719 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -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: diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index dbd931474..fda03b003 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -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()