Skip to content

Commit 034d9d1

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

File tree

2 files changed

+177
-0
lines changed

2 files changed

+177
-0
lines changed

mesonpy/__init__.py

+21
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

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import importlib.abc
2+
import importlib.machinery
3+
import importlib.util
4+
import io
5+
import itertools
6+
import os
7+
import pathlib
8+
import sys
9+
10+
from types import ModuleType
11+
from typing import Optional, Union
12+
13+
from mesonpy._compat import Iterator, Sequence
14+
15+
16+
if sys.version_info >= (3, 11):
17+
import importlib.resources.abc as resources_abc
18+
elif sys.version_info >= (3, 9):
19+
import importlib.abc as resources_abc
20+
else:
21+
import importlib_resources.abc as resources_abc
22+
23+
24+
StrPath = Union[str, os.PathLike[str]]
25+
26+
27+
class Editable:
28+
def __init__(self, source_dir: str) -> None:
29+
self._source_dir = source_dir
30+
31+
@property
32+
def source_dir(self) -> str:
33+
return self._source_dir
34+
35+
36+
class EditablePath(resources_abc.Traversable): # type: ignore[misc]
37+
def __init__(self, project: Editable) -> None:
38+
self._project = project
39+
40+
def iterdir(self) -> Iterator[resources_abc.Traversable]:
41+
raise NotImplementedError
42+
43+
def is_dir(self) -> bool:
44+
raise NotImplementedError
45+
46+
def is_file(self) -> bool:
47+
raise NotImplementedError
48+
49+
def open(
50+
self,
51+
mode: str = 'r',
52+
buffering: int = ...,
53+
encoding: Optional[str] = ...,
54+
errors: Optional[str] = ...,
55+
newline: Optional[str] = ...,
56+
) -> io.TextIOWrapper:
57+
raise NotImplementedError
58+
59+
def name(self) -> str:
60+
raise NotImplementedError
61+
62+
# backport from 3.10
63+
64+
def read_bytes(self) -> bytes:
65+
"""
66+
Read contents of self as bytes
67+
"""
68+
with self.open('rb') as strm:
69+
return strm.read() # type: ignore[return-value]
70+
71+
def read_text(self, encoding: Optional[str] = None) -> str:
72+
"""
73+
Read contents of self as text
74+
"""
75+
with self.open(encoding=encoding) as strm:
76+
return strm.read()
77+
78+
def __truediv__(self, child: StrPath) -> resources_abc.Traversable:
79+
"""
80+
Return Traversable child in self
81+
"""
82+
return self.joinpath(child)
83+
84+
# backport from 3.12
85+
86+
def joinpath(self, *descendants: StrPath) -> resources_abc.Traversable:
87+
if not descendants:
88+
return self
89+
names = itertools.chain.from_iterable(
90+
path.parts for path in map(pathlib.PurePosixPath, descendants)
91+
)
92+
target = next(names)
93+
matches = (
94+
traversable
95+
for traversable in self.iterdir()
96+
if traversable.name == target
97+
)
98+
try:
99+
match = next(matches)
100+
except StopIteration:
101+
raise ImportError(
102+
'Target not found during traversal.', target, list(names)
103+
)
104+
return match.joinpath(*names)
105+
106+
107+
class EditableReader(resources_abc.TraversableResources): # type: ignore[misc]
108+
def __init__(self, path: EditablePath) -> None:
109+
self._path = path
110+
111+
def files(self) -> resources_abc.Traversable:
112+
return self._path
113+
114+
115+
class EditableLoader(importlib.machinery.SourceFileLoader):
116+
def __init__(self, fullname: str, path: EditablePath) -> None:
117+
super().__init__(fullname, fullname)
118+
self._fullname = fullname
119+
self._path = path
120+
121+
def __repr__(self) -> str:
122+
return f'{self.__class__}({self._path})'
123+
124+
def get_data(self, path: Union[str, bytes]) -> bytes:
125+
if path != self._fullname:
126+
raise ImportError(f'Requesting data from unexpected path `{path!r}` in {self}')
127+
return self._path.read_bytes()
128+
129+
def get_resource_reader(self) -> importlib.abc.ResourceReader:
130+
return EditableReader(self._path)
131+
132+
133+
class MesonpyFinder(importlib.abc.MetaPathFinder): # remove instantiation?
134+
def __init__(self, editable: Editable) -> None:
135+
self._editable = editable
136+
self._base_path = EditablePath(self._editable)
137+
138+
def __repr__(self) -> str:
139+
return f'{self.__class__}({self._editable})'
140+
141+
def find_spec(
142+
self,
143+
fullname: str,
144+
path: Optional[Sequence[Union[str, bytes]]],
145+
target: Optional[ModuleType] = None,
146+
) -> Optional[importlib.machinery.ModuleSpec]:
147+
if not fullname.startswith('aaaa'):
148+
return None
149+
spec_path = self._base_path.joinpath(*fullname.split('.'))
150+
return importlib.util.spec_from_loader(fullname, EditableLoader(fullname, spec_path))
151+
152+
153+
def install(path: str) -> None:
154+
editable = Editable(os.path.abspath(path))
155+
sys.meta_path.append(MesonpyFinder(editable))
156+
print('mesonpy import hook installed')

0 commit comments

Comments
 (0)