Skip to content

Commit 95da73a

Browse files
committed
fix: vendor editables
Signed-off-by: Frost Ming <me@frostming.com>
1 parent 3850578 commit 95da73a

File tree

8 files changed

+171
-4
lines changed

8 files changed

+171
-4
lines changed

src/pdm/backend/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def get_requires_for_build_editable(
7777
When C-extension build is needed, setuptools should be required, otherwise
7878
just return an empty list.
7979
"""
80-
return get_requires_for_build_wheel(config_settings) + ["editables"]
80+
return get_requires_for_build_wheel(config_settings)
8181

8282

8383
def prepare_metadata_for_build_editable(
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Copyright (c) 2020 Paul Moore
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of
4+
this software and associated documentation files (the "Software"), to deal in
5+
the Software without restriction, including without limitation the rights to
6+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7+
the Software, and to permit persons to whom the Software is furnished to do so,
8+
subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import os
2+
import re
3+
from pathlib import Path
4+
from typing import Dict, Iterable, List, Tuple, Union
5+
6+
__all__ = (
7+
"EditableProject",
8+
"__version__",
9+
)
10+
11+
__version__ = "0.5"
12+
13+
14+
# Check if a project name is valid, based on PEP 426:
15+
# https://peps.python.org/pep-0426/#name
16+
def is_valid(name: str) -> bool:
17+
return (
18+
re.match(r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", name, re.IGNORECASE)
19+
is not None
20+
)
21+
22+
23+
# Slightly modified version of the normalisation from PEP 503:
24+
# https://peps.python.org/pep-0503/#normalized-names
25+
# This version uses underscore, so that the result is more
26+
# likely to be a valid import name
27+
def normalize(name: str) -> str:
28+
return re.sub(r"[-_.]+", "_", name).lower()
29+
30+
31+
class EditableException(Exception):
32+
pass
33+
34+
35+
class EditableProject:
36+
def __init__(self, project_name: str, project_dir: Union[str, os.PathLike]) -> None:
37+
if not is_valid(project_name):
38+
raise ValueError(f"Project name {project_name} is not valid")
39+
self.project_name = normalize(project_name)
40+
self.bootstrap = f"_editable_impl_{self.project_name}"
41+
self.project_dir = Path(project_dir)
42+
self.redirections: Dict[str, str] = {}
43+
self.path_entries: List[Path] = []
44+
self.subpackages: Dict[str, Path] = {}
45+
46+
def make_absolute(self, path: Union[str, os.PathLike]) -> Path:
47+
return (self.project_dir / path).resolve()
48+
49+
def map(self, name: str, target: Union[str, os.PathLike]) -> None:
50+
if "." in name:
51+
raise EditableException(
52+
f"Cannot map {name} as it is not a top-level package"
53+
)
54+
abs_target = self.make_absolute(target)
55+
if abs_target.is_dir():
56+
abs_target = abs_target / "__init__.py"
57+
if abs_target.is_file():
58+
self.redirections[name] = str(abs_target)
59+
else:
60+
raise EditableException(f"{target} is not a valid Python package or module")
61+
62+
def add_to_path(self, dirname: Union[str, os.PathLike]) -> None:
63+
self.path_entries.append(self.make_absolute(dirname))
64+
65+
def add_to_subpackage(self, package: str, dirname: Union[str, os.PathLike]) -> None:
66+
self.subpackages[package] = self.make_absolute(dirname)
67+
68+
def files(self) -> Iterable[Tuple[str, str]]:
69+
yield f"{self.project_name}.pth", self.pth_file()
70+
if self.subpackages:
71+
for package, location in self.subpackages.items():
72+
yield self.package_redirection(package, location)
73+
if self.redirections:
74+
yield f"{self.bootstrap}.py", self.bootstrap_file()
75+
76+
def dependencies(self) -> List[str]:
77+
deps = []
78+
if self.redirections:
79+
deps.append("editables")
80+
return deps
81+
82+
def pth_file(self) -> str:
83+
lines = []
84+
if self.redirections:
85+
lines.append(f"import {self.bootstrap}")
86+
for entry in self.path_entries:
87+
lines.append(str(entry))
88+
return "\n".join(lines)
89+
90+
def package_redirection(self, package: str, location: Path) -> Tuple[str, str]:
91+
init_py = package.replace(".", "/") + "/__init__.py"
92+
content = f"__path__ = [{str(location)!r}]"
93+
return init_py, content
94+
95+
def bootstrap_file(self) -> str:
96+
bootstrap = [
97+
"from editables.redirector import RedirectingFinder as F",
98+
"F.install()",
99+
]
100+
for name, path in self.redirections.items():
101+
bootstrap.append(f"F.map_module({name!r}, {path!r})")
102+
return "\n".join(bootstrap)

src/pdm/backend/_vendor/editables/py.typed

Whitespace-only changes.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import importlib.abc
2+
import importlib.machinery
3+
import importlib.util
4+
import sys
5+
from types import ModuleType
6+
from typing import Dict, Optional, Sequence, Union
7+
8+
ModulePath = Optional[Sequence[Union[bytes, str]]]
9+
10+
11+
class RedirectingFinder(importlib.abc.MetaPathFinder):
12+
_redirections: Dict[str, str] = {}
13+
14+
@classmethod
15+
def map_module(cls, name: str, path: str) -> None:
16+
cls._redirections[name] = path
17+
18+
@classmethod
19+
def find_spec(
20+
cls, fullname: str, path: ModulePath = None, target: Optional[ModuleType] = None
21+
) -> Optional[importlib.machinery.ModuleSpec]:
22+
if "." in fullname:
23+
return None
24+
if path is not None:
25+
return None
26+
try:
27+
redir = cls._redirections[fullname]
28+
except KeyError:
29+
return None
30+
spec = importlib.util.spec_from_file_location(fullname, redir)
31+
return spec
32+
33+
@classmethod
34+
def install(cls) -> None:
35+
for f in sys.meta_path:
36+
if f == cls:
37+
break
38+
else:
39+
sys.meta_path.append(cls)
40+
41+
@classmethod
42+
def invalidate_caches(cls) -> None:
43+
# importlib.invalidate_caches calls finders' invalidate_caches methods,
44+
# and since we install this meta path finder as a class rather than an instance,
45+
# we have to override the inherited invalidate_caches method (using self)
46+
# as a classmethod instead
47+
pass

src/pdm/backend/_vendor/vendor.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ packaging==24.0
22
tomli==2.0.1
33
tomli_w==1.0.0
44
pyproject-metadata==0.8.0
5+
editables==0.5

src/pdm/backend/editable.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
import warnings
55
from pathlib import Path
66

7-
from editables import EditableProject
8-
7+
from pdm.backend._vendor.editables import EditableProject
98
from pdm.backend._vendor.packaging.utils import canonicalize_name
109
from pdm.backend.exceptions import ConfigError, PDMWarning
1110
from pdm.backend.hooks.base import Context

tests/test_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ def test_build_with_cextension_in_src(dist: Path) -> None:
296296
@pytest.mark.parametrize("name", ["demo-package"])
297297
def test_build_editable(dist: Path, fixture_project: Path) -> None:
298298
wheel_name = api.build_editable(dist.as_posix())
299-
assert api.get_requires_for_build_editable() == ["editables"]
299+
assert api.get_requires_for_build_editable() == []
300300
with zipfile.ZipFile(dist / wheel_name) as zf:
301301
namelist = zf.namelist()
302302
assert "demo_package.pth" in namelist

0 commit comments

Comments
 (0)