Skip to content

Revert #1575 and catch further KeyErrors #1576

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 40 additions & 8 deletions astroid/interpreter/_import/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt

from __future__ import annotations

import pathlib
import sys
from functools import lru_cache
from importlib._bootstrap_external import _NamespacePath
from importlib.util import _find_spec_from_path


Expand All @@ -18,18 +22,46 @@ def is_namespace(modname: str) -> bool:
# That's unacceptable here, so we fallback to _find_spec_from_path(), which does
# not, but requires instead that each single parent ('astroid', 'nodes', etc.)
# be specced from left to right.
components = modname.split(".")
for i in range(1, len(components) + 1):
working_modname = ".".join(components[:i])
processed_components = []
last_submodule_search_locations: _NamespacePath | None = None
for component in modname.split("."):
processed_components.append(component)
working_modname = ".".join(processed_components)
try:
# Search under the highest package name
# Only relevant if package not already on sys.path
# See https://github.com/python/cpython/issues/89754 for reasoning
# Otherwise can raise bare KeyError: https://github.com/python/cpython/issues/93334
found_spec = _find_spec_from_path(working_modname, components[0])
# Both the modname and the path are built iteratively, with the
# path (e.g. ['a', 'a/b', 'a/b/c']) lagging the modname by one
found_spec = _find_spec_from_path(
working_modname, path=last_submodule_search_locations
)
except ValueError:
# executed .pth files may not have __spec__
return True
except KeyError:
# Intermediate steps might raise KeyErrors
# https://github.com/python/cpython/issues/93334
# TODO: update if fixed in importlib
# For tree a > b > c.py
# >>> from importlib.machinery import PathFinder
# >>> PathFinder.find_spec('a.b', ['a'])
# KeyError: 'a'

# Repair last_submodule_search_locations
if last_submodule_search_locations:
# TODO: py38: remove except
try:
# pylint: disable=unsubscriptable-object
last_item = last_submodule_search_locations[-1]
except TypeError:
last_item = last_submodule_search_locations._recalculate()[-1]
# e.g. for failure example above, add 'a/b' and keep going
# so that find_spec('a.b.c', path=['a', 'a/b']) succeeds
assumed_location = pathlib.Path(last_item) / component
last_submodule_search_locations.append(str(assumed_location))
continue

# Update last_submodule_search_locations
if found_spec and found_spec.submodule_search_locations:
last_submodule_search_locations = found_spec.submodule_search_locations

if found_spec is None:
return False
Expand Down
3 changes: 3 additions & 0 deletions tests/unittest_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ def test_submodule_homonym_with_non_module(self) -> None:
util.is_namespace("tests.testdata.python3.data.parent_of_homonym.doc")
)

def test_module_is_not_namespace(self) -> None:
self.assertFalse(util.is_namespace("tests.testdata.python3.data.all"))

def test_implicit_namespace_package(self) -> None:
data_dir = os.path.dirname(resources.find("data/namespace_pep_420"))
contribute = os.path.join(data_dir, "contribute_to_namespace")
Expand Down