1# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
2# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE
3# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt
4
5from __future__ import annotations
6
7import pathlib
8import sys
9from functools import lru_cache
10from importlib._bootstrap_external import _NamespacePath # type: ignore[attr-defined]
11from importlib.util import _find_spec_from_path # type: ignore[attr-defined]
12
13from astroid.const import IS_PYPY
14
15if sys.version_info >= (3, 11):
16 from importlib.machinery import NamespaceLoader
17else:
18 from importlib._bootstrap_external import _NamespaceLoader as NamespaceLoader
19
20
21@lru_cache(maxsize=4096)
22def is_namespace(modname: str) -> bool:
23 from astroid.modutils import ( # pylint: disable=import-outside-toplevel
24 EXT_LIB_DIRS,
25 STD_LIB_DIRS,
26 )
27
28 STD_AND_EXT_LIB_DIRS = STD_LIB_DIRS.union(EXT_LIB_DIRS)
29
30 if modname in sys.builtin_module_names:
31 return False
32
33 found_spec = None
34
35 # find_spec() attempts to import parent packages when given dotted paths.
36 # That's unacceptable here, so we fallback to _find_spec_from_path(), which does
37 # not, but requires instead that each single parent ('astroid', 'nodes', etc.)
38 # be specced from left to right.
39 processed_components = []
40 last_submodule_search_locations: _NamespacePath | None = None
41 for component in modname.split("."):
42 processed_components.append(component)
43 working_modname = ".".join(processed_components)
44 try:
45 # Both the modname and the path are built iteratively, with the
46 # path (e.g. ['a', 'a/b', 'a/b/c']) lagging the modname by one
47 found_spec = _find_spec_from_path(
48 working_modname, path=last_submodule_search_locations
49 )
50 except AttributeError:
51 return False
52 except ValueError:
53 if modname == "__main__":
54 return False
55 try:
56 # .pth files will be on sys.modules
57 # __spec__ is set inconsistently on PyPy so we can't really on the heuristic here
58 # See: https://foss.heptapod.net/pypy/pypy/-/issues/3736
59 # Check first fragment of modname, e.g. "astroid", not "astroid.interpreter"
60 # because of cffi's behavior
61 # See: https://github.com/pylint-dev/astroid/issues/1776
62 mod = sys.modules[processed_components[0]]
63 return (
64 mod.__spec__ is None
65 and getattr(mod, "__file__", None) is None
66 and hasattr(mod, "__path__")
67 and not IS_PYPY
68 )
69 except KeyError:
70 return False
71 except AttributeError:
72 # Workaround for "py" module
73 # https://github.com/pytest-dev/apipkg/issues/13
74 return False
75 except KeyError:
76 # Intermediate steps might raise KeyErrors
77 # https://github.com/python/cpython/issues/93334
78 # TODO: update if fixed in importlib
79 # For tree a > b > c.py
80 # >>> from importlib.machinery import PathFinder
81 # >>> PathFinder.find_spec('a.b', ['a'])
82 # KeyError: 'a'
83
84 # Repair last_submodule_search_locations
85 if last_submodule_search_locations:
86 # pylint: disable=unsubscriptable-object
87 last_item = last_submodule_search_locations[-1]
88 # e.g. for failure example above, add 'a/b' and keep going
89 # so that find_spec('a.b.c', path=['a', 'a/b']) succeeds
90 assumed_location = pathlib.Path(last_item) / component
91 last_submodule_search_locations.append(str(assumed_location))
92 continue
93
94 # Update last_submodule_search_locations for next iteration
95 if found_spec and found_spec.submodule_search_locations:
96 # But immediately return False if we can detect we are in stdlib
97 # or external lib (e.g site-packages)
98 if any(
99 any(location.startswith(lib_dir) for lib_dir in STD_AND_EXT_LIB_DIRS)
100 for location in found_spec.submodule_search_locations
101 ):
102 return False
103 last_submodule_search_locations = found_spec.submodule_search_locations
104
105 return (
106 found_spec is not None
107 and found_spec.submodule_search_locations is not None
108 and found_spec.origin is None
109 and (
110 found_spec.loader is None or isinstance(found_spec.loader, NamespaceLoader)
111 )
112 )