diff --git a/src/pdm/backend/__init__.py b/src/pdm/backend/__init__.py index b99dde8..ec2c61a 100644 --- a/src/pdm/backend/__init__.py +++ b/src/pdm/backend/__init__.py @@ -77,7 +77,7 @@ def get_requires_for_build_editable( When C-extension build is needed, setuptools should be required, otherwise just return an empty list. """ - return get_requires_for_build_wheel(config_settings) + ["editables"] + return get_requires_for_build_wheel(config_settings) def prepare_metadata_for_build_editable( diff --git a/src/pdm/backend/_vendor/editables/LICENSE.txt b/src/pdm/backend/_vendor/editables/LICENSE.txt new file mode 100644 index 0000000..472cbdc --- /dev/null +++ b/src/pdm/backend/_vendor/editables/LICENSE.txt @@ -0,0 +1,18 @@ +Copyright (c) 2020 Paul Moore + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/pdm/backend/_vendor/editables/__init__.py b/src/pdm/backend/_vendor/editables/__init__.py new file mode 100644 index 0000000..63b8d9c --- /dev/null +++ b/src/pdm/backend/_vendor/editables/__init__.py @@ -0,0 +1,102 @@ +import os +import re +from pathlib import Path +from typing import Dict, Iterable, List, Tuple, Union + +__all__ = ( + "EditableProject", + "__version__", +) + +__version__ = "0.5" + + +# Check if a project name is valid, based on PEP 426: +# https://peps.python.org/pep-0426/#name +def is_valid(name: str) -> bool: + return ( + re.match(r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", name, re.IGNORECASE) + is not None + ) + + +# Slightly modified version of the normalisation from PEP 503: +# https://peps.python.org/pep-0503/#normalized-names +# This version uses underscore, so that the result is more +# likely to be a valid import name +def normalize(name: str) -> str: + return re.sub(r"[-_.]+", "_", name).lower() + + +class EditableException(Exception): + pass + + +class EditableProject: + def __init__(self, project_name: str, project_dir: Union[str, os.PathLike]) -> None: + if not is_valid(project_name): + raise ValueError(f"Project name {project_name} is not valid") + self.project_name = normalize(project_name) + self.bootstrap = f"_editable_impl_{self.project_name}" + self.project_dir = Path(project_dir) + self.redirections: Dict[str, str] = {} + self.path_entries: List[Path] = [] + self.subpackages: Dict[str, Path] = {} + + def make_absolute(self, path: Union[str, os.PathLike]) -> Path: + return (self.project_dir / path).resolve() + + def map(self, name: str, target: Union[str, os.PathLike]) -> None: + if "." in name: + raise EditableException( + f"Cannot map {name} as it is not a top-level package" + ) + abs_target = self.make_absolute(target) + if abs_target.is_dir(): + abs_target = abs_target / "__init__.py" + if abs_target.is_file(): + self.redirections[name] = str(abs_target) + else: + raise EditableException(f"{target} is not a valid Python package or module") + + def add_to_path(self, dirname: Union[str, os.PathLike]) -> None: + self.path_entries.append(self.make_absolute(dirname)) + + def add_to_subpackage(self, package: str, dirname: Union[str, os.PathLike]) -> None: + self.subpackages[package] = self.make_absolute(dirname) + + def files(self) -> Iterable[Tuple[str, str]]: + yield f"{self.project_name}.pth", self.pth_file() + if self.subpackages: + for package, location in self.subpackages.items(): + yield self.package_redirection(package, location) + if self.redirections: + yield f"{self.bootstrap}.py", self.bootstrap_file() + + def dependencies(self) -> List[str]: + deps = [] + if self.redirections: + deps.append("editables") + return deps + + def pth_file(self) -> str: + lines = [] + if self.redirections: + lines.append(f"import {self.bootstrap}") + for entry in self.path_entries: + lines.append(str(entry)) + return "\n".join(lines) + + def package_redirection(self, package: str, location: Path) -> Tuple[str, str]: + init_py = package.replace(".", "/") + "/__init__.py" + content = f"__path__ = [{str(location)!r}]" + return init_py, content + + def bootstrap_file(self) -> str: + bootstrap = [ + "from editables.redirector import RedirectingFinder as F", + "F.install()", + ] + for name, path in self.redirections.items(): + bootstrap.append(f"F.map_module({name!r}, {path!r})") + return "\n".join(bootstrap) diff --git a/src/pdm/backend/_vendor/editables/py.typed b/src/pdm/backend/_vendor/editables/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/pdm/backend/_vendor/editables/redirector.py b/src/pdm/backend/_vendor/editables/redirector.py new file mode 100644 index 0000000..7bdef59 --- /dev/null +++ b/src/pdm/backend/_vendor/editables/redirector.py @@ -0,0 +1,47 @@ +import importlib.abc +import importlib.machinery +import importlib.util +import sys +from types import ModuleType +from typing import Dict, Optional, Sequence, Union + +ModulePath = Optional[Sequence[Union[bytes, str]]] + + +class RedirectingFinder(importlib.abc.MetaPathFinder): + _redirections: Dict[str, str] = {} + + @classmethod + def map_module(cls, name: str, path: str) -> None: + cls._redirections[name] = path + + @classmethod + def find_spec( + cls, fullname: str, path: ModulePath = None, target: Optional[ModuleType] = None + ) -> Optional[importlib.machinery.ModuleSpec]: + if "." in fullname: + return None + if path is not None: + return None + try: + redir = cls._redirections[fullname] + except KeyError: + return None + spec = importlib.util.spec_from_file_location(fullname, redir) + return spec + + @classmethod + def install(cls) -> None: + for f in sys.meta_path: + if f == cls: + break + else: + sys.meta_path.append(cls) + + @classmethod + def invalidate_caches(cls) -> None: + # importlib.invalidate_caches calls finders' invalidate_caches methods, + # and since we install this meta path finder as a class rather than an instance, + # we have to override the inherited invalidate_caches method (using self) + # as a classmethod instead + pass diff --git a/src/pdm/backend/_vendor/vendor.txt b/src/pdm/backend/_vendor/vendor.txt index 84a6b9a..c2749ac 100644 --- a/src/pdm/backend/_vendor/vendor.txt +++ b/src/pdm/backend/_vendor/vendor.txt @@ -2,3 +2,4 @@ packaging==24.0 tomli==2.0.1 tomli_w==1.0.0 pyproject-metadata==0.8.0 +editables==0.5 diff --git a/src/pdm/backend/editable.py b/src/pdm/backend/editable.py index 9b44db3..379699d 100644 --- a/src/pdm/backend/editable.py +++ b/src/pdm/backend/editable.py @@ -4,8 +4,7 @@ import warnings from pathlib import Path -from editables import EditableProject - +from pdm.backend._vendor.editables import EditableProject from pdm.backend._vendor.packaging.utils import canonicalize_name from pdm.backend.exceptions import ConfigError, PDMWarning from pdm.backend.hooks.base import Context diff --git a/tests/test_api.py b/tests/test_api.py index 270ce25..b997cc7 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -296,7 +296,7 @@ def test_build_with_cextension_in_src(dist: Path) -> None: @pytest.mark.parametrize("name", ["demo-package"]) def test_build_editable(dist: Path, fixture_project: Path) -> None: wheel_name = api.build_editable(dist.as_posix()) - assert api.get_requires_for_build_editable() == ["editables"] + assert api.get_requires_for_build_editable() == [] with zipfile.ZipFile(dist / wheel_name) as zf: namelist = zf.namelist() assert "demo_package.pth" in namelist