Skip to content

Commit

Permalink
Revert #1575 and catch further KeyErrors (#1576)
Browse files Browse the repository at this point in the history
Provide `ModuleSpec.submodule_search_locations` to the `path` argument of `PathFinder.find_spec()`
  • Loading branch information
jacobtylerwalls authored May 30, 2022
1 parent fd89cc7 commit 18483dc
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 8 deletions.
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

0 comments on commit 18483dc

Please sign in to comment.