Skip to content

Commit 18483dc

Browse files
Revert #1575 and catch further KeyErrors (#1576)
Provide `ModuleSpec.submodule_search_locations` to the `path` argument of `PathFinder.find_spec()`
1 parent fd89cc7 commit 18483dc

File tree

2 files changed

+43
-8
lines changed

2 files changed

+43
-8
lines changed

astroid/interpreter/_import/util.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
33
# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
44

5+
from __future__ import annotations
6+
7+
import pathlib
58
import sys
69
from functools import lru_cache
10+
from importlib._bootstrap_external import _NamespacePath
711
from importlib.util import _find_spec_from_path
812

913

@@ -18,18 +22,46 @@ def is_namespace(modname: str) -> bool:
1822
# That's unacceptable here, so we fallback to _find_spec_from_path(), which does
1923
# not, but requires instead that each single parent ('astroid', 'nodes', etc.)
2024
# be specced from left to right.
21-
components = modname.split(".")
22-
for i in range(1, len(components) + 1):
23-
working_modname = ".".join(components[:i])
25+
processed_components = []
26+
last_submodule_search_locations: _NamespacePath | None = None
27+
for component in modname.split("."):
28+
processed_components.append(component)
29+
working_modname = ".".join(processed_components)
2430
try:
25-
# Search under the highest package name
26-
# Only relevant if package not already on sys.path
27-
# See https://github.com/python/cpython/issues/89754 for reasoning
28-
# Otherwise can raise bare KeyError: https://github.com/python/cpython/issues/93334
29-
found_spec = _find_spec_from_path(working_modname, components[0])
31+
# Both the modname and the path are built iteratively, with the
32+
# path (e.g. ['a', 'a/b', 'a/b/c']) lagging the modname by one
33+
found_spec = _find_spec_from_path(
34+
working_modname, path=last_submodule_search_locations
35+
)
3036
except ValueError:
3137
# executed .pth files may not have __spec__
3238
return True
39+
except KeyError:
40+
# Intermediate steps might raise KeyErrors
41+
# https://github.com/python/cpython/issues/93334
42+
# TODO: update if fixed in importlib
43+
# For tree a > b > c.py
44+
# >>> from importlib.machinery import PathFinder
45+
# >>> PathFinder.find_spec('a.b', ['a'])
46+
# KeyError: 'a'
47+
48+
# Repair last_submodule_search_locations
49+
if last_submodule_search_locations:
50+
# TODO: py38: remove except
51+
try:
52+
# pylint: disable=unsubscriptable-object
53+
last_item = last_submodule_search_locations[-1]
54+
except TypeError:
55+
last_item = last_submodule_search_locations._recalculate()[-1]
56+
# e.g. for failure example above, add 'a/b' and keep going
57+
# so that find_spec('a.b.c', path=['a', 'a/b']) succeeds
58+
assumed_location = pathlib.Path(last_item) / component
59+
last_submodule_search_locations.append(str(assumed_location))
60+
continue
61+
62+
# Update last_submodule_search_locations
63+
if found_spec and found_spec.submodule_search_locations:
64+
last_submodule_search_locations = found_spec.submodule_search_locations
3365

3466
if found_spec is None:
3567
return False

tests/unittest_manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ def test_submodule_homonym_with_non_module(self) -> None:
125125
util.is_namespace("tests.testdata.python3.data.parent_of_homonym.doc")
126126
)
127127

128+
def test_module_is_not_namespace(self) -> None:
129+
self.assertFalse(util.is_namespace("tests.testdata.python3.data.all"))
130+
128131
def test_implicit_namespace_package(self) -> None:
129132
data_dir = os.path.dirname(resources.find("data/namespace_pep_420"))
130133
contribute = os.path.join(data_dir, "contribute_to_namespace")

0 commit comments

Comments
 (0)