Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
editable_wheel.py915 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 13from __future__ import annotations14 15import io16import logging17import operator18import os19import shutil20import traceback21from collections.abc import Iterable, Iterator, Mapping22from contextlib import suppress23from enum import Enum24from inspect import cleandoc25from itertools import chain, starmap26from pathlib import Path27from tempfile import TemporaryDirectory28from types import TracebackType29from typing import TYPE_CHECKING, Protocol, TypeVar, cast30 31from .. import Command, _normalization, _path, _shutil, errors, namespaces32from .._path import StrPath33from ..compat import py310, py31234from ..discovery import find_package_path35from ..dist import Distribution36from ..warnings import InformationOnly, SetuptoolsDeprecationWarning37from .build import build as build_cls38from .build_py import build_py as build_py_cls39from .dist_info import dist_info as dist_info_cls40from .egg_info import egg_info as egg_info_cls41from .install import install as install_cls42from .install_scripts import install_scripts as install_scripts_cls43 44if TYPE_CHECKING:45    from typing_extensions import Self46 47    from .._vendor.wheel.wheelfile import WheelFile48 49_P = TypeVar("_P", bound=StrPath)50_logger = logging.getLogger(__name__)51 52 53class _EditableMode(Enum):54    """55    Possible editable installation modes:56    `lenient` (new files automatically added to the package - DEFAULT);57    `strict` (requires a new installation when files are added/removed); or58    `compat` (attempts to emulate `python setup.py develop` - DEPRECATED).59    """60 61    STRICT = "strict"62    LENIENT = "lenient"63    COMPAT = "compat"  # TODO: Remove `compat` after Dec/2022.64 65    @classmethod66    def convert(cls, mode: str | None) -> _EditableMode:67        if not mode:68            return _EditableMode.LENIENT  # default69 70        _mode = mode.upper()71        if _mode not in _EditableMode.__members__:72            raise errors.OptionError(f"Invalid editable mode: {mode!r}. Try: 'strict'.")73 74        if _mode == "COMPAT":75            SetuptoolsDeprecationWarning.emit(76                "Compat editable installs",77                """78                The 'compat' editable mode is transitional and will be removed79                in future versions of `setuptools`.80                Please adapt your code accordingly to use either the 'strict' or the81                'lenient' modes.82                """,83                see_docs="userguide/development_mode.html",84                # TODO: define due_date85                # There is a series of shortcomings with the available editable install86                # methods, and they are very controversial. This is something that still87                # needs work.88                # Moreover, `pip` is still hiding this warning, so users are not aware.89            )90 91        return _EditableMode[_mode]92 93 94_STRICT_WARNING = """95New or renamed files may not be automatically picked up without a new installation.96"""97 98_LENIENT_WARNING = """99Options like `package-data`, `include/exclude-package-data` or100`packages.find.exclude/include` may have no effect.101"""102 103 104class editable_wheel(Command):105    """Build 'editable' wheel for development.106    This command is private and reserved for internal use of setuptools,107    users should rely on ``setuptools.build_meta`` APIs.108    """109 110    description = "DO NOT CALL DIRECTLY, INTERNAL ONLY: create PEP 660 editable wheel"111 112    user_options = [113        ("dist-dir=", "d", "directory to put final built distributions in"),114        ("dist-info-dir=", "I", "path to a pre-build .dist-info directory"),115        ("mode=", None, cleandoc(_EditableMode.__doc__ or "")),116    ]117 118    def initialize_options(self):119        self.dist_dir = None120        self.dist_info_dir = None121        self.project_dir = None122        self.mode = None123 124    def finalize_options(self) -> None:125        dist = self.distribution126        self.project_dir = dist.src_root or os.curdir127        self.package_dir = dist.package_dir or {}128        self.dist_dir = Path(self.dist_dir or os.path.join(self.project_dir, "dist"))129 130    def run(self) -> None:131        try:132            self.dist_dir.mkdir(exist_ok=True)133            self._ensure_dist_info()134 135            # Add missing dist_info files136            self.reinitialize_command("bdist_wheel")137            bdist_wheel = self.get_finalized_command("bdist_wheel")138            bdist_wheel.write_wheelfile(self.dist_info_dir)139 140            self._create_wheel_file(bdist_wheel)141        except Exception as ex:142            project = self.distribution.name or self.distribution.get_name()143            py310.add_note(144                ex,145                f"An error occurred when building editable wheel for {project}.\n"146                "See debugging tips in: "147                "https://setuptools.pypa.io/en/latest/userguide/development_mode.html#debugging-tips",148            )149            raise150 151    def _ensure_dist_info(self):152        if self.dist_info_dir is None:153            dist_info = cast(dist_info_cls, self.reinitialize_command("dist_info"))154            dist_info.output_dir = self.dist_dir155            dist_info.ensure_finalized()156            dist_info.run()157            self.dist_info_dir = dist_info.dist_info_dir158        else:159            assert str(self.dist_info_dir).endswith(".dist-info")160            assert Path(self.dist_info_dir, "METADATA").exists()161 162    def _install_namespaces(self, installation_dir, pth_prefix):163        # XXX: Only required to support the deprecated namespace practice164        dist = self.distribution165        if not dist.namespace_packages:166            return167 168        src_root = Path(self.project_dir, self.package_dir.get("", ".")).resolve()169        installer = _NamespaceInstaller(dist, installation_dir, pth_prefix, src_root)170        installer.install_namespaces()171 172    def _find_egg_info_dir(self) -> str | None:173        parent_dir = Path(self.dist_info_dir).parent if self.dist_info_dir else Path()174        candidates = map(str, parent_dir.glob("*.egg-info"))175        return next(candidates, None)176 177    def _configure_build(178        self, name: str, unpacked_wheel: StrPath, build_lib: StrPath, tmp_dir: StrPath179    ):180        """Configure commands to behave in the following ways:181 182        - Build commands can write to ``build_lib`` if they really want to...183          (but this folder is expected to be ignored and modules are expected to live184          in the project directory...)185        - Binary extensions should be built in-place (editable_mode = True)186        - Data/header/script files are not part of the "editable" specification187          so they are written directly to the unpacked_wheel directory.188        """189        # Non-editable files (data, headers, scripts) are written directly to the190        # unpacked_wheel191 192        dist = self.distribution193        wheel = str(unpacked_wheel)194        build_lib = str(build_lib)195        data = str(Path(unpacked_wheel, f"{name}.data", "data"))196        headers = str(Path(unpacked_wheel, f"{name}.data", "headers"))197        scripts = str(Path(unpacked_wheel, f"{name}.data", "scripts"))198 199        # egg-info may be generated again to create a manifest (used for package data)200        egg_info = cast(201            egg_info_cls, dist.reinitialize_command("egg_info", reinit_subcommands=True)202        )203        egg_info.egg_base = str(tmp_dir)204        egg_info.ignore_egg_info_in_manifest = True205 206        build = cast(207            build_cls, dist.reinitialize_command("build", reinit_subcommands=True)208        )209        install = cast(210            install_cls, dist.reinitialize_command("install", reinit_subcommands=True)211        )212 213        build.build_platlib = build.build_purelib = build.build_lib = build_lib214        install.install_purelib = install.install_platlib = install.install_lib = wheel215        install.install_scripts = build.build_scripts = scripts216        install.install_headers = headers217        install.install_data = data218 219        # For portability, ensure scripts are built with #!python shebang220        # pypa/setuptools#4863221        build_scripts = dist.get_command_obj("build_scripts")222        build_scripts.executable = 'python'223 224        install_scripts = cast(225            install_scripts_cls, dist.get_command_obj("install_scripts")226        )227        install_scripts.no_ep = True228 229        build.build_temp = str(tmp_dir)230 231        build_py = cast(build_py_cls, dist.get_command_obj("build_py"))232        build_py.compile = False233        build_py.existing_egg_info_dir = self._find_egg_info_dir()234 235        self._set_editable_mode()236 237        build.ensure_finalized()238        install.ensure_finalized()239 240    def _set_editable_mode(self):241        """Set the ``editable_mode`` flag in the build sub-commands"""242        dist = self.distribution243        build = dist.get_command_obj("build")244        for cmd_name in build.get_sub_commands():245            cmd = dist.get_command_obj(cmd_name)246            if hasattr(cmd, "editable_mode"):247                cmd.editable_mode = True248            elif hasattr(cmd, "inplace"):249                cmd.inplace = True  # backward compatibility with distutils250 251    def _collect_build_outputs(self) -> tuple[list[str], dict[str, str]]:252        files: list[str] = []253        mapping: dict[str, str] = {}254        build = self.get_finalized_command("build")255 256        for cmd_name in build.get_sub_commands():257            cmd = self.get_finalized_command(cmd_name)258            if hasattr(cmd, "get_outputs"):259                files.extend(cmd.get_outputs() or [])260            if hasattr(cmd, "get_output_mapping"):261                mapping.update(cmd.get_output_mapping() or {})262 263        return files, mapping264 265    def _run_build_commands(266        self,267        dist_name: str,268        unpacked_wheel: StrPath,269        build_lib: StrPath,270        tmp_dir: StrPath,271    ) -> tuple[list[str], dict[str, str]]:272        self._configure_build(dist_name, unpacked_wheel, build_lib, tmp_dir)273        self._run_build_subcommands()274        files, mapping = self._collect_build_outputs()275        self._run_install("headers")276        self._run_install("scripts")277        self._run_install("data")278        return files, mapping279 280    def _run_build_subcommands(self) -> None:281        """282        Issue #3501 indicates that some plugins/customizations might rely on:283 284        1. ``build_py`` not running285        2. ``build_py`` always copying files to ``build_lib``286 287        However both these assumptions may be false in editable_wheel.288        This method implements a temporary workaround to support the ecosystem289        while the implementations catch up.290        """291        # TODO: Once plugins/customizations had the chance to catch up, replace292        #       `self._run_build_subcommands()` with `self.run_command("build")`.293        #       Also remove _safely_run, TestCustomBuildPy. Suggested date: Aug/2023.294        build = self.get_finalized_command("build")295        for name in build.get_sub_commands():296            cmd = self.get_finalized_command(name)297            if name == "build_py" and type(cmd) is not build_py_cls:298                self._safely_run(name)299            else:300                self.run_command(name)301 302    def _safely_run(self, cmd_name: str):303        try:304            return self.run_command(cmd_name)305        except Exception:306            SetuptoolsDeprecationWarning.emit(307                "Customization incompatible with editable install",308                f"""309                {traceback.format_exc()}310 311                If you are seeing this warning it is very likely that a setuptools312                plugin or customization overrides the `{cmd_name}` command, without313                taking into consideration how editable installs run build steps314                starting from setuptools v64.0.0.315 316                Plugin authors and developers relying on custom build steps are317                encouraged to update their `{cmd_name}` implementation considering the318                information about editable installs in319                https://setuptools.pypa.io/en/latest/userguide/extension.html.320 321                For the time being `setuptools` will silence this error and ignore322                the faulty command, but this behavior will change in future versions.323                """,324                # TODO: define due_date325                # There is a series of shortcomings with the available editable install326                # methods, and they are very controversial. This is something that still327                # needs work.328            )329 330    def _create_wheel_file(self, bdist_wheel):331        from wheel.wheelfile import WheelFile332 333        dist_info = self.get_finalized_command("dist_info")334        dist_name = dist_info.name335        tag = "-".join(bdist_wheel.get_tag())336        build_tag = "0.editable"  # According to PEP 427 needs to start with digit337        archive_name = f"{dist_name}-{build_tag}-{tag}.whl"338        wheel_path = Path(self.dist_dir, archive_name)339        if wheel_path.exists():340            wheel_path.unlink()341 342        unpacked_wheel = TemporaryDirectory(suffix=archive_name)343        build_lib = TemporaryDirectory(suffix=".build-lib")344        build_tmp = TemporaryDirectory(suffix=".build-temp")345 346        with unpacked_wheel as unpacked, build_lib as lib, build_tmp as tmp:347            unpacked_dist_info = Path(unpacked, Path(self.dist_info_dir).name)348            shutil.copytree(self.dist_info_dir, unpacked_dist_info)349            self._install_namespaces(unpacked, dist_name)350            files, mapping = self._run_build_commands(dist_name, unpacked, lib, tmp)351            strategy = self._select_strategy(dist_name, tag, lib)352            with strategy, WheelFile(wheel_path, "w") as wheel_obj:353                strategy(wheel_obj, files, mapping)354                wheel_obj.write_files(unpacked)355 356        return wheel_path357 358    def _run_install(self, category: str):359        has_category = getattr(self.distribution, f"has_{category}", None)360        if has_category and has_category():361            _logger.info(f"Installing {category} as non editable")362            self.run_command(f"install_{category}")363 364    def _select_strategy(365        self,366        name: str,367        tag: str,368        build_lib: StrPath,369    ) -> EditableStrategy:370        """Decides which strategy to use to implement an editable installation."""371        build_name = f"__editable__.{name}-{tag}"372        project_dir = Path(self.project_dir)373        mode = _EditableMode.convert(self.mode)374 375        if mode is _EditableMode.STRICT:376            auxiliary_dir = _empty_dir(Path(self.project_dir, "build", build_name))377            return _LinkTree(self.distribution, name, auxiliary_dir, build_lib)378 379        packages = _find_packages(self.distribution)380        has_simple_layout = _simple_layout(packages, self.package_dir, project_dir)381        is_compat_mode = mode is _EditableMode.COMPAT382        if set(self.package_dir) == {""} and has_simple_layout or is_compat_mode:383            # src-layout(ish) is relatively safe for a simple pth file384            src_dir = self.package_dir.get("", ".")385            return _StaticPth(self.distribution, name, [Path(project_dir, src_dir)])386 387        # Use a MetaPathFinder to avoid adding accidental top-level packages/modules388        return _TopLevelFinder(self.distribution, name)389 390 391class EditableStrategy(Protocol):392    def __call__(393        self, wheel: WheelFile, files: list[str], mapping: Mapping[str, str]394    ) -> object: ...395    def __enter__(self) -> Self: ...396    def __exit__(397        self,398        _exc_type: type[BaseException] | None,399        _exc_value: BaseException | None,400        _traceback: TracebackType | None,401    ) -> object: ...402 403 404class _StaticPth:405    def __init__(self, dist: Distribution, name: str, path_entries: list[Path]) -> None:406        self.dist = dist407        self.name = name408        self.path_entries = path_entries409 410    def __call__(411        self, wheel: WheelFile, files: list[str], mapping: Mapping[str, str]412    ) -> None:413        entries = "\n".join(str(p.resolve()) for p in self.path_entries)414        contents = _encode_pth(f"{entries}\n")415        wheel.writestr(f"__editable__.{self.name}.pth", contents)416 417    def __enter__(self) -> Self:418        msg = f"""419        Editable install will be performed using .pth file to extend `sys.path` with:420        {list(map(os.fspath, self.path_entries))!r}421        """422        _logger.warning(msg + _LENIENT_WARNING)423        return self424 425    def __exit__(426        self,427        _exc_type: object,428        _exc_value: object,429        _traceback: object,430    ) -> None:431        pass432 433 434class _LinkTree(_StaticPth):435    """436    Creates a ``.pth`` file that points to a link tree in the ``auxiliary_dir``.437 438    This strategy will only link files (not dirs), so it can be implemented in439    any OS, even if that means using hardlinks instead of symlinks.440 441    By collocating ``auxiliary_dir`` and the original source code, limitations442    with hardlinks should be avoided.443    """444 445    def __init__(446        self,447        dist: Distribution,448        name: str,449        auxiliary_dir: StrPath,450        build_lib: StrPath,451    ) -> None:452        self.auxiliary_dir = Path(auxiliary_dir)453        self.build_lib = Path(build_lib).resolve()454        self._file = dist.get_command_obj("build_py").copy_file455        super().__init__(dist, name, [self.auxiliary_dir])456 457    def __call__(458        self, wheel: WheelFile, files: list[str], mapping: Mapping[str, str]459    ) -> None:460        self._create_links(files, mapping)461        super().__call__(wheel, files, mapping)462 463    def _normalize_output(self, file: str) -> str | None:464        # Files relative to build_lib will be normalized to None465        with suppress(ValueError):466            path = Path(file).resolve().relative_to(self.build_lib)467            return str(path).replace(os.sep, '/')468        return None469 470    def _create_file(self, relative_output: str, src_file: str, link=None):471        dest = self.auxiliary_dir / relative_output472        if not dest.parent.is_dir():473            dest.parent.mkdir(parents=True)474        self._file(src_file, dest, link=link)475 476    def _create_links(self, outputs, output_mapping: Mapping[str, str]):477        self.auxiliary_dir.mkdir(parents=True, exist_ok=True)478        link_type = "sym" if _can_symlink_files(self.auxiliary_dir) else "hard"479        normalised = ((self._normalize_output(k), v) for k, v in output_mapping.items())480        # remove files that are not relative to build_lib481        mappings = {k: v for k, v in normalised if k is not None}482 483        for output in outputs:484            relative = self._normalize_output(output)485            if relative and relative not in mappings:486                self._create_file(relative, output)487 488        for relative, src in mappings.items():489            self._create_file(relative, src, link=link_type)490 491    def __enter__(self) -> Self:492        msg = "Strict editable install will be performed using a link tree.\n"493        _logger.warning(msg + _STRICT_WARNING)494        return self495 496    def __exit__(497        self,498        _exc_type: object,499        _exc_value: object,500        _traceback: object,501    ) -> None:502        msg = f"""\n503        Strict editable installation performed using the auxiliary directory:504            {self.auxiliary_dir}505 506        Please be careful to not remove this directory, otherwise you might not be able507        to import/use your package.508        """509        InformationOnly.emit("Editable installation.", msg)510 511 512class _TopLevelFinder:513    def __init__(self, dist: Distribution, name: str) -> None:514        self.dist = dist515        self.name = name516 517    def template_vars(self) -> tuple[str, str, dict[str, str], dict[str, list[str]]]:518        src_root = self.dist.src_root or os.curdir519        top_level = chain(_find_packages(self.dist), _find_top_level_modules(self.dist))520        package_dir = self.dist.package_dir or {}521        roots = _find_package_roots(top_level, package_dir, src_root)522 523        namespaces_ = dict(524            chain(525                _find_namespaces(self.dist.packages or [], roots),526                ((ns, []) for ns in _find_virtual_namespaces(roots)),527            )528        )529 530        legacy_namespaces = {531            pkg: find_package_path(pkg, roots, self.dist.src_root or "")532            for pkg in self.dist.namespace_packages or []533        }534 535        mapping = {**roots, **legacy_namespaces}536        # ^-- We need to explicitly add the legacy_namespaces to the mapping to be537        #     able to import their modules even if another package sharing the same538        #     namespace is installed in a conventional (non-editable) way.539 540        name = f"__editable__.{self.name}.finder"541        finder = _normalization.safe_identifier(name)542        return finder, name, mapping, namespaces_543 544    def get_implementation(self) -> Iterator[tuple[str, bytes]]:545        finder, name, mapping, namespaces_ = self.template_vars()546 547        content = bytes(_finder_template(name, mapping, namespaces_), "utf-8")548        yield (f"{finder}.py", content)549 550        content = _encode_pth(f"import {finder}; {finder}.install()")551        yield (f"__editable__.{self.name}.pth", content)552 553    def __call__(554        self, wheel: WheelFile, files: list[str], mapping: Mapping[str, str]555    ) -> None:556        for file, content in self.get_implementation():557            wheel.writestr(file, content)558 559    def __enter__(self) -> Self:560        msg = "Editable install will be performed using a meta path finder.\n"561        _logger.warning(msg + _LENIENT_WARNING)562        return self563 564    def __exit__(565        self,566        _exc_type: object,567        _exc_value: object,568        _traceback: object,569    ) -> None:570        msg = """\n571        Please be careful with folders in your working directory with the same572        name as your package as they may take precedence during imports.573        """574        InformationOnly.emit("Editable installation.", msg)575 576 577def _encode_pth(content: str) -> bytes:578    """579    Prior to Python 3.13 (see https://github.com/python/cpython/issues/77102),580    .pth files are always read with 'locale' encoding, the recommendation581    from the cpython core developers is to write them as ``open(path, "w")``582    and ignore warnings (see python/cpython#77102, pypa/setuptools#3937).583    This function tries to simulate this behavior without having to create an584    actual file, in a way that supports a range of active Python versions.585    (There seems to be some variety in the way different version of Python handle586    ``encoding=None``, not all of them use ``locale.getpreferredencoding(False)``587    or ``locale.getencoding()``).588    """589    with io.BytesIO() as buffer:590        wrapper = io.TextIOWrapper(buffer, encoding=py312.PTH_ENCODING)591        # TODO: Python 3.13 replace the whole function with `bytes(content, "utf-8")`592        wrapper.write(content)593        wrapper.flush()594        buffer.seek(0)595        return buffer.read()596 597 598def _can_symlink_files(base_dir: Path) -> bool:599    with TemporaryDirectory(dir=str(base_dir.resolve())) as tmp:600        path1, path2 = Path(tmp, "file1.txt"), Path(tmp, "file2.txt")601        path1.write_text("file1", encoding="utf-8")602        with suppress(AttributeError, NotImplementedError, OSError):603            os.symlink(path1, path2)604            if path2.is_symlink() and path2.read_text(encoding="utf-8") == "file1":605                return True606 607        try:608            os.link(path1, path2)  # Ensure hard links can be created609        except Exception as ex:610            msg = (611                "File system does not seem to support either symlinks or hard links. "612                "Strict editable installs require one of them to be supported."613            )614            raise LinksNotSupported(msg) from ex615        return False616 617 618def _simple_layout(619    packages: Iterable[str], package_dir: dict[str, str], project_dir: StrPath620) -> bool:621    """Return ``True`` if:622    - all packages are contained by the same parent directory, **and**623    - all packages become importable if the parent directory is added to ``sys.path``.624 625    >>> _simple_layout(['a'], {"": "src"}, "/tmp/myproj")626    True627    >>> _simple_layout(['a', 'a.b'], {"": "src"}, "/tmp/myproj")628    True629    >>> _simple_layout(['a', 'a.b'], {}, "/tmp/myproj")630    True631    >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"": "src"}, "/tmp/myproj")632    True633    >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a": "a", "b": "b"}, ".")634    True635    >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a": "_a", "b": "_b"}, ".")636    False637    >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a": "_a"}, "/tmp/myproj")638    False639    >>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a.a1.a2": "_a2"}, ".")640    False641    >>> _simple_layout(['a', 'a.b'], {"": "src", "a.b": "_ab"}, "/tmp/myproj")642    False643    >>> # Special cases, no packages yet:644    >>> _simple_layout([], {"": "src"}, "/tmp/myproj")645    True646    >>> _simple_layout([], {"a": "_a", "": "src"}, "/tmp/myproj")647    False648    """649    layout = {pkg: find_package_path(pkg, package_dir, project_dir) for pkg in packages}650    if not layout:651        return set(package_dir) in ({}, {""})652    parent = os.path.commonpath(starmap(_parent_path, layout.items()))653    return all(654        _path.same_path(Path(parent, *key.split('.')), value)655        for key, value in layout.items()656    )657 658 659def _parent_path(pkg, pkg_path):660    """Infer the parent path containing a package, that if added to ``sys.path`` would661    allow importing that package.662    When ``pkg`` is directly mapped into a directory with a different name, return its663    own path.664    >>> _parent_path("a", "src/a")665    'src'666    >>> _parent_path("b", "src/c")667    'src/c'668    """669    parent = pkg_path.removesuffix(pkg)670    return parent.rstrip("/" + os.sep)671 672 673def _find_packages(dist: Distribution) -> Iterator[str]:674    yield from iter(dist.packages or [])675 676    py_modules = dist.py_modules or []677    nested_modules = [mod for mod in py_modules if "." in mod]678    if dist.ext_package:679        yield dist.ext_package680    else:681        ext_modules = dist.ext_modules or []682        nested_modules += [x.name for x in ext_modules if "." in x.name]683 684    for module in nested_modules:685        package, _, _ = module.rpartition(".")686        yield package687 688 689def _find_top_level_modules(dist: Distribution) -> Iterator[str]:690    py_modules = dist.py_modules or []691    yield from (mod for mod in py_modules if "." not in mod)692 693    if not dist.ext_package:694        ext_modules = dist.ext_modules or []695        yield from (x.name for x in ext_modules if "." not in x.name)696 697 698def _find_package_roots(699    packages: Iterable[str],700    package_dir: Mapping[str, str],701    src_root: StrPath,702) -> dict[str, str]:703    pkg_roots: dict[str, str] = {704        pkg: _absolute_root(find_package_path(pkg, package_dir, src_root))705        for pkg in sorted(packages)706    }707 708    return _remove_nested(pkg_roots)709 710 711def _absolute_root(path: StrPath) -> str:712    """Works for packages and top-level modules"""713    path_ = Path(path)714    parent = path_.parent715 716    if path_.exists():717        return str(path_.resolve())718    else:719        return str(parent.resolve() / path_.name)720 721 722def _find_virtual_namespaces(pkg_roots: dict[str, str]) -> Iterator[str]:723    """By carefully designing ``package_dir``, it is possible to implement the logical724    structure of PEP 420 in a package without the corresponding directories.725 726    Moreover a parent package can be purposefully/accidentally skipped in the discovery727    phase (e.g. ``find_packages(include=["mypkg.*"])``, when ``mypkg.foo`` is included728    by ``mypkg`` itself is not).729    We consider this case to also be a virtual namespace (ignoring the original730    directory) to emulate a non-editable installation.731 732    This function will try to find these kinds of namespaces.733    """734    for pkg in pkg_roots:735        if "." not in pkg:736            continue737        parts = pkg.split(".")738        for i in range(len(parts) - 1, 0, -1):739            partial_name = ".".join(parts[:i])740            path = Path(find_package_path(partial_name, pkg_roots, ""))741            if not path.exists() or partial_name not in pkg_roots:742                # partial_name not in pkg_roots ==> purposefully/accidentally skipped743                yield partial_name744 745 746def _find_namespaces(747    packages: list[str], pkg_roots: dict[str, str]748) -> Iterator[tuple[str, list[str]]]:749    for pkg in packages:750        path = find_package_path(pkg, pkg_roots, "")751        if Path(path).exists() and not Path(path, "__init__.py").exists():752            yield (pkg, [path])753 754 755def _remove_nested(pkg_roots: dict[str, str]) -> dict[str, str]:756    output = dict(pkg_roots.copy())757 758    for pkg, path in reversed(list(pkg_roots.items())):759        if any(760            pkg != other and _is_nested(pkg, path, other, other_path)761            for other, other_path in pkg_roots.items()762        ):763            output.pop(pkg)764 765    return output766 767 768def _is_nested(pkg: str, pkg_path: str, parent: str, parent_path: str) -> bool:769    """770    Return ``True`` if ``pkg`` is nested inside ``parent`` both logically and in the771    file system.772    >>> _is_nested("a.b", "path/a/b", "a", "path/a")773    True774    >>> _is_nested("a.b", "path/a/b", "a", "otherpath/a")775    False776    >>> _is_nested("a.b", "path/a/b", "c", "path/c")777    False778    >>> _is_nested("a.a", "path/a/a", "a", "path/a")779    True780    >>> _is_nested("b.a", "path/b/a", "a", "path/a")781    False782    """783    norm_pkg_path = _path.normpath(pkg_path)784    rest = pkg.replace(parent, "", 1).strip(".").split(".")785    return pkg.startswith(parent) and norm_pkg_path == _path.normpath(786        Path(parent_path, *rest)787    )788 789 790def _empty_dir(dir_: _P) -> _P:791    """Create a directory ensured to be empty. Existing files may be removed."""792    _shutil.rmtree(dir_, ignore_errors=True)793    os.makedirs(dir_)794    return dir_795 796 797class _NamespaceInstaller(namespaces.Installer):798    def __init__(self, distribution, installation_dir, editable_name, src_root) -> None:799        self.distribution = distribution800        self.src_root = src_root801        self.installation_dir = installation_dir802        self.editable_name = editable_name803        self.outputs: list[str] = []804 805    def _get_nspkg_file(self):806        """Installation target."""807        return os.path.join(self.installation_dir, self.editable_name + self.nspkg_ext)808 809    def _get_root(self):810        """Where the modules/packages should be loaded from."""811        return repr(str(self.src_root))812 813 814_FINDER_TEMPLATE = """\815from __future__ import annotations816import sys817from importlib.machinery import ModuleSpec, PathFinder818from importlib.machinery import all_suffixes as module_suffixes819from importlib.util import spec_from_file_location820from itertools import chain821from pathlib import Path822 823MAPPING: dict[str, str] = {mapping!r}824NAMESPACES: dict[str, list[str]] = {namespaces!r}825PATH_PLACEHOLDER = {name!r} + ".__path_hook__"826 827 828class _EditableFinder:  # MetaPathFinder829    @classmethod830    def find_spec(cls, fullname: str, path=None, target=None) -> ModuleSpec | None:  # type: ignore831        # Top-level packages and modules (we know these exist in the FS)832        if fullname in MAPPING:833            pkg_path = MAPPING[fullname]834            return cls._find_spec(fullname, Path(pkg_path))835 836        # Handle immediate children modules (required for namespaces to work)837        # To avoid problems with case sensitivity in the file system we delegate838        # to the importlib.machinery implementation.839        parent, _, child = fullname.rpartition(".")840        if parent and parent in MAPPING:841            return PathFinder.find_spec(fullname, path=[MAPPING[parent]])842 843        # Other levels of nesting should be handled automatically by importlib844        # using the parent path.845        return None846 847    @classmethod848    def _find_spec(cls, fullname: str, candidate_path: Path) -> ModuleSpec | None:849        init = candidate_path / "__init__.py"850        candidates = (candidate_path.with_suffix(x) for x in module_suffixes())851        for candidate in chain([init], candidates):852            if candidate.exists():853                return spec_from_file_location(fullname, candidate)854        return None855 856 857class _EditableNamespaceFinder:  # PathEntryFinder858    @classmethod859    def _path_hook(cls, path) -> type[_EditableNamespaceFinder]:860        if path == PATH_PLACEHOLDER:861            return cls862        raise ImportError863 864    @classmethod865    def _paths(cls, fullname: str) -> list[str]:866        paths = NAMESPACES[fullname]867        if not paths and fullname in MAPPING:868            paths = [MAPPING[fullname]]869        # Always add placeholder, for 2 reasons:870        # 1. __path__ cannot be empty for the spec to be considered namespace.871        # 2. In the case of nested namespaces, we need to force872        #    import machinery to query _EditableNamespaceFinder again.873        return [*paths, PATH_PLACEHOLDER]874 875    @classmethod876    def find_spec(cls, fullname: str, target=None) -> ModuleSpec | None:  # type: ignore877        if fullname in NAMESPACES:878            spec = ModuleSpec(fullname, None, is_package=True)879            spec.submodule_search_locations = cls._paths(fullname)880            return spec881        return None882 883    @classmethod884    def find_module(cls, _fullname) -> None:885        return None886 887 888def install():889    if not any(finder == _EditableFinder for finder in sys.meta_path):890        sys.meta_path.append(_EditableFinder)891 892    if not NAMESPACES:893        return894 895    if not any(hook == _EditableNamespaceFinder._path_hook for hook in sys.path_hooks):896        # PathEntryFinder is needed to create NamespaceSpec without private APIS897        sys.path_hooks.append(_EditableNamespaceFinder._path_hook)898    if PATH_PLACEHOLDER not in sys.path:899        sys.path.append(PATH_PLACEHOLDER)  # Used just to trigger the path hook900"""901 902 903def _finder_template(904    name: str, mapping: Mapping[str, str], namespaces: dict[str, list[str]]905) -> str:906    """Create a string containing the code for the``MetaPathFinder`` and907    ``PathEntryFinder``.908    """909    mapping = dict(sorted(mapping.items(), key=operator.itemgetter(0)))910    return _FINDER_TEMPLATE.format(name=name, mapping=mapping, namespaces=namespaces)911 912 913class LinksNotSupported(errors.FileError):914    """File system does not seem to support either symlinks or hard links."""915 
codekingpro/portable-devtools · Team Ai