2
2
# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
3
3
# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
4
4
5
+ from __future__ import annotations
6
+
7
+ import pathlib
5
8
import sys
6
9
from functools import lru_cache
10
+ from importlib ._bootstrap_external import _NamespacePath
7
11
from importlib .util import _find_spec_from_path
8
12
9
13
@@ -18,18 +22,46 @@ def is_namespace(modname: str) -> bool:
18
22
# That's unacceptable here, so we fallback to _find_spec_from_path(), which does
19
23
# not, but requires instead that each single parent ('astroid', 'nodes', etc.)
20
24
# 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 )
24
30
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
+ )
30
36
except ValueError :
31
37
# executed .pth files may not have __spec__
32
38
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
33
65
34
66
if found_spec is None :
35
67
return False
0 commit comments