Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
build_meta.py515 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 frontend would6would 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 29import io30import os31import shlex32import sys33import tokenize34import shutil35import contextlib36import tempfile37import warnings38from pathlib import Path39from typing import Dict, Iterator, List, Optional, Union40 41import setuptools42import distutils43from . import errors44from ._path import same_path45from ._reqs import parse_strings46from .warnings import SetuptoolsDeprecationWarning47from distutils.util import strtobool48 49 50__all__ = [51    'get_requires_for_build_sdist',52    'get_requires_for_build_wheel',53    'prepare_metadata_for_build_wheel',54    'build_wheel',55    'build_sdist',56    'get_requires_for_build_editable',57    'prepare_metadata_for_build_editable',58    'build_editable',59    '__legacy__',60    'SetupRequirementsError',61]62 63SETUPTOOLS_ENABLE_FEATURES = os.getenv("SETUPTOOLS_ENABLE_FEATURES", "").lower()64LEGACY_EDITABLE = "legacy-editable" in SETUPTOOLS_ENABLE_FEATURES.replace("_", "-")65 66 67class SetupRequirementsError(BaseException):68    def __init__(self, specifiers):69        self.specifiers = specifiers70 71 72class Distribution(setuptools.dist.Distribution):73    def fetch_build_eggs(self, specifiers):74        specifier_list = list(parse_strings(specifiers))75 76        raise SetupRequirementsError(specifier_list)77 78    @classmethod79    @contextlib.contextmanager80    def patch(cls):81        """82        Replace83        distutils.dist.Distribution with this class84        for the duration of this context.85        """86        orig = distutils.core.Distribution87        distutils.core.Distribution = cls88        try:89            yield90        finally:91            distutils.core.Distribution = orig92 93 94@contextlib.contextmanager95def no_install_setup_requires():96    """Temporarily disable installing setup_requires97 98    Under PEP 517, the backend reports build dependencies to the frontend,99    and the frontend is responsible for ensuring they're installed.100    So setuptools (acting as a backend) should not try to install them.101    """102    orig = setuptools._install_setup_requires103    setuptools._install_setup_requires = lambda attrs: None104    try:105        yield106    finally:107        setuptools._install_setup_requires = orig108 109 110def _get_immediate_subdirectories(a_dir):111    return [112        name for name in os.listdir(a_dir) if os.path.isdir(os.path.join(a_dir, name))113    ]114 115 116def _file_with_extension(directory, extension):117    matching = (f for f in os.listdir(directory) if f.endswith(extension))118    try:119        (file,) = matching120    except ValueError:121        raise ValueError(122            'No distribution was found. Ensure that `setup.py` '123            'is not empty and that it calls `setup()`.'124        ) from None125    return file126 127 128def _open_setup_script(setup_script):129    if not os.path.exists(setup_script):130        # Supply a default setup.py131        return io.StringIO("from setuptools import setup; setup()")132 133    return tokenize.open(setup_script)134 135 136@contextlib.contextmanager137def suppress_known_deprecation():138    with warnings.catch_warnings():139        warnings.filterwarnings('ignore', 'setup.py install is deprecated')140        yield141 142 143_ConfigSettings = Optional[Dict[str, Union[str, List[str], None]]]144"""145Currently the user can run::146 147    pip install -e . --config-settings key=value148    python -m build -C--key=value -C key=value149 150- pip will pass both key and value as strings and overwriting repeated keys151  (pypa/pip#11059).152- build will accumulate values associated with repeated keys in a list.153  It will also accept keys with no associated value.154  This means that an option passed by build can be ``str | list[str] | None``.155- PEP 517 specifies that ``config_settings`` is an optional dict.156"""157 158 159class _ConfigSettingsTranslator:160    """Translate ``config_settings`` into distutils-style command arguments.161    Only a limited number of options is currently supported.162    """163 164    # See pypa/setuptools#1928 pypa/setuptools#2491165 166    def _get_config(self, key: str, config_settings: _ConfigSettings) -> List[str]:167        """168        Get the value of a specific key in ``config_settings`` as a list of strings.169 170        >>> fn = _ConfigSettingsTranslator()._get_config171        >>> fn("--global-option", None)172        []173        >>> fn("--global-option", {})174        []175        >>> fn("--global-option", {'--global-option': 'foo'})176        ['foo']177        >>> fn("--global-option", {'--global-option': ['foo']})178        ['foo']179        >>> fn("--global-option", {'--global-option': 'foo'})180        ['foo']181        >>> fn("--global-option", {'--global-option': 'foo bar'})182        ['foo', 'bar']183        """184        cfg = config_settings or {}185        opts = cfg.get(key) or []186        return shlex.split(opts) if isinstance(opts, str) else opts187 188    def _global_args(self, config_settings: _ConfigSettings) -> Iterator[str]:189        """190        Let the user specify ``verbose`` or ``quiet`` + escape hatch via191        ``--global-option``.192        Note: ``-v``, ``-vv``, ``-vvv`` have similar effects in setuptools,193        so we just have to cover the basic scenario ``-v``.194 195        >>> fn = _ConfigSettingsTranslator()._global_args196        >>> list(fn(None))197        []198        >>> list(fn({"verbose": "False"}))199        ['-q']200        >>> list(fn({"verbose": "1"}))201        ['-v']202        >>> list(fn({"--verbose": None}))203        ['-v']204        >>> list(fn({"verbose": "true", "--global-option": "-q --no-user-cfg"}))205        ['-v', '-q', '--no-user-cfg']206        >>> list(fn({"--quiet": None}))207        ['-q']208        """209        cfg = config_settings or {}210        falsey = {"false", "no", "0", "off"}211        if "verbose" in cfg or "--verbose" in cfg:212            level = str(cfg.get("verbose") or cfg.get("--verbose") or "1")213            yield ("-q" if level.lower() in falsey else "-v")214        if "quiet" in cfg or "--quiet" in cfg:215            level = str(cfg.get("quiet") or cfg.get("--quiet") or "1")216            yield ("-v" if level.lower() in falsey else "-q")217 218        yield from self._get_config("--global-option", config_settings)219 220    def __dist_info_args(self, config_settings: _ConfigSettings) -> Iterator[str]:221        """222        The ``dist_info`` command accepts ``tag-date`` and ``tag-build``.223 224        .. warning::225           We cannot use this yet as it requires the ``sdist`` and ``bdist_wheel``226           commands run in ``build_sdist`` and ``build_wheel`` to reuse the egg-info227           directory created in ``prepare_metadata_for_build_wheel``.228 229        >>> fn = _ConfigSettingsTranslator()._ConfigSettingsTranslator__dist_info_args230        >>> list(fn(None))231        []232        >>> list(fn({"tag-date": "False"}))233        ['--no-date']234        >>> list(fn({"tag-date": None}))235        ['--no-date']236        >>> list(fn({"tag-date": "true", "tag-build": ".a"}))237        ['--tag-date', '--tag-build', '.a']238        """239        cfg = config_settings or {}240        if "tag-date" in cfg:241            val = strtobool(str(cfg["tag-date"] or "false"))242            yield ("--tag-date" if val else "--no-date")243        if "tag-build" in cfg:244            yield from ["--tag-build", str(cfg["tag-build"])]245 246    def _editable_args(self, config_settings: _ConfigSettings) -> Iterator[str]:247        """248        The ``editable_wheel`` command accepts ``editable-mode=strict``.249 250        >>> fn = _ConfigSettingsTranslator()._editable_args251        >>> list(fn(None))252        []253        >>> list(fn({"editable-mode": "strict"}))254        ['--mode', 'strict']255        """256        cfg = config_settings or {}257        mode = cfg.get("editable-mode") or cfg.get("editable_mode")258        if not mode:259            return260        yield from ["--mode", str(mode)]261 262    def _arbitrary_args(self, config_settings: _ConfigSettings) -> Iterator[str]:263        """264        Users may expect to pass arbitrary lists of arguments to a command265        via "--global-option" (example provided in PEP 517 of a "escape hatch").266 267        >>> fn = _ConfigSettingsTranslator()._arbitrary_args268        >>> list(fn(None))269        []270        >>> list(fn({}))271        []272        >>> list(fn({'--build-option': 'foo'}))273        ['foo']274        >>> list(fn({'--build-option': ['foo']}))275        ['foo']276        >>> list(fn({'--build-option': 'foo'}))277        ['foo']278        >>> list(fn({'--build-option': 'foo bar'}))279        ['foo', 'bar']280        >>> list(fn({'--global-option': 'foo'}))281        []282        """283        yield from self._get_config("--build-option", config_settings)284 285 286class _BuildMetaBackend(_ConfigSettingsTranslator):287    def _get_build_requires(self, config_settings, requirements):288        sys.argv = [289            *sys.argv[:1],290            *self._global_args(config_settings),291            "egg_info",292        ]293        try:294            with Distribution.patch():295                self.run_setup()296        except SetupRequirementsError as e:297            requirements += e.specifiers298 299        return requirements300 301    def run_setup(self, setup_script='setup.py'):302        # Note that we can reuse our build directory between calls303        # Correctness comes first, then optimization later304        __file__ = os.path.abspath(setup_script)305        __name__ = '__main__'306 307        with _open_setup_script(__file__) as f:308            code = f.read().replace(r'\r\n', r'\n')309 310        try:311            exec(code, locals())312        except SystemExit as e:313            if e.code:314                raise315            # We ignore exit code indicating success316            SetuptoolsDeprecationWarning.emit(317                "Running `setup.py` directly as CLI tool is deprecated.",318                "Please avoid using `sys.exit(0)` or similar statements "319                "that don't fit in the paradigm of a configuration file.",320                see_url="https://blog.ganssle.io/articles/2021/10/"321                "setup-py-deprecated.html",322            )323 324    def get_requires_for_build_wheel(self, config_settings=None):325        return self._get_build_requires(config_settings, requirements=['wheel'])326 327    def get_requires_for_build_sdist(self, config_settings=None):328        return self._get_build_requires(config_settings, requirements=[])329 330    def _bubble_up_info_directory(self, metadata_directory: str, suffix: str) -> str:331        """332        PEP 517 requires that the .dist-info directory be placed in the333        metadata_directory. To comply, we MUST copy the directory to the root.334 335        Returns the basename of the info directory, e.g. `proj-0.0.0.dist-info`.336        """337        info_dir = self._find_info_directory(metadata_directory, suffix)338        if not same_path(info_dir.parent, metadata_directory):339            shutil.move(str(info_dir), metadata_directory)340            # PEP 517 allow other files and dirs to exist in metadata_directory341        return info_dir.name342 343    def _find_info_directory(self, metadata_directory: str, suffix: str) -> Path:344        for parent, dirs, _ in os.walk(metadata_directory):345            candidates = [f for f in dirs if f.endswith(suffix)]346 347            if len(candidates) != 0 or len(dirs) != 1:348                assert len(candidates) == 1, f"Multiple {suffix} directories found"349                return Path(parent, candidates[0])350 351        msg = f"No {suffix} directory found in {metadata_directory}"352        raise errors.InternalError(msg)353 354    def prepare_metadata_for_build_wheel(355        self, metadata_directory, config_settings=None356    ):357        sys.argv = [358            *sys.argv[:1],359            *self._global_args(config_settings),360            "dist_info",361            "--output-dir",362            metadata_directory,363            "--keep-egg-info",364        ]365        with no_install_setup_requires():366            self.run_setup()367 368        self._bubble_up_info_directory(metadata_directory, ".egg-info")369        return self._bubble_up_info_directory(metadata_directory, ".dist-info")370 371    def _build_with_temp_dir(372        self,373        setup_command,374        result_extension,375        result_directory,376        config_settings,377        arbitrary_args=(),378    ):379        result_directory = os.path.abspath(result_directory)380 381        # Build in a temporary directory, then copy to the target.382        os.makedirs(result_directory, exist_ok=True)383        temp_opts = {"prefix": ".tmp-", "dir": result_directory}384 385        with tempfile.TemporaryDirectory(**temp_opts) as tmp_dist_dir:386            sys.argv = [387                *sys.argv[:1],388                *self._global_args(config_settings),389                *setup_command,390                "--dist-dir",391                tmp_dist_dir,392                *arbitrary_args,393            ]394            with no_install_setup_requires():395                self.run_setup()396 397            result_basename = _file_with_extension(tmp_dist_dir, result_extension)398            result_path = os.path.join(result_directory, result_basename)399            if os.path.exists(result_path):400                # os.rename will fail overwriting on non-Unix.401                os.remove(result_path)402            os.rename(os.path.join(tmp_dist_dir, result_basename), result_path)403 404        return result_basename405 406    def build_wheel(407        self, wheel_directory, config_settings=None, metadata_directory=None408    ):409        with suppress_known_deprecation():410            return self._build_with_temp_dir(411                ['bdist_wheel'],412                '.whl',413                wheel_directory,414                config_settings,415                self._arbitrary_args(config_settings),416            )417 418    def build_sdist(self, sdist_directory, config_settings=None):419        return self._build_with_temp_dir(420            ['sdist', '--formats', 'gztar'], '.tar.gz', sdist_directory, config_settings421        )422 423    def _get_dist_info_dir(self, metadata_directory: Optional[str]) -> Optional[str]:424        if not metadata_directory:425            return None426        dist_info_candidates = list(Path(metadata_directory).glob("*.dist-info"))427        assert len(dist_info_candidates) <= 1428        return str(dist_info_candidates[0]) if dist_info_candidates else None429 430    if not LEGACY_EDITABLE:431        # PEP660 hooks:432        # build_editable433        # get_requires_for_build_editable434        # prepare_metadata_for_build_editable435        def build_editable(436            self, wheel_directory, config_settings=None, metadata_directory=None437        ):438            # XXX can or should we hide our editable_wheel command normally?439            info_dir = self._get_dist_info_dir(metadata_directory)440            opts = ["--dist-info-dir", info_dir] if info_dir else []441            cmd = ["editable_wheel", *opts, *self._editable_args(config_settings)]442            with suppress_known_deprecation():443                return self._build_with_temp_dir(444                    cmd, ".whl", wheel_directory, config_settings445                )446 447        def get_requires_for_build_editable(self, config_settings=None):448            return self.get_requires_for_build_wheel(config_settings)449 450        def prepare_metadata_for_build_editable(451            self, metadata_directory, config_settings=None452        ):453            return self.prepare_metadata_for_build_wheel(454                metadata_directory, config_settings455            )456 457 458class _BuildMetaLegacyBackend(_BuildMetaBackend):459    """Compatibility backend for setuptools460 461    This is a version of setuptools.build_meta that endeavors462    to maintain backwards463    compatibility with pre-PEP 517 modes of invocation. It464    exists as a temporary465    bridge between the old packaging mechanism and the new466    packaging mechanism,467    and will eventually be removed.468    """469 470    def run_setup(self, setup_script='setup.py'):471        # In order to maintain compatibility with scripts assuming that472        # the setup.py script is in a directory on the PYTHONPATH, inject473        # '' into sys.path. (pypa/setuptools#1642)474        sys_path = list(sys.path)  # Save the original path475 476        script_dir = os.path.dirname(os.path.abspath(setup_script))477        if script_dir not in sys.path:478            sys.path.insert(0, script_dir)479 480        # Some setup.py scripts (e.g. in pygame and numpy) use sys.argv[0] to481        # get the directory of the source code. They expect it to refer to the482        # setup.py script.483        sys_argv_0 = sys.argv[0]484        sys.argv[0] = setup_script485 486        try:487            super().run_setup(setup_script=setup_script)488        finally:489            # While PEP 517 frontends should be calling each hook in a fresh490            # subprocess according to the standard (and thus it should not be491            # strictly necessary to restore the old sys.path), we'll restore492            # the original path so that the path manipulation does not persist493            # within the hook after run_setup is called.494            sys.path[:] = sys_path495            sys.argv[0] = sys_argv_0496 497 498# The primary backend499_BACKEND = _BuildMetaBackend()500 501get_requires_for_build_wheel = _BACKEND.get_requires_for_build_wheel502get_requires_for_build_sdist = _BACKEND.get_requires_for_build_sdist503prepare_metadata_for_build_wheel = _BACKEND.prepare_metadata_for_build_wheel504build_wheel = _BACKEND.build_wheel505build_sdist = _BACKEND.build_sdist506 507if not LEGACY_EDITABLE:508    get_requires_for_build_editable = _BACKEND.get_requires_for_build_editable509    prepare_metadata_for_build_editable = _BACKEND.prepare_metadata_for_build_editable510    build_editable = _BACKEND.build_editable511 512 513# The legacy backend514__legacy__ = _BuildMetaLegacyBackend()515 
codekingpro/portable-devtools · Team Ai