summaryrefslogtreecommitdiff
path: root/astroid/interpreter/_import/util.py
blob: 06afd192679d4902c871f84d3bad98fe75e35375 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE
# Copyright (c) https://github.com/pylint-dev/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  # type: ignore[attr-defined]

from astroid.const import IS_PYPY


@lru_cache(maxsize=4096)
def is_namespace(modname: str) -> bool:
    from astroid.modutils import (  # pylint: disable=import-outside-toplevel
        EXT_LIB_DIRS,
        STD_LIB_DIRS,
    )

    STD_AND_EXT_LIB_DIRS = STD_LIB_DIRS.union(EXT_LIB_DIRS)

    if modname in sys.builtin_module_names:
        return False

    found_spec = None

    # find_spec() attempts to import parent packages when given dotted paths.
    # 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.
    processed_components = []
    last_submodule_search_locations: _NamespacePath | None = None
    for component in modname.split("."):
        processed_components.append(component)
        working_modname = ".".join(processed_components)
        try:
            # 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 AttributeError:
            return False
        except ValueError:
            if modname == "__main__":
                return False
            try:
                # .pth files will be on sys.modules
                # __spec__ is set inconsistently on PyPy so we can't really on the heuristic here
                # See: https://foss.heptapod.net/pypy/pypy/-/issues/3736
                # Check first fragment of modname, e.g. "astroid", not "astroid.interpreter"
                # because of cffi's behavior
                # See: https://github.com/pylint-dev/astroid/issues/1776
                mod = sys.modules[processed_components[0]]
                return (
                    mod.__spec__ is None
                    and getattr(mod, "__file__", None) is None
                    and hasattr(mod, "__path__")
                    and not IS_PYPY
                )
            except KeyError:
                return False
            except AttributeError:
                # Workaround for "py" module
                # https://github.com/pytest-dev/apipkg/issues/13
                return False
        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 for next iteration
        if found_spec and found_spec.submodule_search_locations:
            # But immediately return False if we can detect we are in stdlib
            # or external lib (e.g site-packages)
            if any(
                any(location.startswith(lib_dir) for lib_dir in STD_AND_EXT_LIB_DIRS)
                for location in found_spec.submodule_search_locations
            ):
                return False
            last_submodule_search_locations = found_spec.submodule_search_locations

    return (
        found_spec is not None
        and found_spec.submodule_search_locations is not None
        and found_spec.origin is None
    )