Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
_builder.py394 linesDownload Raw Back to build
1# SPDX-License-Identifier: MIT2 3from __future__ import annotations4 5 6__lazy_modules__ = [7    'contextlib',8    'difflib',9    f'{__spec__.parent}._compat',10    f'{__spec__.parent}._exceptions',11    f'{__spec__.parent}._util',12    'os',13    'pyproject_hooks',14    'subprocess',15    'sys',16    'warnings',17    'zipfile',18]19 20import contextlib21import difflib22import os23import subprocess24import sys25import warnings26import zipfile27 28from typing import Any29 30import pyproject_hooks31 32from . import _ctx, env33from ._compat import tomllib34from ._exceptions import (35    BuildBackendException,36    BuildException,37    BuildSystemTableValidationError,38    TypoWarning,39)40from ._util import check_dependency, parse_wheel_filename41 42 43TYPE_CHECKING = False44 45if TYPE_CHECKING:46    from collections.abc import Iterator, Mapping, Sequence47 48    if sys.version_info < (3, 11):49        from typing_extensions import Self50    else:51        from typing import Self52 53    from ._types import ConfigSettings, Distribution, StrPath, SubprocessRunner54 55 56_DEFAULT_BACKEND = {57    'build-backend': 'setuptools.build_meta:__legacy__',58    'requires': ['setuptools >= 40.8.0'],59}60 61 62def _find_typo(dictionary: Mapping[str, str], expected: str) -> None:63    for obj in dictionary:64        if difflib.SequenceMatcher(None, expected, obj).ratio() >= 0.8:65            warnings.warn(66                f"Found '{obj}' in pyproject.toml, did you mean '{expected}'?",67                TypoWarning,68                stacklevel=2,69            )70 71 72def _validate_source_directory(source_dir: StrPath) -> None:73    if not os.path.isdir(source_dir):74        msg = f'Source {source_dir} is not a directory'75        raise BuildException(msg)76    pyproject_toml = os.path.join(source_dir, 'pyproject.toml')77    setup_py = os.path.join(source_dir, 'setup.py')78    if not os.path.exists(pyproject_toml) and not os.path.exists(setup_py):79        msg = f'Source {source_dir} does not appear to be a Python project: no pyproject.toml or setup.py'80        raise BuildException(msg)81 82 83def _read_pyproject_toml(path: StrPath) -> Mapping[str, Any]:84    try:85        with open(path, 'rb') as f:86            return tomllib.loads(f.read().decode())87    except FileNotFoundError:88        return {}89    except PermissionError as e:  # pragma: win32 no cover90        msg = f"{e.strerror}: '{e.filename}' "91        raise BuildException(msg) from None92    except tomllib.TOMLDecodeError as e:93        msg = f'Failed to parse {path}: {e} '94        raise BuildException(msg) from None95 96 97def _parse_build_system_table(pyproject_toml: Mapping[str, Any]) -> Mapping[str, Any]:98    # If pyproject.toml is missing (per PEP 517) or [build-system] is missing99    # (per PEP 518), use default values100    if 'build-system' not in pyproject_toml:101        _find_typo(pyproject_toml, 'build-system')102        return _DEFAULT_BACKEND103 104    build_system_table = dict(pyproject_toml['build-system'])105 106    # If [build-system] is present, it must have a ``requires`` field (per PEP 518)107    if 'requires' not in build_system_table:108        _find_typo(build_system_table, 'requires')109        msg = '`requires` is a required property'110        raise BuildSystemTableValidationError(msg)111    if not isinstance(build_system_table['requires'], list) or not all(112        isinstance(i, str) for i in build_system_table['requires']113    ):114        msg = '`requires` must be an array of strings'115        raise BuildSystemTableValidationError(msg)116 117    match build_system_table:118        case {'build-backend': str()}:119            pass120        case {'build-backend': _}:121            msg = '`build-backend` must be a string'122            raise BuildSystemTableValidationError(msg)123        case _:124            _find_typo(build_system_table, 'build-backend')125            # If ``build-backend`` is missing, inject the legacy setuptools backend126            # but leave ``requires`` intact to emulate pip127            build_system_table['build-backend'] = _DEFAULT_BACKEND['build-backend']128 129    match build_system_table:130        case {'backend-path': list() as backend_path} if all(isinstance(i, str) for i in backend_path):131            pass132        case {'backend-path': _}:133            msg = '`backend-path` must be an array of strings'134            raise BuildSystemTableValidationError(msg)135 136    unknown_props = build_system_table.keys() - {'requires', 'build-backend', 'backend-path'}137    if unknown_props:138        msg = f'Unknown properties: {", ".join(unknown_props)}'139        raise BuildSystemTableValidationError(msg)140 141    return build_system_table142 143 144def _validate_backend_path(source_dir: str, backend_paths: Sequence[str]) -> None:145    for path in backend_paths:146        resolved = os.path.join(source_dir, path)147        if not os.path.isdir(resolved):148            msg = f'`backend-path` entry {path!r} does not exist or is not a directory'149            raise BuildSystemTableValidationError(msg)150 151 152def _wrap_subprocess_runner(runner: SubprocessRunner, env: env.IsolatedEnv) -> SubprocessRunner:153    def _invoke_wrapped_runner(154        cmd: Sequence[str], cwd: str | None = None, extra_environ: Mapping[str, str] | None = None155    ) -> None:156        runner(cmd, cwd, {**(env.make_extra_environ() or {}), **(extra_environ or {})})157 158    return _invoke_wrapped_runner159 160 161class ProjectBuilder:162    """163    The PEP 517 consumer API.164    """165 166    def __init__(167        self,168        source_dir: StrPath,169        python_executable: str = sys.executable,170        runner: SubprocessRunner = pyproject_hooks.default_subprocess_runner,171    ) -> None:172        """173        :param source_dir: The source directory174        :param python_executable: The python executable where the backend lives175        :param runner: Runner for backend subprocesses176 177        The ``runner``, if provided, must accept the following arguments:178 179        - ``cmd``: a list of strings representing the command and arguments to180          execute, as would be passed to e.g. 'subprocess.check_call'.181        - ``cwd``: a string representing the working directory that must be182          used for the subprocess. Corresponds to the provided source_dir.183        - ``extra_environ``: a dict mapping environment variable names to values184          which must be set for the subprocess execution.185 186        The default runner simply calls the backend hooks in a subprocess, writing backend output187        to stdout/stderr.188        """189        self._source_dir: str = os.path.abspath(source_dir)190        _validate_source_directory(source_dir)191 192        self._python_executable = python_executable193        self._runner = runner194 195        pyproject_toml_path = os.path.join(source_dir, 'pyproject.toml')196        self._build_system = _parse_build_system_table(_read_pyproject_toml(pyproject_toml_path))197 198        self._backend = self._build_system['build-backend']199 200        if backend_paths := self._build_system.get('backend-path'):201            _validate_backend_path(self._source_dir, backend_paths)202 203        self._hook = pyproject_hooks.BuildBackendHookCaller(204            self._source_dir,205            self._backend,206            backend_path=self._build_system.get('backend-path'),207            python_executable=self._python_executable,208            runner=self._runner,209        )210 211    @classmethod212    def from_isolated_env(213        cls,214        env: env.IsolatedEnv,215        source_dir: StrPath,216        runner: SubprocessRunner = pyproject_hooks.default_subprocess_runner,217    ) -> Self:218        return cls(219            source_dir=source_dir,220            python_executable=env.python_executable,221            runner=_wrap_subprocess_runner(runner, env),222        )223 224    @property225    def source_dir(self) -> str:226        """Project source directory."""227        return self._source_dir228 229    @property230    def python_executable(self) -> str:231        """232        The Python executable used to invoke the backend.233        """234        return self._python_executable235 236    @property237    def build_system_requires(self) -> set[str]:238        """239        The dependencies defined in the ``pyproject.toml``'s240        ``build-system.requires`` field or the default build dependencies241        if ``pyproject.toml`` is missing or ``build-system`` is undefined.242        """243        return set(self._build_system['requires'])244 245    def get_requires_for_build(246        self,247        distribution: Distribution,248        config_settings: ConfigSettings | None = None,249    ) -> set[str]:250        """251        Return the dependencies defined by the backend in addition to252        :attr:`build_system_requires` for a given distribution.253 254        :param distribution: Distribution to get the dependencies of255            (``sdist`` or ``wheel``)256        :param config_settings: Config settings for the build backend257        """258        _ctx.log(f'Getting build dependencies for {distribution}...', kind=('step',))259        hook_name = f'get_requires_for_build_{distribution}'260        get_requires = getattr(self._hook, hook_name)261 262        with self._handle_backend(hook_name):263            return set(get_requires(config_settings))264 265    def check_dependencies(266        self,267        distribution: Distribution,268        config_settings: ConfigSettings | None = None,269    ) -> set[tuple[str, ...]]:270        """271        Return the dependencies which are not satisfied from the combined set of272        :attr:`build_system_requires` and :meth:`get_requires_for_build` for a given273        distribution.274 275        :param distribution: Distribution to check (``sdist`` or ``wheel``)276        :param config_settings: Config settings for the build backend277        :returns: Set of variable-length unmet dependency tuples278        """279        dependencies = self.get_requires_for_build(distribution, config_settings).union(self.build_system_requires)280        return {u for d in dependencies for u in check_dependency(d)}281 282    def prepare(283        self,284        distribution: Distribution,285        output_directory: StrPath,286        config_settings: ConfigSettings | None = None,287    ) -> str | None:288        """289        Prepare metadata for a distribution.290 291        :param distribution: Distribution to build (must be ``wheel``)292        :param output_directory: Directory to put the prepared metadata in293        :param config_settings: Config settings for the build backend294        :returns: The full path to the prepared metadata directory295        """296        _ctx.log(f'Getting metadata for {distribution}...', kind=('step',))297        try:298            return self._call_backend(299                f'prepare_metadata_for_build_{distribution}',300                output_directory,301                config_settings,302                _allow_fallback=False,303            )304        except BuildBackendException as exception:305            if isinstance(exception.exception, pyproject_hooks.HookMissing):306                return None307            raise308 309    def build(310        self,311        distribution: Distribution,312        output_directory: StrPath,313        config_settings: ConfigSettings | None = None,314        metadata_directory: str | None = None,315    ) -> str:316        """317        Build a distribution.318 319        :param distribution: Distribution to build (``sdist`` or ``wheel``)320        :param output_directory: Directory to put the built distribution in321        :param config_settings: Config settings for the build backend322        :param metadata_directory: If provided, should be the return value of a323            previous ``prepare`` call on the same ``distribution`` kind324        :returns: The full path to the built distribution325        """326        _ctx.log(f'Building {distribution}...', kind=('step',))327 328        kwargs = {} if metadata_directory is None else {'metadata_directory': metadata_directory}329        return self._call_backend(f'build_{distribution}', output_directory, config_settings, **kwargs)330 331    def metadata_path(self, output_directory: StrPath) -> str:332        """333        Generate the metadata directory of a distribution and return its path.334 335        If the backend does not support the ``prepare_metadata_for_build_wheel``336        hook, a wheel will be built and the metadata will be extracted from it.337 338        :param output_directory: Directory to put the metadata distribution in339        :returns: The path of the metadata directory340        """341        # prepare_metadata hook342        metadata = self.prepare('wheel', output_directory)343        if metadata is not None:344            return metadata345 346        # fallback to build_wheel hook347        wheel = self.build('wheel', output_directory)348        match = parse_wheel_filename(os.path.basename(wheel))349        if not match:350            msg = 'Invalid wheel'351            raise ValueError(msg)352        distinfo = f'{match["distribution"]}-{match["version"]}.dist-info'353        member_prefix = f'{distinfo}/'354        with zipfile.ZipFile(wheel) as w:355            w.extractall(356                output_directory,357                (member for member in w.namelist() if member.startswith(member_prefix)),358            )359        return os.path.join(output_directory, distinfo)360 361    def _call_backend(362        self, hook_name: str, outdir: StrPath, config_settings: ConfigSettings | None = None, **kwargs: Any363    ) -> str:364        outdir = os.path.abspath(outdir)365 366        callback = getattr(self._hook, hook_name)367 368        if os.path.exists(outdir):369            if not os.path.isdir(outdir):370                msg = f"Build path '{outdir}' exists and is not a directory"371                raise BuildException(msg)372        else:373            os.makedirs(outdir, exist_ok=True)374 375        with self._handle_backend(hook_name):376            basename: str = callback(outdir, config_settings, **kwargs)377 378        return os.path.join(outdir, basename)379 380    @contextlib.contextmanager381    def _handle_backend(self, hook: str) -> Iterator[None]:382        try:383            yield384        except pyproject_hooks.BackendUnavailable as exception:385            raise BuildBackendException(386                exception,387                f"Backend '{self._backend}' is not available.",388                sys.exc_info(),389            ) from None390        except subprocess.CalledProcessError as exception:391            raise BuildBackendException(exception, f'Backend subprocess exited when trying to invoke {hook}') from None392        except Exception as exception:393            raise BuildBackendException(exception, exc_info=sys.exc_info()) from None394 
codekingpro/portable-devtools · Team Ai