Skip to content

Commit

Permalink
fix: Support (setuptools) editable packages with multiple roots
Browse files Browse the repository at this point in the history
Python packages may have multiple root modules where
each is installed to its own folder under `site-packages`.

When using the new setuptools editable install, in such cases
Griffe, when used by mkdocstrings fails to find the correct paths.

The root cause it that Griffe assumes a variable named
`MAPPING` in the `.py` file created by the editable install,
and assumes that this variable is a dict with single entry.

When a package has multiple roots - then this dictionary
contains multiple entries.

This commit aims to handle such cases.

The unit test added simulates the case, and this has been tested
against a real complex package with multiple roots using setuptools
65 editable install.

Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me>
  • Loading branch information
gilfree and pawamoy authored Jan 18, 2023
1 parent 34bc108 commit bd37dfb
Show file tree
Hide file tree
Showing 2 changed files with 30 additions and 15 deletions.
28 changes: 15 additions & 13 deletions src/griffe/finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import ast
import os
import re
import sys
Expand Down Expand Up @@ -264,12 +265,13 @@ def _extend_from_pth_files(self):
for directory in _handle_pth_file(item):
self._append_search_path(directory)

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

def _filter_py_modules(self, path: Path) -> Iterator[Path]:
for root, dirs, files in os.walk(path, topdown=True):
Expand Down Expand Up @@ -299,7 +301,6 @@ 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 @@ -344,16 +345,17 @@ def _handle_editable_module(path: Path): # noqa: WPS231
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
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
# example line: MAPPING = {'griffe': '/media/data/dev/griffe/src/griffe', 'briffe': '/media/data/dev/griffe/src/briffe'}
parsed_module = ast.parse(path.read_text())
for node in parsed_module.body:
if isinstance(node, ast.Assign):
if isinstance(node.targets[0], ast.Name) and node.targets[0].id == "MAPPING":
if isinstance(node.value, ast.Dict):
return [
Path(constant.s).parent for constant in node.value.values if isinstance(constant, ast.Str)
]
raise UnhandledEditableModuleError(path)
17 changes: 15 additions & 2 deletions tests/test_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def test_editables_file_handling(tmp_path):
"""
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")
assert _handle_editable_module(pth_file) == [Path("src")]


def test_setuptools_file_handling(tmp_path):
Expand All @@ -105,4 +105,17 @@ def test_setuptools_file_handling(tmp_path):
"""
pth_file = tmp_path / "__editable__whatever.py"
pth_file.write_text("hello\nMAPPING = {'griffe': 'src/griffe'}")
assert _handle_editable_module(pth_file) == Path("src")
assert _handle_editable_module(pth_file) == [Path("src")]


def test_setuptools_file_handling_multiple_paths(tmp_path):
"""Assert editable modules by `setuptools` are handled when multiple packages are installed in the same editable.
Parameters:
tmp_path: Pytest fixture.
"""
pth_file = tmp_path / "__editable__whatever.py"
pth_file.write_text(
"hello=1\nMAPPING = {\n'griffe':\n 'src1/griffe', 'briffe':'src2/briffe'}\ndef printer():\n print(hello)"
)
assert _handle_editable_module(pth_file) == [Path("src1"), Path("src2")]

0 comments on commit bd37dfb

Please sign in to comment.