Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix setuptools>=64 import hooks #1752

Merged
merged 9 commits into from
Feb 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
jacobtylerwalls marked this conversation as resolved.
Show resolved Hide resolved
if spec:
return (
meta_finder,
ModuleSpec(
spec.name,
module_type,
spec.origin,
jacobtylerwalls marked this conversation as resolved.
Show resolved Hide resolved
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):
jacobtylerwalls marked this conversation as resolved.
Show resolved Hide resolved
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