Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
editable_wheel.py907 linesDownload Raw Back to command
1"""2Create a wheel that, when installed, will make the source package 'editable'3(add it to the interpreter's path, including metadata) per PEP 660. Replaces4'setup.py develop'.5 6.. note::7   One of the mechanisms briefly mentioned in PEP 660 to implement editable installs is8   to create a separated directory inside ``build`` and use a .pth file to point to that9   directory. In the context of this file such directory is referred as10   *auxiliary build directory* or ``auxiliary_dir``.11"""12 13import logging14import io15import os16import shutil17import traceback18from contextlib import suppress19from enum import Enum20from inspect import cleandoc21from itertools import chain, starmap22from pathlib import Path23from tempfile import TemporaryDirectory24from typing import (25    TYPE_CHECKING,26    Dict,27    Iterable,28    Iterator,29    List,30    Mapping,31    Optional,32    Protocol,33    Tuple,34    TypeVar,35    cast,36)37 38from .. import (39    Command,40    _normalization,41    _path,42    errors,43    namespaces,44)45from .._path import StrPath46from ..compat import py3947from ..discovery import find_package_path48from ..dist import Distribution49from ..warnings import (50    InformationOnly,51    SetuptoolsDeprecationWarning,52    SetuptoolsWarning,53)54from .build import build as build_cls55from .build_py import build_py as build_py_cls56from .dist_info import dist_info as dist_info_cls57from .egg_info import egg_info as egg_info_cls58from .install import install as install_cls59from .install_scripts import install_scripts as install_scripts_cls60 61if TYPE_CHECKING:62    from wheel.wheelfile import WheelFile  # type:ignore[import-untyped] # noqa63 64_P = TypeVar("_P", bound=StrPath)65_logger = logging.getLogger(__name__)66 67 68class _EditableMode(Enum):69    """70    Possible editable installation modes:71    `lenient` (new files automatically added to the package - DEFAULT);72    `strict` (requires a new installation when files are added/removed); or73    `compat` (attempts to emulate `python setup.py develop` - DEPRECATED).74    """75 76    STRICT = "strict"77    LENIENT = "lenient"78    COMPAT = "compat"  # TODO: Remove `compat` after Dec/2022.79 80    @classmethod81    def convert(cls, mode: Optional[str]) -> "_EditableMode":82        if not mode:83            return _EditableMode.LENIENT  # default84 85        _mode = mode.upper()86        if _mode not in _EditableMode.__members__:87            raise errors.OptionError(f"Invalid editable mode: {mode!r}. Try: 'strict'.")88 89        if _mode == "COMPAT":90            SetuptoolsDeprecationWarning.emit(91                "Compat editable installs",92                """93                The 'compat' editable mode is transitional and will be removed94                in future versions of `setuptools`.95                Please adapt your code accordingly to use either the 'strict' or the96                'lenient' modes.97                """,98                see_docs="userguide/development_mode.html",99                # TODO: define due_date100                # There is a series of shortcomings with the available editable install101                # methods, and they are very controversial. This is something that still102                # needs work.103                # Moreover, `pip` is still hiding this warning, so users are not aware.104            )105 106        return _EditableMode[_mode]107 108 109_STRICT_WARNING = """110New or renamed files may not be automatically picked up without a new installation.111"""112 113_LENIENT_WARNING = """114Options like `package-data`, `include/exclude-package-data` or115`packages.find.exclude/include` may have no effect.116"""117 118 119class editable_wheel(Command):120    """Build 'editable' wheel for development.121    This command is private and reserved for internal use of setuptools,122    users should rely on ``setuptools.build_meta`` APIs.123    """124 125    description = "DO NOT CALL DIRECTLY, INTERNAL ONLY: create PEP 660 editable wheel"126 127    user_options = [128        ("dist-dir=", "d", "directory to put final built distributions in"),129        ("dist-info-dir=", "I", "path to a pre-build .dist-info directory"),130        ("mode=", None, cleandoc(_EditableMode.__doc__ or "")),131    ]132 133    def initialize_options(self):134        self.dist_dir = None135        self.dist_info_dir = None136        self.project_dir = None137        self.mode = None138 139    def finalize_options(self):140        dist = self.distribution141        self.project_dir = dist.src_root or os.curdir142        self.package_dir = dist.package_dir or {}143        self.dist_dir = Path(self.dist_dir or os.path.join(self.project_dir, "dist"))144 145    def run(self):146        try:147            self.dist_dir.mkdir(exist_ok=True)148            self._ensure_dist_info()149 150            # Add missing dist_info files151            self.reinitialize_command("bdist_wheel")152            bdist_wheel = self.get_finalized_command("bdist_wheel")153            bdist_wheel.write_wheelfile(self.dist_info_dir)154 155            self._create_wheel_file(bdist_wheel)156        except Exception:157            traceback.print_exc()158            project = self.distribution.name or self.distribution.get_name()159            _DebuggingTips.emit(project=project)160            raise161 162    def _ensure_dist_info(self):163        if self.dist_info_dir is None:164            dist_info = cast(dist_info_cls, self.reinitialize_command("dist_info"))165            dist_info.output_dir = self.dist_dir166            dist_info.ensure_finalized()167            dist_info.run()168            self.dist_info_dir = dist_info.dist_info_dir169        else:170            assert str(self.dist_info_dir).endswith(".dist-info")171            assert Path(self.dist_info_dir, "METADATA").exists()172 173    def _install_namespaces(self, installation_dir, pth_prefix):174        # XXX: Only required to support the deprecated namespace practice175        dist = self.distribution176        if not dist.namespace_packages:177            return178 179        src_root = Path(self.project_dir, self.package_dir.get("", ".")).resolve()180        installer = _NamespaceInstaller(dist, installation_dir, pth_prefix, src_root)181        installer.install_namespaces()182 183    def _find_egg_info_dir(self) -> Optional[str]:184        parent_dir = Path(self.dist_info_dir).parent if self.dist_info_dir else Path()185        candidates = map(str, parent_dir.glob("*.egg-info"))186        return next(candidates, None)187 188    def _configure_build(189        self, name: str, unpacked_wheel: StrPath, build_lib: StrPath, tmp_dir: StrPath190    ):191        """Configure commands to behave in the following ways:192 193        - Build commands can write to ``build_lib`` if they really want to...194          (but this folder is expected to be ignored and modules are expected to live195          in the project directory...)196        - Binary extensions should be built in-place (editable_mode = True)197        - Data/header/script files are not part of the "editable" specification198          so they are written directly to the unpacked_wheel directory.199        """200        # Non-editable files (data, headers, scripts) are written directly to the201        # unpacked_wheel202 203        dist = self.distribution204        wheel = str(unpacked_wheel)205        build_lib = str(build_lib)206        data = str(Path(unpacked_wheel, f"{name}.data", "data"))207        headers = str(Path(unpacked_wheel, f"{name}.data", "headers"))208        scripts = str(Path(unpacked_wheel, f"{name}.data", "scripts"))209 210        # egg-info may be generated again to create a manifest (used for package data)211        egg_info = cast(212            egg_info_cls, dist.reinitialize_command("egg_info", reinit_subcommands=True)213        )214        egg_info.egg_base = str(tmp_dir)215        egg_info.ignore_egg_info_in_manifest = True216 217        build = cast(218            build_cls, dist.reinitialize_command("build", reinit_subcommands=True)219        )220        install = cast(221            install_cls, dist.reinitialize_command("install", reinit_subcommands=True)222        )223 224        build.build_platlib = build.build_purelib = build.build_lib = build_lib225        install.install_purelib = install.install_platlib = install.install_lib = wheel226        install.install_scripts = build.build_scripts = scripts227        install.install_headers = headers228        install.install_data = data229 230        install_scripts = cast(231            install_scripts_cls, dist.get_command_obj("install_scripts")232        )233        install_scripts.no_ep = True234 235        build.build_temp = str(tmp_dir)236 237        build_py = cast(build_py_cls, dist.get_command_obj("build_py"))238        build_py.compile = False239        build_py.existing_egg_info_dir = self._find_egg_info_dir()240 241        self._set_editable_mode()242 243        build.ensure_finalized()244        install.ensure_finalized()245 246    def _set_editable_mode(self):247        """Set the ``editable_mode`` flag in the build sub-commands"""248        dist = self.distribution249        build = dist.get_command_obj("build")250        # TODO: Update typeshed distutils stubs to overload non-None return type by default251        for cmd_name in build.get_sub_commands():252            cmd = dist.get_command_obj(cmd_name)253            if hasattr(cmd, "editable_mode"):254                cmd.editable_mode = True255            elif hasattr(cmd, "inplace"):256                cmd.inplace = True  # backward compatibility with distutils257 258    def _collect_build_outputs(self) -> Tuple[List[str], Dict[str, str]]:259        files: List[str] = []260        mapping: Dict[str, str] = {}261        build = self.get_finalized_command("build")262 263        for cmd_name in build.get_sub_commands():264            cmd = self.get_finalized_command(cmd_name)265            if hasattr(cmd, "get_outputs"):266                files.extend(cmd.get_outputs() or [])267            if hasattr(cmd, "get_output_mapping"):268                mapping.update(cmd.get_output_mapping() or {})269 270        return files, mapping271 272    def _run_build_commands(273        self,274        dist_name: str,275        unpacked_wheel: StrPath,276        build_lib: StrPath,277        tmp_dir: StrPath,278    ) -> Tuple[List[str], Dict[str, str]]:279        self._configure_build(dist_name, unpacked_wheel, build_lib, tmp_dir)280        self._run_build_subcommands()281        files, mapping = self._collect_build_outputs()282        self._run_install("headers")283        self._run_install("scripts")284        self._run_install("data")285        return files, mapping286 287    def _run_build_subcommands(self) -> None:288        """289        Issue #3501 indicates that some plugins/customizations might rely on:290 291        1. ``build_py`` not running292        2. ``build_py`` always copying files to ``build_lib``293 294        However both these assumptions may be false in editable_wheel.295        This method implements a temporary workaround to support the ecosystem296        while the implementations catch up.297        """298        # TODO: Once plugins/customisations had the chance to catch up, replace299        #       `self._run_build_subcommands()` with `self.run_command("build")`.300        #       Also remove _safely_run, TestCustomBuildPy. Suggested date: Aug/2023.301        build = self.get_finalized_command("build")302        for name in build.get_sub_commands():303            cmd = self.get_finalized_command(name)304            if name == "build_py" and type(cmd) != build_py_cls:305                self._safely_run(name)306            else:307                self.run_command(name)308 309    def _safely_run(self, cmd_name: str):310        try:311            return self.run_command(cmd_name)312        except Exception:313            SetuptoolsDeprecationWarning.emit(314                "Customization incompatible with editable install",315                f"""316                {traceback.format_exc()}317 318                If you are seeing this warning it is very likely that a setuptools319                plugin or customization overrides the `{cmd_name}` command, without320                taking into consideration how editable installs run build steps321                starting from setuptools v64.0.0.322 323                Plugin authors and developers relying on custom build steps are324                encouraged to update their `{cmd_name}` implementation considering the325                information about editable installs in326                https://setuptools.pypa.io/en/latest/userguide/extension.html.327 328                For the time being `setuptools` will silence this error and ignore329                the faulty command, but this behaviour will change in future versions.330                """,331                # TODO: define due_date332                # There is a series of shortcomings with the available editable install333                # methods, and they are very controversial. This is something that still334                # needs work.335            )336 337    def _create_wheel_file(self, bdist_wheel):338        from wheel.wheelfile import WheelFile339 340        dist_info = self.get_finalized_command("dist_info")341        dist_name = dist_info.name342        tag = "-".join(bdist_wheel.get_tag())343        build_tag = "0.editable"  # According to PEP 427 needs to start with digit344        archive_name = f"{dist_name}-{build_tag}-{tag}.whl"345        wheel_path = Path(self.dist_dir, archive_name)346        if wheel_path.exists():347            wheel_path.unlink()348 349        unpacked_wheel = TemporaryDirectory(suffix=archive_name)350        build_lib = TemporaryDirectory(suffix=".build-lib")351        build_tmp = TemporaryDirectory(suffix=".build-temp")352 353        with unpacked_wheel as unpacked, build_lib as lib, build_tmp as tmp:354            unpacked_dist_info = Path(unpacked, Path(self.dist_info_dir).name)355            shutil.copytree(self.dist_info_dir, unpacked_dist_info)356            self._install_namespaces(unpacked, dist_name)357            files, mapping = self._run_build_commands(dist_name, unpacked, lib, tmp)358            strategy = self._select_strategy(dist_name, tag, lib)359            with strategy, WheelFile(wheel_path, "w") as wheel_obj:360                strategy(wheel_obj, files, mapping)361                wheel_obj.write_files(unpacked)362 363        return wheel_path364 365    def _run_install(self, category: str):366        has_category = getattr(self.distribution, f"has_{category}", None)367        if has_category and has_category():368            _logger.info(f"Installing {category} as non editable")369            self.run_command(f"install_{category}")370 371    def _select_strategy(372        self,373        name: str,374        tag: str,375        build_lib: StrPath,376    ) -> "EditableStrategy":377        """Decides which strategy to use to implement an editable installation."""378        build_name = f"__editable__.{name}-{tag}"379        project_dir = Path(self.project_dir)380        mode = _EditableMode.convert(self.mode)381 382        if mode is _EditableMode.STRICT:383            auxiliary_dir = _empty_dir(Path(self.project_dir, "build", build_name))384            return _LinkTree(self.distribution, name, auxiliary_dir, build_lib)385 386        packages = _find_packages(self.distribution)387        has_simple_layout = _simple_layout(packages, self.package_dir, project_dir)388        is_compat_mode = mode is _EditableMode.COMPAT389        if set(self.package_dir) == {""} and has_simple_layout or is_compat_mode:390            # src-layout(ish) is relatively safe for a simple pth file391            src_dir = self.package_dir.get("", ".")392            return _StaticPth(self.distribution, name, [Path(project_dir, src_dir)])393 394        # Use a MetaPathFinder to avoid adding accidental top-level packages/modules395        return _TopLevelFinder(self.distribution, name)396 397 398class EditableStrategy(Protocol):399    def __call__(400        self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]401    ): ...402 403    def __enter__(self): ...404 405    def __exit__(self, _exc_type, _exc_value, _traceback): ...406 407 408class _StaticPth:409    def __init__(self, dist: Distribution, name: str, path_entries: List[Path]):410        self.dist = dist411        self.name = name412        self.path_entries = path_entries413 414    def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):415        entries = "\n".join(str(p.resolve()) for p in self.path_entries)416        contents = _encode_pth(f"{entries}\n")417        wheel.writestr(f"__editable__.{self.name}.pth", contents)418 419    def __enter__(self):420        msg = f"""421        Editable install will be performed using .pth file to extend `sys.path` with:422        {list(map(os.fspath, self.path_entries))!r}423        """424        _logger.warning(msg + _LENIENT_WARNING)425        return self426 427    def __exit__(self, _exc_type, _exc_value, _traceback): ...428 429 430class _LinkTree(_StaticPth):431    """432    Creates a ``.pth`` file that points to a link tree in the ``auxiliary_dir``.433 434    This strategy will only link files (not dirs), so it can be implemented in435    any OS, even if that means using hardlinks instead of symlinks.436 437    By collocating ``auxiliary_dir`` and the original source code, limitations438    with hardlinks should be avoided.439    """440 441    def __init__(442        self,443        dist: Distribution,444        name: str,445        auxiliary_dir: StrPath,446        build_lib: StrPath,447    ):448        self.auxiliary_dir = Path(auxiliary_dir)449        self.build_lib = Path(build_lib).resolve()450        # TODO: Update typeshed distutils stubs to overload non-None return type by default451        self._file = dist.get_command_obj("build_py").copy_file  # type: ignore[union-attr]452        super().__init__(dist, name, [self.auxiliary_dir])453 454    def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):455        self._create_links(files, mapping)456        super().__call__(wheel, files, mapping)457 458    def _normalize_output(self, file: str) -> Optional[str]:459        # Files relative to build_lib will be normalized to None460        with suppress(ValueError):461            path = Path(file).resolve().relative_to(self.build_lib)462            return str(path).replace(os.sep, '/')463        return None464 465    def _create_file(self, relative_output: str, src_file: str, link=None):466        dest = self.auxiliary_dir / relative_output467        if not dest.parent.is_dir():468            dest.parent.mkdir(parents=True)469        # TODO: Update typeshed distutils stubs so distutils.cmd.Command.copy_file, accepts PathLike470        # same with methods used by copy_file471        self._file(src_file, dest, link=link)  # type: ignore[arg-type]472 473    def _create_links(self, outputs, output_mapping):474        self.auxiliary_dir.mkdir(parents=True, exist_ok=True)475        link_type = "sym" if _can_symlink_files(self.auxiliary_dir) else "hard"476        mappings = {self._normalize_output(k): v for k, v in output_mapping.items()}477        mappings.pop(None, None)  # remove files that are not relative to build_lib478 479        for output in outputs:480            relative = self._normalize_output(output)481            if relative and relative not in mappings:482                self._create_file(relative, output)483 484        for relative, src in mappings.items():485            self._create_file(relative, src, link=link_type)486 487    def __enter__(self):488        msg = "Strict editable install will be performed using a link tree.\n"489        _logger.warning(msg + _STRICT_WARNING)490        return self491 492    def __exit__(self, _exc_type, _exc_value, _traceback):493        msg = f"""\n494        Strict editable installation performed using the auxiliary directory:495            {self.auxiliary_dir}496 497        Please be careful to not remove this directory, otherwise you might not be able498        to import/use your package.499        """500        InformationOnly.emit("Editable installation.", msg)501 502 503class _TopLevelFinder:504    def __init__(self, dist: Distribution, name: str):505        self.dist = dist506        self.name = name507 508    def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):509        src_root = self.dist.src_root or os.curdir510        top_level = chain(_find_packages(self.dist), _find_top_level_modules(self.dist))511        package_dir = self.dist.package_dir or {}512        roots = _find_package_roots(top_level, package_dir, src_root)513 514        namespaces_: Dict[str, List[str]] = dict(515            chain(516                _find_namespaces(self.dist.packages or [], roots),517                ((ns, []) for ns in _find_virtual_namespaces(roots)),518            )519        )520 521        legacy_namespaces = {522            pkg: find_package_path(pkg, roots, self.dist.src_root or "")523            for pkg in self.dist.namespace_packages or []524        }525 526        mapping = {**roots, **legacy_namespaces}527        # ^-- We need to explicitly add the legacy_namespaces to the mapping to be528        #     able to import their modules even if another package sharing the same529        #     namespace is installed in a conventional (non-editable) way.530 531        name = f"__editable__.{self.name}.finder"532        finder = _normalization.safe_identifier(name)533        content = bytes(_finder_template(name, mapping, namespaces_), "utf-8")534        wheel.writestr(f"{finder}.py", content)535 536        content = _encode_pth(f"import {finder}; {finder}.install()")537        wheel.writestr(f"__editable__.{self.name}.pth", content)538 539    def __enter__(self):540        msg = "Editable install will be performed using a meta path finder.\n"541        _logger.warning(msg + _LENIENT_WARNING)542        return self543 544    def __exit__(self, _exc_type, _exc_value, _traceback):545        msg = """\n546        Please be careful with folders in your working directory with the same547        name as your package as they may take precedence during imports.548        """549        InformationOnly.emit("Editable installation.", msg)550 551 552def _encode_pth(content: str) -> bytes:553    """.pth files are always read with 'locale' encoding, the recommendation554    from the cpython core developers is to write them as ``open(path, "w")``555    and ignore warnings (see python/cpython#77102, pypa/setuptools#3937).556    This function tries to simulate this behaviour without having to create an557    actual file, in a way that supports a range of active Python versions.558    (There seems to be some variety in the way different version of Python handle559    ``encoding=None``, not all of them use ``locale.getpreferredencoding(False)``).560    """561    with io.BytesIO() as buffer:562        wrapper = io.TextIOWrapper(buffer, encoding=py39.LOCALE_ENCODING)563        wrapper.write(content)564        wrapper.flush()565        buffer.seek(0)566        return buffer.read()567 568 569def _can_symlink_files(base_dir: Path) -> bool:570    with TemporaryDirectory(dir=str(base_dir.resolve())) as tmp:571        path1, path2 = Path(tmp, "file1.txt"), Path(tmp, "file2.txt")572        path1.write_text("file1", encoding="utf-8")573        with suppress(AttributeError, NotImplementedError, OSError):574            os.symlink(path1, path2)575            if path2.is_symlink() and path2.read_text(encoding="utf-8") == "file1":576                return True577 578        try:579            os.link(path1, path2)  # Ensure hard links can be created580        except Exception as ex:581            msg = (582                "File system does not seem to support either symlinks or hard links. "583                "Strict editable installs require one of them to be supported."584            )585            raise LinksNotSupported(msg) from ex586        return False587 588 589def _simple_layout(590    packages: Iterable[str], package_dir: Dict[str, str], project_dir: StrPath591) -> bool:592    """Return ``True`` if:593    - all packages are contained by the same parent directory, **and**594    - all packages become importable if the parent directory is added to ``sys.path``.595 596    >>> _simple_layout(['a'], {"": "src"}, "/tmp/myproj")597    True598    >>> _simple_layout(['a', 'a.b'], {"": "src"}, "/tmp/myproj")599    True600    >>> _simple_layout(['a', 'a.b'], {}, "/tmp/myproj")601    True602    >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"": "src"}, "/tmp/myproj")603    True604    >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a": "a", "b": "b"}, ".")605    True606    >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a": "_a", "b": "_b"}, ".")607    False608    >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a": "_a"}, "/tmp/myproj")609    False610    >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a.a1.a2": "_a2"}, ".")611    False612    >>> _simple_layout(['a', 'a.b'], {"": "src", "a.b": "_ab"}, "/tmp/myproj")613    False614    >>> # Special cases, no packages yet:615    >>> _simple_layout([], {"": "src"}, "/tmp/myproj")616    True617    >>> _simple_layout([], {"a": "_a", "": "src"}, "/tmp/myproj")618    False619    """620    layout = {pkg: find_package_path(pkg, package_dir, project_dir) for pkg in packages}621    if not layout:622        return set(package_dir) in ({}, {""})623    parent = os.path.commonpath(starmap(_parent_path, layout.items()))624    return all(625        _path.same_path(Path(parent, *key.split('.')), value)626        for key, value in layout.items()627    )628 629 630def _parent_path(pkg, pkg_path):631    """Infer the parent path containing a package, that if added to ``sys.path`` would632    allow importing that package.633    When ``pkg`` is directly mapped into a directory with a different name, return its634    own path.635    >>> _parent_path("a", "src/a")636    'src'637    >>> _parent_path("b", "src/c")638    'src/c'639    """640    parent = pkg_path[: -len(pkg)] if pkg_path.endswith(pkg) else pkg_path641    return parent.rstrip("/" + os.sep)642 643 644def _find_packages(dist: Distribution) -> Iterator[str]:645    yield from iter(dist.packages or [])646 647    py_modules = dist.py_modules or []648    nested_modules = [mod for mod in py_modules if "." in mod]649    if dist.ext_package:650        yield dist.ext_package651    else:652        ext_modules = dist.ext_modules or []653        nested_modules += [x.name for x in ext_modules if "." in x.name]654 655    for module in nested_modules:656        package, _, _ = module.rpartition(".")657        yield package658 659 660def _find_top_level_modules(dist: Distribution) -> Iterator[str]:661    py_modules = dist.py_modules or []662    yield from (mod for mod in py_modules if "." not in mod)663 664    if not dist.ext_package:665        ext_modules = dist.ext_modules or []666        yield from (x.name for x in ext_modules if "." not in x.name)667 668 669def _find_package_roots(670    packages: Iterable[str],671    package_dir: Mapping[str, str],672    src_root: StrPath,673) -> Dict[str, str]:674    pkg_roots: Dict[str, str] = {675        pkg: _absolute_root(find_package_path(pkg, package_dir, src_root))676        for pkg in sorted(packages)677    }678 679    return _remove_nested(pkg_roots)680 681 682def _absolute_root(path: StrPath) -> str:683    """Works for packages and top-level modules"""684    path_ = Path(path)685    parent = path_.parent686 687    if path_.exists():688        return str(path_.resolve())689    else:690        return str(parent.resolve() / path_.name)691 692 693def _find_virtual_namespaces(pkg_roots: Dict[str, str]) -> Iterator[str]:694    """By carefully designing ``package_dir``, it is possible to implement the logical695    structure of PEP 420 in a package without the corresponding directories.696 697    Moreover a parent package can be purposefully/accidentally skipped in the discovery698    phase (e.g. ``find_packages(include=["mypkg.*"])``, when ``mypkg.foo`` is included699    by ``mypkg`` itself is not).700    We consider this case to also be a virtual namespace (ignoring the original701    directory) to emulate a non-editable installation.702 703    This function will try to find these kinds of namespaces.704    """705    for pkg in pkg_roots:706        if "." not in pkg:707            continue708        parts = pkg.split(".")709        for i in range(len(parts) - 1, 0, -1):710            partial_name = ".".join(parts[:i])711            path = Path(find_package_path(partial_name, pkg_roots, ""))712            if not path.exists() or partial_name not in pkg_roots:713                # partial_name not in pkg_roots ==> purposefully/accidentally skipped714                yield partial_name715 716 717def _find_namespaces(718    packages: List[str], pkg_roots: Dict[str, str]719) -> Iterator[Tuple[str, List[str]]]:720    for pkg in packages:721        path = find_package_path(pkg, pkg_roots, "")722        if Path(path).exists() and not Path(path, "__init__.py").exists():723            yield (pkg, [path])724 725 726def _remove_nested(pkg_roots: Dict[str, str]) -> Dict[str, str]:727    output = dict(pkg_roots.copy())728 729    for pkg, path in reversed(list(pkg_roots.items())):730        if any(731            pkg != other and _is_nested(pkg, path, other, other_path)732            for other, other_path in pkg_roots.items()733        ):734            output.pop(pkg)735 736    return output737 738 739def _is_nested(pkg: str, pkg_path: str, parent: str, parent_path: str) -> bool:740    """741    Return ``True`` if ``pkg`` is nested inside ``parent`` both logically and in the742    file system.743    >>> _is_nested("a.b", "path/a/b", "a", "path/a")744    True745    >>> _is_nested("a.b", "path/a/b", "a", "otherpath/a")746    False747    >>> _is_nested("a.b", "path/a/b", "c", "path/c")748    False749    >>> _is_nested("a.a", "path/a/a", "a", "path/a")750    True751    >>> _is_nested("b.a", "path/b/a", "a", "path/a")752    False753    """754    norm_pkg_path = _path.normpath(pkg_path)755    rest = pkg.replace(parent, "", 1).strip(".").split(".")756    return pkg.startswith(parent) and norm_pkg_path == _path.normpath(757        Path(parent_path, *rest)758    )759 760 761def _empty_dir(dir_: _P) -> _P:762    """Create a directory ensured to be empty. Existing files may be removed."""763    shutil.rmtree(dir_, ignore_errors=True)764    os.makedirs(dir_)765    return dir_766 767 768class _NamespaceInstaller(namespaces.Installer):769    def __init__(self, distribution, installation_dir, editable_name, src_root):770        self.distribution = distribution771        self.src_root = src_root772        self.installation_dir = installation_dir773        self.editable_name = editable_name774        self.outputs = []775        self.dry_run = False776 777    def _get_nspkg_file(self):778        """Installation target."""779        return os.path.join(self.installation_dir, self.editable_name + self.nspkg_ext)780 781    def _get_root(self):782        """Where the modules/packages should be loaded from."""783        return repr(str(self.src_root))784 785 786_FINDER_TEMPLATE = """\787import sys788from importlib.machinery import ModuleSpec, PathFinder789from importlib.machinery import all_suffixes as module_suffixes790from importlib.util import spec_from_file_location791from itertools import chain792from pathlib import Path793 794MAPPING = {mapping!r}795NAMESPACES = {namespaces!r}796PATH_PLACEHOLDER = {name!r} + ".__path_hook__"797 798 799class _EditableFinder:  # MetaPathFinder800    @classmethod801    def find_spec(cls, fullname, path=None, target=None):802        extra_path = []803 804        # Top-level packages and modules (we know these exist in the FS)805        if fullname in MAPPING:806            pkg_path = MAPPING[fullname]807            return cls._find_spec(fullname, Path(pkg_path))808 809        # Handle immediate children modules (required for namespaces to work)810        # To avoid problems with case sensitivity in the file system we delegate811        # to the importlib.machinery implementation.812        parent, _, child = fullname.rpartition(".")813        if parent and parent in MAPPING:814            return PathFinder.find_spec(fullname, path=[MAPPING[parent], *extra_path])815 816        # Other levels of nesting should be handled automatically by importlib817        # using the parent path.818        return None819 820    @classmethod821    def _find_spec(cls, fullname, candidate_path):822        init = candidate_path / "__init__.py"823        candidates = (candidate_path.with_suffix(x) for x in module_suffixes())824        for candidate in chain([init], candidates):825            if candidate.exists():826                return spec_from_file_location(fullname, candidate)827 828 829class _EditableNamespaceFinder:  # PathEntryFinder830    @classmethod831    def _path_hook(cls, path):832        if path == PATH_PLACEHOLDER:833            return cls834        raise ImportError835 836    @classmethod837    def _paths(cls, fullname):838        # Ensure __path__ is not empty for the spec to be considered a namespace.839        return NAMESPACES[fullname] or MAPPING.get(fullname) or [PATH_PLACEHOLDER]840 841    @classmethod842    def find_spec(cls, fullname, target=None):843        if fullname in NAMESPACES:844            spec = ModuleSpec(fullname, None, is_package=True)845            spec.submodule_search_locations = cls._paths(fullname)846            return spec847        return None848 849    @classmethod850    def find_module(cls, fullname):851        return None852 853 854def install():855    if not any(finder == _EditableFinder for finder in sys.meta_path):856        sys.meta_path.append(_EditableFinder)857 858    if not NAMESPACES:859        return860 861    if not any(hook == _EditableNamespaceFinder._path_hook for hook in sys.path_hooks):862        # PathEntryFinder is needed to create NamespaceSpec without private APIS863        sys.path_hooks.append(_EditableNamespaceFinder._path_hook)864    if PATH_PLACEHOLDER not in sys.path:865        sys.path.append(PATH_PLACEHOLDER)  # Used just to trigger the path hook866"""867 868 869def _finder_template(870    name: str, mapping: Mapping[str, str], namespaces: Dict[str, List[str]]871) -> str:872    """Create a string containing the code for the``MetaPathFinder`` and873    ``PathEntryFinder``.874    """875    mapping = dict(sorted(mapping.items(), key=lambda p: p[0]))876    return _FINDER_TEMPLATE.format(name=name, mapping=mapping, namespaces=namespaces)877 878 879class LinksNotSupported(errors.FileError):880    """File system does not seem to support either symlinks or hard links."""881 882 883class _DebuggingTips(SetuptoolsWarning):884    _SUMMARY = "Problem in editable installation."885    _DETAILS = """886    An error happened while installing `{project}` in editable mode.887 888    The following steps are recommended to help debug this problem:889 890    - Try to install the project normally, without using the editable mode.891      Does the error still persist?892      (If it does, try fixing the problem before attempting the editable mode).893    - If you are using binary extensions, make sure you have all OS-level894      dependencies installed (e.g. compilers, toolchains, binary libraries, ...).895    - Try the latest version of setuptools (maybe the error was already fixed).896    - If you (or your project dependencies) are using any setuptools extension897      or customization, make sure they support the editable mode.898 899    After following the steps above, if the problem still persists and900    you think this is related to how setuptools handles editable installations,901    please submit a reproducible example902    (see https://stackoverflow.com/help/minimal-reproducible-example) to:903 904        https://github.com/pypa/setuptools/issues905    """906    _SEE_DOCS = "userguide/development_mode.html"907 
codekingpro/portable-devtools · Team Ai