| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496 |
- # 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 abc
- import enum
- import importlib
- import importlib.machinery
- import importlib.util
- import os
- import pathlib
- import sys
- import types
- import warnings
- import zipimport
- from collections.abc import Iterable, Iterator, Sequence
- from functools import lru_cache
- from pathlib import Path
- from typing import Literal, NamedTuple, Protocol
- from . import util
- # The MetaPathFinder protocol comes from typeshed, which says:
- # Intentionally omits one deprecated and one optional method of `importlib.abc.MetaPathFinder`
- class _MetaPathFinder(Protocol):
- def find_spec(
- self,
- fullname: str,
- path: Sequence[str] | None,
- target: types.ModuleType | None = ...,
- ) -> importlib.machinery.ModuleSpec | None: ... # pragma: no cover
- class ModuleType(enum.Enum):
- """Python module types used for ModuleSpec."""
- C_BUILTIN = enum.auto()
- C_EXTENSION = enum.auto()
- PKG_DIRECTORY = enum.auto()
- PY_CODERESOURCE = enum.auto()
- PY_COMPILED = enum.auto()
- PY_FROZEN = enum.auto()
- PY_RESOURCE = enum.auto()
- PY_SOURCE = enum.auto()
- PY_ZIPMODULE = enum.auto()
- PY_NAMESPACE = enum.auto()
- _MetaPathFinderModuleTypes: dict[str, ModuleType] = {
- # Finders created by setuptools editable installs
- "_EditableFinder": ModuleType.PY_SOURCE,
- "_EditableNamespaceFinder": ModuleType.PY_NAMESPACE,
- # Finders create by six
- "_SixMetaPathImporter": ModuleType.PY_SOURCE,
- }
- _EditableFinderClasses: set[str] = {
- "_EditableFinder",
- "_EditableNamespaceFinder",
- }
- class ModuleSpec(NamedTuple):
- """Defines a class similar to PEP 420's ModuleSpec.
- A module spec defines a name of a module, its type, location
- and where submodules can be found, if the module is a package.
- """
- name: str
- type: ModuleType | None
- location: str | None = None
- origin: str | None = None
- submodule_search_locations: Sequence[str] | None = None
- class Finder:
- """A finder is a class which knows how to find a particular module."""
- def __init__(self, path: Sequence[str] | None = None) -> None:
- self._path = path or sys.path
- @staticmethod
- @abc.abstractmethod
- def find_module(
- modname: str,
- module_parts: tuple[str, ...],
- processed: tuple[str, ...],
- submodule_path: tuple[str, ...] | None,
- ) -> ModuleSpec | None:
- """Find the given module.
- Each finder is responsible for each protocol of finding, as long as
- they all return a ModuleSpec.
- :param modname: The module which needs to be searched.
- :param module_parts: It should be a tuple of strings,
- where each part contributes to the module's
- namespace.
- :param processed: What parts from the module parts were processed
- so far.
- :param submodule_path: A tuple of paths where the module
- can be looked into.
- :returns: A ModuleSpec, describing how and where the module was found,
- None, otherwise.
- """
- def contribute_to_path(
- self, spec: ModuleSpec, processed: list[str]
- ) -> Sequence[str] | None:
- """Get a list of extra paths where this finder can search."""
- class ImportlibFinder(Finder):
- """A finder based on the importlib module."""
- _SUFFIXES: Sequence[tuple[str, ModuleType]] = (
- [(s, ModuleType.C_EXTENSION) for s in importlib.machinery.EXTENSION_SUFFIXES]
- + [(s, ModuleType.PY_SOURCE) for s in importlib.machinery.SOURCE_SUFFIXES]
- + [(s, ModuleType.PY_COMPILED) for s in importlib.machinery.BYTECODE_SUFFIXES]
- )
- @staticmethod
- @lru_cache(maxsize=1024)
- def find_module(
- modname: str,
- module_parts: tuple[str, ...],
- processed: tuple[str, ...],
- submodule_path: tuple[str, ...] | None,
- ) -> ModuleSpec | None:
- # pylint: disable-next=import-outside-toplevel
- from astroid.modutils import cached_os_path_isfile
- # Although we should be able to use `find_spec` this doesn't work on PyPy for builtins.
- # Therefore, we use the `builtin_module_nams` heuristic for these.
- if submodule_path is None and modname in sys.builtin_module_names:
- return ModuleSpec(
- name=modname,
- location=None,
- type=ModuleType.C_BUILTIN,
- )
- if submodule_path is not None:
- search_paths = list(submodule_path)
- else:
- search_paths = sys.path
- suffixes = (".py", ".pyi", importlib.machinery.BYTECODE_SUFFIXES[0])
- for entry in search_paths:
- package_directory = os.path.join(entry, modname)
- for suffix in suffixes:
- package_file_name = "__init__" + suffix
- file_path = os.path.join(package_directory, package_file_name)
- if cached_os_path_isfile(file_path):
- return ModuleSpec(
- name=modname,
- location=package_directory,
- type=ModuleType.PKG_DIRECTORY,
- )
- for suffix, type_ in ImportlibFinder._SUFFIXES:
- file_name = modname + suffix
- file_path = os.path.join(entry, file_name)
- if cached_os_path_isfile(file_path):
- return ModuleSpec(name=modname, location=file_path, type=type_)
- # If the module name matches a stdlib module name, check whether this is a frozen
- # module. Note that `find_spec` actually imports parent modules, so we want to make
- # sure we only run this code for stuff that can be expected to be frozen. For now
- # this is only stdlib.
- if (modname in sys.stdlib_module_names and not processed) or (
- processed and processed[0] in sys.stdlib_module_names
- ):
- try:
- with warnings.catch_warnings():
- warnings.filterwarnings("ignore", category=Warning)
- spec = importlib.util.find_spec(".".join((*processed, modname)))
- except ValueError:
- spec = None
- if (
- spec
- and spec.loader # type: ignore[comparison-overlap] # noqa: E501
- is importlib.machinery.FrozenImporter
- ):
- return ModuleSpec(
- name=modname,
- location=getattr(spec.loader_state, "filename", None),
- type=ModuleType.PY_FROZEN,
- )
- return None
- def contribute_to_path(
- self, spec: ModuleSpec, processed: list[str]
- ) -> Sequence[str] | None:
- if spec.location is None:
- # Builtin.
- return None
- # pylint: disable-next=import-outside-toplevel
- from astroid.modutils import EXT_LIB_DIRS
- if _is_setuptools_namespace(Path(spec.location)):
- # extend_path is called, search sys.path for module/packages
- # of this name see pkgutil.extend_path documentation
- path = [
- os.path.join(p, *processed)
- for p in sys.path
- if os.path.isdir(os.path.join(p, *processed))
- ]
- elif spec.name == "distutils" and not any(
- spec.location.lower().startswith(ext_lib_dir.lower())
- for ext_lib_dir in EXT_LIB_DIRS
- ):
- # virtualenv below 20.0 patches distutils in an unexpected way
- # so we just find the location of distutils that will be
- # imported to avoid spurious import-error messages
- # https://github.com/pylint-dev/pylint/issues/5645
- # A regression test to create this scenario exists in release-tests.yml
- # and can be triggered manually from GitHub Actions
- distutils_spec = importlib.util.find_spec("distutils")
- if distutils_spec and distutils_spec.origin:
- origin_path = Path(
- distutils_spec.origin
- ) # e.g. .../distutils/__init__.py
- path = [str(origin_path.parent)] # e.g. .../distutils
- else:
- path = [spec.location]
- else:
- path = [spec.location]
- return path
- class ExplicitNamespacePackageFinder(ImportlibFinder):
- """A finder for the explicit namespace packages."""
- @staticmethod
- @lru_cache(maxsize=1024)
- def find_module(
- modname: str,
- module_parts: tuple[str, ...],
- processed: tuple[str, ...],
- submodule_path: tuple[str, ...] | None,
- ) -> ModuleSpec | None:
- if processed:
- modname = ".".join([*processed, modname])
- if util.is_namespace(modname) and modname in sys.modules:
- return ModuleSpec(
- name=modname,
- location="",
- origin="namespace",
- type=ModuleType.PY_NAMESPACE,
- submodule_search_locations=sys.modules[modname].__path__,
- )
- return None
- def contribute_to_path(
- self, spec: ModuleSpec, processed: list[str]
- ) -> Sequence[str] | None:
- return spec.submodule_search_locations
- class ZipFinder(Finder):
- """Finder that knows how to find a module inside zip files."""
- def __init__(self, path: Sequence[str]) -> None:
- super().__init__(path)
- for entry_path in path:
- if entry_path not in sys.path_importer_cache:
- try:
- sys.path_importer_cache[entry_path] = zipimport.zipimporter(
- entry_path
- )
- except zipimport.ZipImportError:
- continue
- @staticmethod
- @lru_cache(maxsize=1024)
- def find_module(
- modname: str,
- module_parts: tuple[str, ...],
- processed: tuple[str, ...],
- submodule_path: tuple[str, ...] | None,
- ) -> ModuleSpec | None:
- try:
- file_type, filename, path = _search_zip(module_parts)
- except ImportError:
- return None
- return ModuleSpec(
- name=modname,
- location=filename,
- origin="egg",
- type=file_type,
- submodule_search_locations=path,
- )
- def contribute_to_path(
- self, spec: ModuleSpec, processed: list[str]
- ) -> Sequence[str] | None:
- return spec.submodule_search_locations
- class PathSpecFinder(Finder):
- """Finder based on importlib.machinery.PathFinder."""
- @staticmethod
- @lru_cache(maxsize=1024)
- def find_module(
- modname: str,
- module_parts: tuple[str, ...],
- processed: tuple[str, ...],
- submodule_path: tuple[str, ...] | None,
- ) -> ModuleSpec | None:
- spec = importlib.machinery.PathFinder.find_spec(modname, path=submodule_path)
- if spec is not None:
- is_namespace_pkg = spec.origin is None
- location = spec.origin if not is_namespace_pkg else None
- module_type = ModuleType.PY_NAMESPACE if is_namespace_pkg else None
- return ModuleSpec(
- name=spec.name,
- location=location,
- origin=spec.origin,
- type=module_type,
- submodule_search_locations=list(spec.submodule_search_locations or []),
- )
- return spec
- def contribute_to_path(
- self, spec: ModuleSpec, processed: list[str]
- ) -> Sequence[str] | None:
- if spec.type == ModuleType.PY_NAMESPACE:
- return spec.submodule_search_locations
- return None
- _SPEC_FINDERS = (
- ImportlibFinder,
- ZipFinder,
- PathSpecFinder,
- ExplicitNamespacePackageFinder,
- )
- @lru_cache(maxsize=1024)
- def _is_setuptools_namespace(location: pathlib.Path) -> bool:
- try:
- with open(location / "__init__.py", "rb") as stream:
- data = stream.read(4096)
- except OSError:
- return False
- extend_path = b"pkgutil" in data and b"extend_path" in data
- declare_namespace = (
- b"pkg_resources" in data and b"declare_namespace(__name__)" in data
- )
- return extend_path or declare_namespace
- def _get_zipimporters() -> Iterator[tuple[str, zipimport.zipimporter]]:
- for filepath, importer in sys.path_importer_cache.items():
- if importer is not None and isinstance(importer, zipimport.zipimporter):
- yield filepath, importer
- def _search_zip(
- modpath: tuple[str, ...],
- ) -> tuple[Literal[ModuleType.PY_ZIPMODULE], str, str]:
- for filepath, importer in _get_zipimporters():
- found = importer.find_spec(modpath[0])
- if found:
- if not importer.find_spec(os.path.sep.join(modpath)):
- raise ImportError(
- "No module named {} in {}/{}".format(
- ".".join(modpath[1:]), filepath, modpath
- )
- )
- return (
- ModuleType.PY_ZIPMODULE,
- os.path.abspath(filepath) + os.path.sep + os.path.sep.join(modpath),
- filepath,
- )
- raise ImportError(f"No module named {'.'.join(modpath)}")
- def _find_spec_with_path(
- search_path: Sequence[str],
- modname: str,
- module_parts: tuple[str, ...],
- processed: tuple[str, ...],
- submodule_path: tuple[str, ...] | None,
- ) -> tuple[Finder | _MetaPathFinder, ModuleSpec]:
- for finder in _SPEC_FINDERS:
- finder_instance = finder(search_path)
- mod_spec = finder.find_module(modname, module_parts, processed, submodule_path)
- if mod_spec is None:
- continue
- return finder_instance, mod_spec
- # Support for custom finders
- for meta_finder in sys.meta_path:
- # See if we support the customer import hook of the meta_finder
- meta_finder_name = meta_finder.__class__.__name__
- if meta_finder_name not in _MetaPathFinderModuleTypes:
- # Setuptools>62 creates its EditableFinders dynamically and have
- # "type" as their __class__.__name__. We check __name__ as well
- # to see if we can support the finder.
- try:
- meta_finder_name = meta_finder.__name__ # type: ignore[attr-defined]
- except AttributeError:
- continue
- if meta_finder_name not in _MetaPathFinderModuleTypes:
- continue
- module_type = _MetaPathFinderModuleTypes[meta_finder_name]
- # Meta path finders are supposed to have a find_spec method since
- # Python 3.4. However, some third-party finders do not implement it.
- # PEP302 does not refer to find_spec as well.
- # See: https://github.com/pylint-dev/astroid/pull/1752/
- if not hasattr(meta_finder, "find_spec"):
- continue
- spec = meta_finder.find_spec(modname, submodule_path)
- if spec:
- return (
- meta_finder,
- ModuleSpec(
- spec.name,
- module_type,
- spec.origin,
- spec.origin,
- spec.submodule_search_locations,
- ),
- )
- raise ImportError(f"No module named {'.'.join(module_parts)}")
- def find_spec(modpath: Iterable[str], path: Iterable[str] | None = None) -> ModuleSpec:
- """Find a spec for the given module.
- :type modpath: list or tuple
- :param modpath:
- split module's name (i.e name of a module or package split
- on '.'), with leading empty strings for explicit relative import
- :type path: list or None
- :param path:
- optional list of path where the module or package should be
- searched (use sys.path if nothing or None is given)
- :rtype: ModuleSpec
- :return: A module spec, which describes how the module was
- found and where.
- """
- return _find_spec(tuple(modpath), tuple(path) if path else None)
- @lru_cache(maxsize=1024)
- def _find_spec(
- module_path: tuple[str, ...], path: tuple[str, ...] | None
- ) -> ModuleSpec:
- _path = path or sys.path
- # Need a copy for not mutating the argument.
- modpath = list(module_path)
- search_paths = None
- processed: list[str] = []
- while modpath:
- modname = modpath.pop(0)
- submodule_path = search_paths or path
- if submodule_path is not None:
- submodule_path = tuple(submodule_path)
- finder, spec = _find_spec_with_path(
- _path, modname, module_path, tuple(processed), submodule_path
- )
- processed.append(modname)
- if modpath:
- if isinstance(finder, Finder):
- search_paths = finder.contribute_to_path(spec, processed)
- # If modname is a package from an editable install, update search_paths
- # so that the next module in the path will be found inside of it using importlib.
- # Existence of __name__ is guaranteed by _find_spec_with_path.
- elif finder.__name__ in _EditableFinderClasses: # type: ignore[attr-defined]
- search_paths = spec.submodule_search_locations
- if spec.type == ModuleType.PKG_DIRECTORY:
- spec = spec._replace(submodule_search_locations=search_paths)
- return spec
|