diff --git a/changelog/13026.bugfix.rst b/changelog/13026.bugfix.rst new file mode 100644 index 00000000000..d10edbd111a --- /dev/null +++ b/changelog/13026.bugfix.rst @@ -0,0 +1 @@ +Fixed :class:`AttributeError` crash when using ``--import-mode=importlib`` when top-level directory same name as another module of the standard library. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index dd36559ce1b..a2318e79280 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -668,7 +668,10 @@ def _import_module_using_spec( parent_module: ModuleType | None = None if parent_module_name: parent_module = sys.modules.get(parent_module_name) - if parent_module is None: + # If the parent_module lacks the `__path__` attribute, AttributeError when finding a submodule's spec, + # requiring re-import according to the path. + need_reimport = not hasattr(parent_module, "__path__") + if parent_module is None or need_reimport: # Get parent_location based on location, get parent_path based on path. if module_path.name == "__init__.py": # If the current module is in a package, diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 62359303f3b..e57e031c445 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -922,6 +922,37 @@ def test_my_test(): result = pytester.runpytest("--import-mode=importlib") result.stdout.fnmatch_lines("* 1 passed *") + @pytest.mark.parametrize("name", ["code", "time", "math"]) + def test_importlib_same_name_as_stl( + self, pytester, ns_param: bool, tmp_path: Path, name: str + ): + """Import a namespace package with the same name as the standard library (#13026).""" + file_path = pytester.path / f"{name}/foo/test_demo.py" + file_path.parent.mkdir(parents=True) + file_path.write_text( + dedent( + """ + def test_demo(): + pass + """ + ), + encoding="utf-8", + ) + + # unit test + __import__(name) # import standard library + + import_path( # import user files + file_path, + mode=ImportMode.importlib, + root=pytester.path, + consider_namespace_packages=ns_param, + ) + + # E2E test + result = pytester.runpytest("--import-mode=importlib") + result.stdout.fnmatch_lines("* 1 passed *") + def create_installed_doctests_and_tests_dir( self, path: Path, monkeypatch: MonkeyPatch ) -> tuple[Path, Path, Path]: