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
58import sys
69from functools import lru_cache
10+ from importlib ._bootstrap_external import _NamespacePath
711from 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
0 commit comments