Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
build_meta.py557 linesDownload Raw Back to setuptools
1"""A PEP 517 interface to setuptools2 3Previously, when a user or a command line tool (let's call it a "frontend")4needed to make a request of setuptools to take a certain action, for5example, generating a list of installation requirements, the frontend6would call "setup.py egg_info" or "setup.py bdist_wheel" on the command line.7 8PEP 517 defines a different method of interfacing with setuptools. Rather9than calling "setup.py" directly, the frontend should:10 11  1. Set the current directory to the directory with a setup.py file12  2. Import this module into a safe python interpreter (one in which13     setuptools can potentially set global variables or crash hard).14  3. Call one of the functions defined in PEP 517.15 16What each function does is defined in PEP 517. However, here is a "casual"17definition of the functions (this definition should not be relied on for18bug reports or API stability):19 20  - `build_wheel`: build a wheel in the folder and return the basename21  - `get_requires_for_build_wheel`: get the `setup_requires` to build22  - `prepare_metadata_for_build_wheel`: get the `install_requires`23  - `build_sdist`: build an sdist in the folder and return the basename24  - `get_requires_for_build_sdist`: get the `setup_requires` to build25 26Again, this is not a formal definition! Just a "taste" of the module.27"""28 29from __future__ import annotations30 31import contextlib32import io33import os34import shlex35import shutil36import sys37import tempfile38import tokenize39import warnings40from collections.abc import Iterable, Iterator, Mapping41from pathlib import Path42from typing import TYPE_CHECKING, NoReturn, Union43 44import setuptools45 46from . import errors47from ._path import StrPath, same_path48from ._reqs import parse_strings49from .warnings import SetuptoolsDeprecationWarning50 51import distutils52from distutils.util import strtobool53 54if TYPE_CHECKING:55    from typing_extensions import TypeAlias56 57__all__ = [58    'get_requires_for_build_sdist',59    'get_requires_for_build_wheel',60    'prepare_metadata_for_build_wheel',61    'build_wheel',62    'build_sdist',63    'get_requires_for_build_editable',64    'prepare_metadata_for_build_editable',65    'build_editable',66    '__legacy__',67    'SetupRequirementsError',68]69 70 71class SetupRequirementsError(BaseException):72    def __init__(self, specifiers) -> None:73        self.specifiers = specifiers74 75 76class Distribution(setuptools.dist.Distribution):77    def fetch_build_eggs(self, specifiers) -> NoReturn:78        specifier_list = list(parse_strings(specifiers))79 80        raise SetupRequirementsError(specifier_list)81 82    @classmethod83    @contextlib.contextmanager84    def patch(cls) -> Iterator[None]:85        """86        Replace87        distutils.dist.Distribution with this class88        for the duration of this context.89        """90        orig = distutils.core.Distribution91        distutils.core.Distribution = cls  # type: ignore[misc] # monkeypatching92        try:93            yield94        finally:95            distutils.core.Distribution = orig  # type: ignore[misc] # monkeypatching96 97 98@contextlib.contextmanager99def no_install_setup_requires():100    """Temporarily disable installing setup_requires101 102    Under PEP 517, the backend reports build dependencies to the frontend,103    and the frontend is responsible for ensuring they're installed.104    So setuptools (acting as a backend) should not try to install them.105    """106    orig = setuptools._install_setup_requires107    setuptools._install_setup_requires = lambda attrs: None108    try:109        yield110    finally:111        setuptools._install_setup_requires = orig112 113 114def _get_immediate_subdirectories(a_dir):115    return [116        name for name in os.listdir(a_dir) if os.path.isdir(os.path.join(a_dir, name))117    ]118 119 120def _file_with_extension(directory: StrPath, extension: str | tuple[str, ...]):121    matching = (f for f in os.listdir(directory) if f.endswith(extension))122    try:123        (file,) = matching124    except ValueError:125        raise ValueError(126            'No distribution was found. Ensure that `setup.py` '127            'is not empty and that it calls `setup()`.'128        ) from None129    return file130 131 132def _open_setup_script(setup_script):133    if not os.path.exists(setup_script):134        # Supply a default setup.py135        return io.StringIO("from setuptools import setup; setup()")136 137    return tokenize.open(setup_script)138 139 140@contextlib.contextmanager141def suppress_known_deprecation():142    with warnings.catch_warnings():143        warnings.filterwarnings('ignore', 'setup.py install is deprecated')144        yield145 146 147_ConfigSettings: TypeAlias = Union[Mapping[str, Union[str, list[str], None]], None]148"""149Currently the user can run::150 151    pip install -e . --config-settings key=value152    python -m build -C--key=value -C key=value153 154- pip will pass both key and value as strings and overwriting repeated keys155  (pypa/pip#11059).156- build will accumulate values associated with repeated keys in a list.157  It will also accept keys with no associated value.158  This means that an option passed by build can be ``str | list[str] | None``.159- PEP 517 specifies that ``config_settings`` is an optional dict.160"""161 162 163class _ConfigSettingsTranslator:164    """Translate ``config_settings`` into distutils-style command arguments.165    Only a limited number of options is currently supported.166    """167 168    # See pypa/setuptools#1928 pypa/setuptools#2491169 170    def _get_config(self, key: str, config_settings: _ConfigSettings) -> list[str]:171        """172        Get the value of a specific key in ``config_settings`` as a list of strings.173 174        >>> fn = _ConfigSettingsTranslator()._get_config175        >>> fn("--global-option", None)176        []177        >>> fn("--global-option", {})178        []179        >>> fn("--global-option", {'--global-option': 'foo'})180        ['foo']181        >>> fn("--global-option", {'--global-option': ['foo']})182        ['foo']183        >>> fn("--global-option", {'--global-option': 'foo'})184        ['foo']185        >>> fn("--global-option", {'--global-option': 'foo bar'})186        ['foo', 'bar']187        """188        cfg = config_settings or {}189        opts = cfg.get(key) or []190        return shlex.split(opts) if isinstance(opts, str) else opts191 192    def _global_args(self, config_settings: _ConfigSettings) -> Iterator[str]:193        """194        Let the user specify ``verbose`` or ``quiet`` + escape hatch via195        ``--global-option``.196        Note: ``-v``, ``-vv``, ``-vvv`` have similar effects in setuptools,197        so we just have to cover the basic scenario ``-v``.198 199        >>> fn = _ConfigSettingsTranslator()._global_args200        >>> list(fn(None))201        []202        >>> list(fn({"verbose": "False"}))203        ['-q']204        >>> list(fn({"verbose": "1"}))205        ['-v']206        >>> list(fn({"--verbose": None}))207        ['-v']208        >>> list(fn({"verbose": "true", "--global-option": "-q --no-user-cfg"}))209        ['-v', '-q', '--no-user-cfg']210        >>> list(fn({"--quiet": None}))211        ['-q']212        """213        cfg = config_settings or {}214        falsey = {"false", "no", "0", "off"}215        if "verbose" in cfg or "--verbose" in cfg:216            level = str(cfg.get("verbose") or cfg.get("--verbose") or "1")217            yield ("-q" if level.lower() in falsey else "-v")218        if "quiet" in cfg or "--quiet" in cfg:219            level = str(cfg.get("quiet") or cfg.get("--quiet") or "1")220            yield ("-v" if level.lower() in falsey else "-q")221 222        yield from self._get_config("--global-option", config_settings)223 224    def __dist_info_args(self, config_settings: _ConfigSettings) -> Iterator[str]:225        """226        The ``dist_info`` command accepts ``tag-date`` and ``tag-build``.227 228        .. warning::229           We cannot use this yet as it requires the ``sdist`` and ``bdist_wheel``230           commands run in ``build_sdist`` and ``build_wheel`` to reuse the egg-info231           directory created in ``prepare_metadata_for_build_wheel``.232 233        >>> fn = _ConfigSettingsTranslator()._ConfigSettingsTranslator__dist_info_args234        >>> list(fn(None))235        []236        >>> list(fn({"tag-date": "False"}))237        ['--no-date']238        >>> list(fn({"tag-date": None}))239        ['--no-date']240        >>> list(fn({"tag-date": "true", "tag-build": ".a"}))241        ['--tag-date', '--tag-build', '.a']242        """243        cfg = config_settings or {}244        if "tag-date" in cfg:245            val = strtobool(str(cfg["tag-date"] or "false"))246            yield ("--tag-date" if val else "--no-date")247        if "tag-build" in cfg:248            yield from ["--tag-build", str(cfg["tag-build"])]249 250    def _editable_args(self, config_settings: _ConfigSettings) -> Iterator[str]:251        """252        The ``editable_wheel`` command accepts ``editable-mode=strict``.253 254        >>> fn = _ConfigSettingsTranslator()._editable_args255        >>> list(fn(None))256        []257        >>> list(fn({"editable-mode": "strict"}))258        ['--mode', 'strict']259        """260        cfg = config_settings or {}261        mode = cfg.get("editable-mode") or cfg.get("editable_mode")262        if not mode:263            return264        yield from ["--mode", str(mode)]265 266    def _arbitrary_args(self, config_settings: _ConfigSettings) -> Iterator[str]:267        """268        Users may expect to pass arbitrary lists of arguments to a command269        via "--global-option" (example provided in PEP 517 of a "escape hatch").270 271        >>> fn = _ConfigSettingsTranslator()._arbitrary_args272        >>> list(fn(None))273        []274        >>> list(fn({}))275        []276        >>> list(fn({'--build-option': 'foo'}))277        ['foo']278        >>> list(fn({'--build-option': ['foo']}))279        ['foo']280        >>> list(fn({'--build-option': 'foo'}))281        ['foo']282        >>> list(fn({'--build-option': 'foo bar'}))283        ['foo', 'bar']284        >>> list(fn({'--global-option': 'foo'}))285        []286        """287        yield from self._get_config("--build-option", config_settings)288 289 290class _BuildMetaBackend(_ConfigSettingsTranslator):291    def _get_build_requires(292        self, config_settings: _ConfigSettings, requirements: list[str]293    ):294        sys.argv = [295            *sys.argv[:1],296            *self._global_args(config_settings),297            "egg_info",298        ]299        try:300            with Distribution.patch():301                self.run_setup()302        except SetupRequirementsError as e:303            requirements += e.specifiers304 305        return requirements306 307    def run_setup(self, setup_script: str = 'setup.py') -> None:308        # Note that we can reuse our build directory between calls309        # Correctness comes first, then optimization later310        __file__ = os.path.abspath(setup_script)311        __name__ = '__main__'312 313        with _open_setup_script(__file__) as f:314            code = f.read().replace(r'\r\n', r'\n')315 316        try:317            exec(code, locals())318        except SystemExit as e:319            if e.code:320                raise321            # We ignore exit code indicating success322            SetuptoolsDeprecationWarning.emit(323                "Running `setup.py` directly as CLI tool is deprecated.",324                "Please avoid using `sys.exit(0)` or similar statements "325                "that don't fit in the paradigm of a configuration file.",326                see_url="https://blog.ganssle.io/articles/2021/10/"327                "setup-py-deprecated.html",328            )329 330    def get_requires_for_build_wheel(331        self, config_settings: _ConfigSettings = None332    ) -> list[str]:333        return self._get_build_requires(config_settings, requirements=[])334 335    def get_requires_for_build_sdist(336        self, config_settings: _ConfigSettings = None337    ) -> list[str]:338        return self._get_build_requires(config_settings, requirements=[])339 340    def _bubble_up_info_directory(341        self, metadata_directory: StrPath, suffix: str342    ) -> str:343        """344        PEP 517 requires that the .dist-info directory be placed in the345        metadata_directory. To comply, we MUST copy the directory to the root.346 347        Returns the basename of the info directory, e.g. `proj-0.0.0.dist-info`.348        """349        info_dir = self._find_info_directory(metadata_directory, suffix)350        if not same_path(info_dir.parent, metadata_directory):351            shutil.move(str(info_dir), metadata_directory)352            # PEP 517 allow other files and dirs to exist in metadata_directory353        return info_dir.name354 355    def _find_info_directory(self, metadata_directory: StrPath, suffix: str) -> Path:356        for parent, dirs, _ in os.walk(metadata_directory):357            candidates = [f for f in dirs if f.endswith(suffix)]358 359            if len(candidates) != 0 or len(dirs) != 1:360                assert len(candidates) == 1, (361                    f"Exactly one {suffix} should have been produced, but found {len(candidates)}: {candidates}"362                )363                return Path(parent, candidates[0])364 365        msg = f"No {suffix} directory found in {metadata_directory}"366        raise errors.InternalError(msg)367 368    def prepare_metadata_for_build_wheel(369        self, metadata_directory: StrPath, config_settings: _ConfigSettings = None370    ) -> str:371        sys.argv = [372            *sys.argv[:1],373            *self._global_args(config_settings),374            "dist_info",375            "--output-dir",376            str(metadata_directory),377            "--keep-egg-info",378        ]379        with no_install_setup_requires():380            self.run_setup()381 382        self._bubble_up_info_directory(metadata_directory, ".egg-info")383        return self._bubble_up_info_directory(metadata_directory, ".dist-info")384 385    def _build_with_temp_dir(386        self,387        setup_command: Iterable[str],388        result_extension: str | tuple[str, ...],389        result_directory: StrPath,390        config_settings: _ConfigSettings,391        arbitrary_args: Iterable[str] = (),392    ):393        result_directory = os.path.abspath(result_directory)394 395        # Build in a temporary directory, then copy to the target.396        os.makedirs(result_directory, exist_ok=True)397 398        with tempfile.TemporaryDirectory(399            prefix=".tmp-", dir=result_directory400        ) as tmp_dist_dir:401            sys.argv = [402                *sys.argv[:1],403                *self._global_args(config_settings),404                *setup_command,405                "--dist-dir",406                tmp_dist_dir,407                *arbitrary_args,408            ]409            with no_install_setup_requires():410                self.run_setup()411 412            result_basename = _file_with_extension(tmp_dist_dir, result_extension)413            result_path = os.path.join(result_directory, result_basename)414            if os.path.exists(result_path):415                # os.rename will fail overwriting on non-Unix.416                os.remove(result_path)417            os.rename(os.path.join(tmp_dist_dir, result_basename), result_path)418 419        return result_basename420 421    def build_wheel(422        self,423        wheel_directory: StrPath,424        config_settings: _ConfigSettings = None,425        metadata_directory: StrPath | None = None,426    ) -> str:427        def _build(cmd: list[str]):428            with suppress_known_deprecation():429                return self._build_with_temp_dir(430                    cmd,431                    '.whl',432                    wheel_directory,433                    config_settings,434                    self._arbitrary_args(config_settings),435                )436 437        if metadata_directory is None:438            return _build(['bdist_wheel'])439 440        try:441            return _build(['bdist_wheel', '--dist-info-dir', str(metadata_directory)])442        except SystemExit as ex:  # pragma: nocover443            # pypa/setuptools#4683444            if "--dist-info-dir not recognized" not in str(ex):445                raise446            _IncompatibleBdistWheel.emit()447            return _build(['bdist_wheel'])448 449    def build_sdist(450        self, sdist_directory: StrPath, config_settings: _ConfigSettings = None451    ) -> str:452        return self._build_with_temp_dir(453            ['sdist', '--formats', 'gztar'], '.tar.gz', sdist_directory, config_settings454        )455 456    def _get_dist_info_dir(self, metadata_directory: StrPath | None) -> str | None:457        if not metadata_directory:458            return None459        dist_info_candidates = list(Path(metadata_directory).glob("*.dist-info"))460        assert len(dist_info_candidates) <= 1461        return str(dist_info_candidates[0]) if dist_info_candidates else None462 463    def build_editable(464        self,465        wheel_directory: StrPath,466        config_settings: _ConfigSettings = None,467        metadata_directory: StrPath | None = None,468    ) -> str:469        # XXX can or should we hide our editable_wheel command normally?470        info_dir = self._get_dist_info_dir(metadata_directory)471        opts = ["--dist-info-dir", info_dir] if info_dir else []472        cmd = ["editable_wheel", *opts, *self._editable_args(config_settings)]473        with suppress_known_deprecation():474            return self._build_with_temp_dir(475                cmd, ".whl", wheel_directory, config_settings476            )477 478    def get_requires_for_build_editable(479        self, config_settings: _ConfigSettings = None480    ) -> list[str]:481        return self.get_requires_for_build_wheel(config_settings)482 483    def prepare_metadata_for_build_editable(484        self, metadata_directory: StrPath, config_settings: _ConfigSettings = None485    ) -> str:486        return self.prepare_metadata_for_build_wheel(487            metadata_directory, config_settings488        )489 490 491class _BuildMetaLegacyBackend(_BuildMetaBackend):492    """Compatibility backend for setuptools493 494    This is a version of setuptools.build_meta that endeavors495    to maintain backwards496    compatibility with pre-PEP 517 modes of invocation. It497    exists as a temporary498    bridge between the old packaging mechanism and the new499    packaging mechanism,500    and will eventually be removed.501    """502 503    def run_setup(self, setup_script: str = 'setup.py') -> None:504        # In order to maintain compatibility with scripts assuming that505        # the setup.py script is in a directory on the PYTHONPATH, inject506        # '' into sys.path. (pypa/setuptools#1642)507        sys_path = list(sys.path)  # Save the original path508 509        script_dir = os.path.dirname(os.path.abspath(setup_script))510        if script_dir not in sys.path:511            sys.path.insert(0, script_dir)512 513        # Some setup.py scripts (e.g. in pygame and numpy) use sys.argv[0] to514        # get the directory of the source code. They expect it to refer to the515        # setup.py script.516        sys_argv_0 = sys.argv[0]517        sys.argv[0] = setup_script518 519        try:520            super().run_setup(setup_script=setup_script)521        finally:522            # While PEP 517 frontends should be calling each hook in a fresh523            # subprocess according to the standard (and thus it should not be524            # strictly necessary to restore the old sys.path), we'll restore525            # the original path so that the path manipulation does not persist526            # within the hook after run_setup is called.527            sys.path[:] = sys_path528            sys.argv[0] = sys_argv_0529 530 531class _IncompatibleBdistWheel(SetuptoolsDeprecationWarning):532    _SUMMARY = "wheel.bdist_wheel is deprecated, please import it from setuptools"533    _DETAILS = """534    Ensure that any custom bdist_wheel implementation is a subclass of535    setuptools.command.bdist_wheel.bdist_wheel.536    """537    _DUE_DATE = (2025, 10, 15)538    # Initially introduced in 2024/10/15, but maybe too disruptive to be enforced?539    _SEE_URL = "https://github.com/pypa/wheel/pull/631"540 541 542# The primary backend543_BACKEND = _BuildMetaBackend()544 545get_requires_for_build_wheel = _BACKEND.get_requires_for_build_wheel546get_requires_for_build_sdist = _BACKEND.get_requires_for_build_sdist547prepare_metadata_for_build_wheel = _BACKEND.prepare_metadata_for_build_wheel548build_wheel = _BACKEND.build_wheel549build_sdist = _BACKEND.build_sdist550get_requires_for_build_editable = _BACKEND.get_requires_for_build_editable551prepare_metadata_for_build_editable = _BACKEND.prepare_metadata_for_build_editable552build_editable = _BACKEND.build_editable553 554 555# The legacy backend556__legacy__ = _BuildMetaLegacyBackend()557