Skip to content

Commit 64625c2

Browse files
committed
ENH: add support for editable installs
Signed-off-by: Filipe Laíns <[email protected]>
1 parent d098706 commit 64625c2

File tree

2 files changed

+91
-0
lines changed

2 files changed

+91
-0
lines changed

mesonpy/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,9 @@ def wheel(self, directory: Path) -> pathlib.Path: # noqa: F811
918918
shutil.move(os.fspath(wheel), final_wheel)
919919
return final_wheel
920920

921+
def editable(self, directory: Path) -> pathlib.Path:
922+
raise NotImplementedError
923+
921924

922925
@contextlib.contextmanager
923926
def _project(config_settings: Optional[Dict[Any, Any]]) -> Iterator[Project]:
@@ -1054,3 +1057,21 @@ def build_wheel(
10541057
out = pathlib.Path(wheel_directory)
10551058
with _project(config_settings) as project:
10561059
return project.wheel(out).name
1060+
1061+
1062+
def build_editable(
1063+
wheel_directory: str,
1064+
config_settings: Optional[Dict[Any, Any]] = None,
1065+
metadata_directory: Optional[str] = None,
1066+
) -> str:
1067+
_setup_cli()
1068+
1069+
out = pathlib.Path(wheel_directory)
1070+
with _project(config_settings) as project:
1071+
return project.editable(out).name
1072+
1073+
1074+
def get_requires_for_build_editable(
1075+
config_settings: Optional[Dict[str, str]] = None,
1076+
) -> List[str]:
1077+
return get_requires_for_build_wheel()

mesonpy/_editable.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import functools
2+
import importlib.abc
3+
import os
4+
import subprocess
5+
import sys
6+
7+
from types import ModuleType
8+
from typing import Optional, Union
9+
10+
from mesonpy._compat import Collection, Sequence
11+
12+
13+
class MesonpyFinder(importlib.abc.MetaPathFinder):
14+
"""Custom loader that whose purpose is to detect when the import system is
15+
trying to load our modules, and trigger a rebuild. After triggering a
16+
rebuild, we return None in find_spec, letting the normal finders pick up the
17+
modules.
18+
"""
19+
20+
def __init__(
21+
self,
22+
project_path: str,
23+
top_level_modules: Collection[str],
24+
rebuild_commands: Sequence[Sequence[str]],
25+
) -> None:
26+
if not os.path.isabs(project_path):
27+
raise ImportError('Project path must be absolute')
28+
self._project_path = project_path
29+
self._top_level_modules = top_level_modules
30+
self._rebuild_commands = rebuild_commands
31+
32+
def __repr__(self) -> str:
33+
return f'{self.__class__}({self._project_path})'
34+
35+
def __hash__(self) -> int:
36+
return hash((self._project_path, self._top_level_modules, self._rebuild_commands))
37+
38+
@functools.lru_cache(maxsize=1)
39+
def rebuild(self) -> None:
40+
for command in self._rebuild_commands:
41+
subprocess.check_call(command)
42+
43+
def find_spec(
44+
self,
45+
fullname: str,
46+
path: Optional[Sequence[Union[str, bytes]]],
47+
target: Optional[ModuleType] = None,
48+
) -> None:
49+
# if it's one of our modules, trigger a rebuild
50+
if fullname.split('.', maxsplit=1)[0] in self._top_level_modules:
51+
self.rebuild()
52+
# return none (meaning we "didn't find" the module) and let the normal
53+
# finders find/import it
54+
return None
55+
56+
@classmethod
57+
def install(
58+
cls,
59+
project_path: str,
60+
top_level_modules: Collection[str],
61+
rebuild_commands: Sequence[Sequence[str]],
62+
) -> None:
63+
finder = cls(project_path, top_level_modules, rebuild_commands)
64+
if finder not in sys.meta_path:
65+
# prepend our finder to sys.meta_path, so that it is queried before
66+
# the normal finders, and can trigger a project rebuild
67+
sys.meta_path.insert(0, finder)
68+
# add the project path to sys.path, so that the normal finder can
69+
# find our modules
70+
sys.path.append(project_path)

0 commit comments

Comments
 (0)