Skip to content

Commit

Permalink
feat: Support setuptools editable modules
Browse files Browse the repository at this point in the history
  • Loading branch information
pawamoy committed Dec 4, 2022
1 parent a77e4e8 commit abc18f7
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 19 deletions.
2 changes: 1 addition & 1 deletion src/griffe/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class NameResolutionError(GriffeError):
"""Exception for names that cannot be resolved in a object scope."""


class UnhandledEditablesModuleError(GriffeError):
class UnhandledEditableModuleError(GriffeError):
"""Exception for unhandled editables modules, when searching modules."""


Expand Down
46 changes: 29 additions & 17 deletions src/griffe/finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from typing import Iterator, Sequence, Tuple

from griffe.dataclasses import Module
from griffe.exceptions import UnhandledEditablesModuleError
from griffe.exceptions import UnhandledEditableModuleError
from griffe.logger import get_logger

NamePartsType = Tuple[str, ...]
Expand Down Expand Up @@ -66,7 +66,7 @@ def __init__(self, search_paths: Sequence[str | Path] | None = None) -> None:
if bool(search_paths):
# without custom search paths, sys.path is used, and is already extended from .pth files
self._extend_from_pth_files()
self._extend_from_editables_modules()
self._extend_from_editable_modules()

def find_spec(
self,
Expand Down Expand Up @@ -264,12 +264,12 @@ def _extend_from_pth_files(self):
for directory in _handle_pth_file(item):
self._append_search_path(directory)

def _extend_from_editables_modules(self):
def _extend_from_editable_modules(self):
for path in self.search_paths: # noqa: WPS440
for item in self._contents(path):
if item.stem.startswith("__editables_") and item.suffix == ".py":
with suppress(UnhandledEditablesModuleError):
self._append_search_path(_handle_editables_module(item))
if item.stem.startswith(("__editables_", "__editable__")) and item.suffix == ".py":
with suppress(UnhandledEditableModuleError):
self._append_search_path(_handle_editable_module(item))

def _filter_py_modules(self, path: Path) -> Iterator[Path]:
for root, dirs, files in os.walk(path, topdown=True):
Expand Down Expand Up @@ -299,6 +299,7 @@ def _top_module_name(self, path: Path) -> str:
_re_pkgresources = re.compile(r"(?:__import__\([\"']pkg_resources[\"']\).declare_namespace\(__name__\))")
_re_pkgutil = re.compile(r"(?:__path__ = __import__\([\"']pkgutil[\"']\).extend_path\(__path__, __name__\))")
_re_import_line = re.compile(r"^import[ \t]")
_re_mapping_line = re.compile(r"^MAPPING = \{['\"].+['\"]: +['\"](.+)['\"]\}")


# TODO: for better robustness, we should load and minify the AST
Expand Down Expand Up @@ -332,16 +333,27 @@ def _handle_pth_file(path) -> list[Path]: # noqa: WPS231
return directories


def _handle_editables_module(path: Path):
def _handle_editable_module(path: Path): # noqa: WPS231
try:
editables_lines = path.read_text(encoding="utf8").splitlines(keepends=False)
editable_lines = path.read_text(encoding="utf8").splitlines(keepends=False)
except FileNotFoundError:
raise UnhandledEditablesModuleError(path)
# example line: F.map_module('griffe', '/media/data/dev/griffe/src/griffe/__init__.py')
# TODO: write something more robust
new_path = Path(editables_lines[-1].split("'")[3])
if new_path.exists():
if new_path.name.startswith("__init__"):
return new_path.parent.parent
return new_path
raise UnhandledEditablesModuleError(path)
raise UnhandledEditableModuleError(path)
if path.name.startswith("__editables_"):
# support for how 'editables' writes these files:
# example line: F.map_module('griffe', '/media/data/dev/griffe/src/griffe/__init__.py')
new_path = Path(editable_lines[-1].split("'")[3])
if new_path.exists(): # TODO: could remove existence check
if new_path.name.startswith("__init__"):
return new_path.parent.parent
return new_path
elif path.name.startswith("__editable__"):
# support for how 'setuptools' writes these files:
# example line: MAPPING = {'griffe': '/media/data/dev/griffe/src/griffe'}
for line in editable_lines:
match = _re_mapping_line.match(line)
if match:
new_path = Path(match.group(1))
if new_path.exists(): # TODO: could remove existence check
return new_path.parent
break
raise UnhandledEditableModuleError(path)
24 changes: 23 additions & 1 deletion tests/test_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import pytest

from griffe.finder import ModuleFinder, NamespacePackage, _handle_pth_file # noqa: WPS450
from griffe.finder import ModuleFinder, NamespacePackage, _handle_editable_module, _handle_pth_file # noqa: WPS450
from tests.helpers import temporary_pypackage


Expand Down Expand Up @@ -84,3 +84,25 @@ def test_pth_file_handling(tmp_path):
)
directories = _handle_pth_file(pth_file)
assert directories == [Path("tests")]


def test_editables_file_handling(tmp_path):
"""Assert editable modules by `editables` are handled.
Parameters:
tmp_path: Pytest fixture.
"""
pth_file = tmp_path / "__editables_whatever.py"
pth_file.write_text("hello\nF.map_module('griffe', 'src/griffe/__init__.py')")
assert _handle_editable_module(pth_file) == Path("src")


def test_setuptools_file_handling(tmp_path):
"""Assert editable modules by `setuptools` are handled.
Parameters:
tmp_path: Pytest fixture.
"""
pth_file = tmp_path / "__editable__whatever.py"
pth_file.write_text("hello\nMAPPING = {'griffe': 'src/griffe'}")
assert _handle_editable_module(pth_file) == Path("src")

0 comments on commit abc18f7

Please sign in to comment.