Skip to content

Commit bcaecce

Browse files
Add support for custom import hooks (#1752)
Co-authored-by: Jacob Walls <jacobtylerwalls@gmail.com>
1 parent ac41d4e commit bcaecce

File tree

3 files changed

+71
-13
lines changed

3 files changed

+71
-13
lines changed

ChangeLog

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ What's New in astroid 2.15.0?
66
=============================
77
Release date: TBA
88

9+
* ``Astroid`` now supports custom import hooks.
10+
11+
Refs PyCQA/pylint#7306
912

1013

1114
What's New in astroid 2.14.2?

astroid/interpreter/_import/spec.py

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import os
1313
import pathlib
1414
import sys
15+
import types
1516
import zipimport
1617
from collections.abc import Iterator, Sequence
1718
from pathlib import Path
@@ -23,9 +24,21 @@
2324
from . import util
2425

2526
if sys.version_info >= (3, 8):
26-
from typing import Literal
27+
from typing import Literal, Protocol
2728
else:
28-
from typing_extensions import Literal
29+
from typing_extensions import Literal, Protocol
30+
31+
32+
# The MetaPathFinder protocol comes from typeshed, which says:
33+
# Intentionally omits one deprecated and one optional method of `importlib.abc.MetaPathFinder`
34+
class _MetaPathFinder(Protocol):
35+
def find_spec(
36+
self,
37+
fullname: str,
38+
path: Sequence[str] | None,
39+
target: types.ModuleType | None = ...,
40+
) -> importlib.machinery.ModuleSpec | None:
41+
... # pragma: no cover
2942

3043

3144
class ModuleType(enum.Enum):
@@ -43,6 +56,15 @@ class ModuleType(enum.Enum):
4356
PY_NAMESPACE = enum.auto()
4457

4558

59+
_MetaPathFinderModuleTypes: dict[str, ModuleType] = {
60+
# Finders created by setuptools editable installs
61+
"_EditableFinder": ModuleType.PY_SOURCE,
62+
"_EditableNamespaceFinder": ModuleType.PY_NAMESPACE,
63+
# Finders create by six
64+
"_SixMetaPathImporter": ModuleType.PY_SOURCE,
65+
}
66+
67+
4668
class ModuleSpec(NamedTuple):
4769
"""Defines a class similar to PEP 420's ModuleSpec.
4870
@@ -122,8 +144,10 @@ def find_module(
122144
try:
123145
spec = importlib.util.find_spec(modname)
124146
if (
125-
spec and spec.loader is importlib.machinery.FrozenImporter
126-
): # noqa: E501 # type: ignore[comparison-overlap]
147+
spec
148+
and spec.loader # type: ignore[comparison-overlap] # noqa: E501
149+
is importlib.machinery.FrozenImporter
150+
):
127151
# No need for BuiltinImporter; builtins handled above
128152
return ModuleSpec(
129153
name=modname,
@@ -226,7 +250,6 @@ def __init__(self, path: Sequence[str]) -> None:
226250
super().__init__(path)
227251
for entry_path in path:
228252
if entry_path not in sys.path_importer_cache:
229-
# pylint: disable=no-member
230253
try:
231254
sys.path_importer_cache[entry_path] = zipimport.zipimporter( # type: ignore[assignment]
232255
entry_path
@@ -310,7 +333,6 @@ def _is_setuptools_namespace(location: pathlib.Path) -> bool:
310333

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

@@ -349,7 +371,7 @@ def _find_spec_with_path(
349371
module_parts: list[str],
350372
processed: list[str],
351373
submodule_path: Sequence[str] | None,
352-
) -> tuple[Finder, ModuleSpec]:
374+
) -> tuple[Finder | _MetaPathFinder, ModuleSpec]:
353375
for finder in _SPEC_FINDERS:
354376
finder_instance = finder(search_path)
355377
spec = finder_instance.find_module(
@@ -359,6 +381,43 @@ def _find_spec_with_path(
359381
continue
360382
return finder_instance, spec
361383

384+
# Support for custom finders
385+
for meta_finder in sys.meta_path:
386+
# See if we support the customer import hook of the meta_finder
387+
meta_finder_name = meta_finder.__class__.__name__
388+
if meta_finder_name not in _MetaPathFinderModuleTypes:
389+
# Setuptools>62 creates its EditableFinders dynamically and have
390+
# "type" as their __class__.__name__. We check __name__ as well
391+
# to see if we can support the finder.
392+
try:
393+
meta_finder_name = meta_finder.__name__
394+
except AttributeError:
395+
continue
396+
if meta_finder_name not in _MetaPathFinderModuleTypes:
397+
continue
398+
399+
module_type = _MetaPathFinderModuleTypes[meta_finder_name]
400+
401+
# Meta path finders are supposed to have a find_spec method since
402+
# Python 3.4. However, some third-party finders do not implement it.
403+
# PEP302 does not refer to find_spec as well.
404+
# See: https://github.com/PyCQA/astroid/pull/1752/
405+
if not hasattr(meta_finder, "find_spec"):
406+
continue
407+
408+
spec = meta_finder.find_spec(modname, submodule_path)
409+
if spec:
410+
return (
411+
meta_finder,
412+
ModuleSpec(
413+
spec.name,
414+
module_type,
415+
spec.origin,
416+
spec.origin,
417+
spec.submodule_search_locations,
418+
),
419+
)
420+
362421
raise ImportError(f"No module named {'.'.join(module_parts)}")
363422

364423

@@ -394,7 +453,7 @@ def find_spec(modpath: list[str], path: Sequence[str] | None = None) -> ModuleSp
394453
_path, modname, module_parts, processed, submodule_path or path
395454
)
396455
processed.append(modname)
397-
if modpath:
456+
if modpath and isinstance(finder, Finder):
398457
submodule_path = finder.contribute_to_path(spec, processed)
399458

400459
if spec.type == ModuleType.PKG_DIRECTORY:

tests/unittest_modutils.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -445,11 +445,7 @@ def test_is_module_name_part_of_extension_package_whitelist_success(self) -> Non
445445

446446
@pytest.mark.skipif(not HAS_URLLIB3, reason="This test requires urllib3.")
447447
def test_file_info_from_modpath__SixMetaPathImporter() -> None:
448-
pytest.raises(
449-
ImportError,
450-
modutils.file_info_from_modpath,
451-
["urllib3.packages.six.moves.http_client"],
452-
)
448+
assert modutils.file_info_from_modpath(["urllib3.packages.six.moves.http_client"])
453449

454450

455451
if __name__ == "__main__":

0 commit comments

Comments
 (0)