Skip to content

Commit 7108bb9

Browse files
Fix: Using --import-mode=importlib, a directory with the same name in the namespace package causes a KeyError.(#12592
1 parent 7928dad commit 7108bb9

File tree

3 files changed

+95
-32
lines changed

3 files changed

+95
-32
lines changed

changelog/12592.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed the issue of causes ``KeyError`` when using the parameter ``--import-mode=importlib`` in pytest>=8.2 .

src/_pytest/pathlib.py

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import fnmatch
1111
from functools import partial
1212
from importlib.machinery import ModuleSpec
13+
from importlib.machinery import PathFinder
1314
import importlib.util
1415
import itertools
1516
import os
@@ -37,8 +38,12 @@
3738
from _pytest.warning_types import PytestWarning
3839

3940

40-
LOCK_TIMEOUT = 60 * 60 * 24 * 3
41+
if sys.version_info < (3, 11):
42+
from importlib._bootstrap_external import _NamespaceLoader as NamespaceLoader
43+
else:
44+
from importlib.machinery import NamespaceLoader
4145

46+
LOCK_TIMEOUT = 60 * 60 * 24 * 3
4247

4348
_AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath)
4449

@@ -618,6 +623,34 @@ def _import_module_using_spec(
618623
If True, will call insert_missing_modules to create empty intermediate modules
619624
for made-up module names (when importing test files not reachable from sys.path).
620625
"""
626+
# Attempt to import the parent module, seems is our responsibility:
627+
# https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311
628+
parent_module_name, _, name = module_name.rpartition(".")
629+
parent_module: ModuleType | None = None
630+
if parent_module_name:
631+
parent_module = sys.modules.get(parent_module_name)
632+
if parent_module is None:
633+
if (module_path.parent / "__init__.py").is_file():
634+
parent_module_location = module_location
635+
else:
636+
parent_module_location = module_location.parent
637+
638+
if module_path.name == "__init__.py":
639+
parent_module_path = module_path.parent.parent
640+
else:
641+
parent_module_path = module_path.parent
642+
643+
# Consider the parent module path as its __init__.py file, if it has one.
644+
if (parent_module_path / "__init__.py").is_file():
645+
parent_module_path = parent_module_path / "__init__.py"
646+
647+
parent_module = _import_module_using_spec(
648+
parent_module_name,
649+
parent_module_path,
650+
parent_module_location,
651+
insert_modules=insert_modules,
652+
)
653+
621654
# Checking with sys.meta_path first in case one of its hooks can import this module,
622655
# such as our own assertion-rewrite hook.
623656
for meta_importer in sys.meta_path:
@@ -627,36 +660,18 @@ def _import_module_using_spec(
627660
if spec_matches_module_path(spec, module_path):
628661
break
629662
else:
630-
spec = importlib.util.spec_from_file_location(module_name, str(module_path))
663+
loader = None
664+
if "." in module_path.name and module_path.is_dir():
665+
# The `spec_from_file_location` matches a loader based on the file extension by default.
666+
# For a namespace package, you need to manually specify a loader.
667+
loader = NamespaceLoader(name, module_path, PathFinder())
668+
669+
spec = importlib.util.spec_from_file_location(
670+
module_name, str(module_path), loader=loader
671+
)
631672

632673
if spec_matches_module_path(spec, module_path):
633674
assert spec is not None
634-
# Attempt to import the parent module, seems is our responsibility:
635-
# https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311
636-
parent_module_name, _, name = module_name.rpartition(".")
637-
parent_module: ModuleType | None = None
638-
if parent_module_name:
639-
parent_module = sys.modules.get(parent_module_name)
640-
if parent_module is None:
641-
# Find the directory of this module's parent.
642-
parent_dir = (
643-
module_path.parent.parent
644-
if module_path.name == "__init__.py"
645-
else module_path.parent
646-
)
647-
# Consider the parent module path as its __init__.py file, if it has one.
648-
parent_module_path = (
649-
parent_dir / "__init__.py"
650-
if (parent_dir / "__init__.py").is_file()
651-
else parent_dir
652-
)
653-
parent_module = _import_module_using_spec(
654-
parent_module_name,
655-
parent_module_path,
656-
parent_dir,
657-
insert_modules=insert_modules,
658-
)
659-
660675
# Find spec and import this module.
661676
mod = importlib.util.module_from_spec(spec)
662677
sys.modules[module_name] = mod
@@ -669,16 +684,25 @@ def _import_module_using_spec(
669684
if insert_modules:
670685
insert_missing_modules(sys.modules, module_name)
671686
return mod
672-
673687
return None
674688

675689

676690
def spec_matches_module_path(module_spec: ModuleSpec | None, module_path: Path) -> bool:
677691
"""Return true if the given ModuleSpec can be used to import the given module path."""
678-
if module_spec is None or module_spec.origin is None:
692+
if module_spec is None:
679693
return False
680694

681-
return Path(module_spec.origin) == module_path
695+
if module_spec.origin:
696+
return Path(module_spec.origin) == module_path
697+
698+
# If this is a namespace package, compare the path with the `module_spec.submodule_Search_Locations`
699+
# https://docs.python.org/zh-cn/3/library/importlib.html#importlib.machinery.ModuleSpec.submodule_search_locations
700+
if module_spec.submodule_search_locations:
701+
for _path in module_spec.submodule_search_locations:
702+
if Path(_path) == module_path:
703+
return True
704+
705+
return False
682706

683707

684708
# Implement a special _is_same function on Windows which returns True if the two filenames

testing/test_pathlib.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ def test_no_meta_path_found(
416416
del sys.modules[module.__name__]
417417

418418
monkeypatch.setattr(
419-
importlib.util, "spec_from_file_location", lambda *args: None
419+
importlib.util, "spec_from_file_location", lambda *args, **kwargs: None
420420
)
421421
with pytest.raises(ImportError):
422422
import_path(
@@ -1541,6 +1541,44 @@ def test_full_ns_packages_without_init_files(
15411541
tmp_path / "src/dist2/ns/a/core/foo/m.py", consider_namespace_packages=True
15421542
) == (tmp_path / "src/dist2", "ns.a.core.foo.m")
15431543

1544+
def test_ns_multiple_levels_import_same_name_directory(
1545+
self,
1546+
tmp_path: Path,
1547+
monkeypatch: MonkeyPatch,
1548+
pytester: Pytester,
1549+
) -> None:
1550+
"""Check KeyError with `--import-mode=importlib` (#12592)."""
1551+
self.setup_directories(tmp_path, monkeypatch, pytester)
1552+
code = dedent("""
1553+
def test():
1554+
assert "four lights" == "five lights"
1555+
""")
1556+
1557+
# a subdirectories with the same name in the namespace package,
1558+
# along with a tests file
1559+
test_base_path = tmp_path / "src/dist2/com/company"
1560+
test_py = test_base_path / "a/b/c/test_demo.py"
1561+
test_dir = test_base_path / "a/b/c/c"
1562+
1563+
test_dir.mkdir(parents=True)
1564+
test_py.write_text(code, encoding="UTF-8")
1565+
1566+
pkg_root, module_name = resolve_pkg_root_and_module_name(
1567+
test_py, consider_namespace_packages=True
1568+
)
1569+
assert (pkg_root, module_name) == (
1570+
tmp_path / "src/dist2",
1571+
"com.company.a.b.c.test_demo",
1572+
)
1573+
1574+
result = pytester.runpytest("--import-mode=importlib", test_py)
1575+
1576+
result.stdout.no_fnmatch_line(
1577+
"E KeyError: 'test_ns_multiple_levels_import1.src.dist2.com.company.a.b'"
1578+
)
1579+
1580+
assert "KeyError" not in result.stdout.str()
1581+
15441582

15451583
def test_is_importable(pytester: Pytester) -> None:
15461584
pytester.syspathinsert()

0 commit comments

Comments
 (0)