Skip to content

Commit

Permalink
Add support for custom import hooks (#1752)
Browse files Browse the repository at this point in the history
Co-authored-by: Jacob Walls <[email protected]>
  • Loading branch information
DanielNoord and jacobtylerwalls authored Feb 5, 2023
1 parent ac41d4e commit bcaecce
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 13 deletions.
3 changes: 3 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ What's New in astroid 2.15.0?
=============================
Release date: TBA

* ``Astroid`` now supports custom import hooks.

Refs PyCQA/pylint#7306


What's New in astroid 2.14.2?
Expand Down
75 changes: 67 additions & 8 deletions astroid/interpreter/_import/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import os
import pathlib
import sys
import types
import zipimport
from collections.abc import Iterator, Sequence
from pathlib import Path
Expand All @@ -23,9 +24,21 @@
from . import util

if sys.version_info >= (3, 8):
from typing import Literal
from typing import Literal, Protocol
else:
from typing_extensions import Literal
from typing_extensions import Literal, Protocol


# 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):
Expand All @@ -43,6 +56,15 @@ class ModuleType(enum.Enum):
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,
}


class ModuleSpec(NamedTuple):
"""Defines a class similar to PEP 420's ModuleSpec.
Expand Down Expand Up @@ -122,8 +144,10 @@ def find_module(
try:
spec = importlib.util.find_spec(modname)
if (
spec and spec.loader is importlib.machinery.FrozenImporter
): # noqa: E501 # type: ignore[comparison-overlap]
spec
and spec.loader # type: ignore[comparison-overlap] # noqa: E501
is importlib.machinery.FrozenImporter
):
# No need for BuiltinImporter; builtins handled above
return ModuleSpec(
name=modname,
Expand Down Expand Up @@ -226,7 +250,6 @@ def __init__(self, path: Sequence[str]) -> None:
super().__init__(path)
for entry_path in path:
if entry_path not in sys.path_importer_cache:
# pylint: disable=no-member
try:
sys.path_importer_cache[entry_path] = zipimport.zipimporter( # type: ignore[assignment]
entry_path
Expand Down Expand Up @@ -310,7 +333,6 @@ def _is_setuptools_namespace(location: pathlib.Path) -> bool:

def _get_zipimporters() -> Iterator[tuple[str, zipimport.zipimporter]]:
for filepath, importer in sys.path_importer_cache.items():
# pylint: disable-next=no-member
if isinstance(importer, zipimport.zipimporter):
yield filepath, importer

Expand Down Expand Up @@ -349,7 +371,7 @@ def _find_spec_with_path(
module_parts: list[str],
processed: list[str],
submodule_path: Sequence[str] | None,
) -> tuple[Finder, ModuleSpec]:
) -> tuple[Finder | _MetaPathFinder, ModuleSpec]:
for finder in _SPEC_FINDERS:
finder_instance = finder(search_path)
spec = finder_instance.find_module(
Expand All @@ -359,6 +381,43 @@ def _find_spec_with_path(
continue
return finder_instance, 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__
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/PyCQA/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)}")


Expand Down Expand Up @@ -394,7 +453,7 @@ def find_spec(modpath: list[str], path: Sequence[str] | None = None) -> ModuleSp
_path, modname, module_parts, processed, submodule_path or path
)
processed.append(modname)
if modpath:
if modpath and isinstance(finder, Finder):
submodule_path = finder.contribute_to_path(spec, processed)

if spec.type == ModuleType.PKG_DIRECTORY:
Expand Down
6 changes: 1 addition & 5 deletions tests/unittest_modutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,11 +445,7 @@ def test_is_module_name_part_of_extension_package_whitelist_success(self) -> Non

@pytest.mark.skipif(not HAS_URLLIB3, reason="This test requires urllib3.")
def test_file_info_from_modpath__SixMetaPathImporter() -> None:
pytest.raises(
ImportError,
modutils.file_info_from_modpath,
["urllib3.packages.six.moves.http_client"],
)
assert modutils.file_info_from_modpath(["urllib3.packages.six.moves.http_client"])


if __name__ == "__main__":
Expand Down

0 comments on commit bcaecce

Please sign in to comment.