chore: 添加虚拟环境到仓库
- 添加 backend_service/venv 虚拟环境 - 包含所有Python依赖包 - 注意:虚拟环境约393MB,包含12655个文件
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import difflib
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import warnings
|
||||
import zipfile
|
||||
|
||||
from collections.abc import Iterator, Mapping, Sequence
|
||||
from typing import Any, TypeVar
|
||||
|
||||
import pyproject_hooks
|
||||
|
||||
from . import _ctx, env
|
||||
from ._compat import tomllib
|
||||
from ._exceptions import (
|
||||
BuildBackendException,
|
||||
BuildException,
|
||||
BuildSystemTableValidationError,
|
||||
TypoWarning,
|
||||
)
|
||||
from ._types import ConfigSettings, Distribution, StrPath, SubprocessRunner
|
||||
from ._util import check_dependency, parse_wheel_filename
|
||||
|
||||
|
||||
_TProjectBuilder = TypeVar('_TProjectBuilder', bound='ProjectBuilder')
|
||||
|
||||
|
||||
_DEFAULT_BACKEND = {
|
||||
'build-backend': 'setuptools.build_meta:__legacy__',
|
||||
'requires': ['setuptools >= 40.8.0'],
|
||||
}
|
||||
|
||||
|
||||
def _find_typo(dictionary: Mapping[str, str], expected: str) -> None:
|
||||
for obj in dictionary:
|
||||
if difflib.SequenceMatcher(None, expected, obj).ratio() >= 0.8:
|
||||
warnings.warn(
|
||||
f"Found '{obj}' in pyproject.toml, did you mean '{expected}'?",
|
||||
TypoWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
||||
def _validate_source_directory(source_dir: StrPath) -> None:
|
||||
if not os.path.isdir(source_dir):
|
||||
msg = f'Source {source_dir} is not a directory'
|
||||
raise BuildException(msg)
|
||||
pyproject_toml = os.path.join(source_dir, 'pyproject.toml')
|
||||
setup_py = os.path.join(source_dir, 'setup.py')
|
||||
if not os.path.exists(pyproject_toml) and not os.path.exists(setup_py):
|
||||
msg = f'Source {source_dir} does not appear to be a Python project: no pyproject.toml or setup.py'
|
||||
raise BuildException(msg)
|
||||
|
||||
|
||||
def _read_pyproject_toml(path: StrPath) -> Mapping[str, Any]:
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
return tomllib.loads(f.read().decode())
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
except PermissionError as e:
|
||||
msg = f"{e.strerror}: '{e.filename}' "
|
||||
raise BuildException(msg) from None
|
||||
except tomllib.TOMLDecodeError as e:
|
||||
msg = f'Failed to parse {path}: {e} '
|
||||
raise BuildException(msg) from None
|
||||
|
||||
|
||||
def _parse_build_system_table(pyproject_toml: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
# If pyproject.toml is missing (per PEP 517) or [build-system] is missing
|
||||
# (per PEP 518), use default values
|
||||
if 'build-system' not in pyproject_toml:
|
||||
_find_typo(pyproject_toml, 'build-system')
|
||||
return _DEFAULT_BACKEND
|
||||
|
||||
build_system_table = dict(pyproject_toml['build-system'])
|
||||
|
||||
# If [build-system] is present, it must have a ``requires`` field (per PEP 518)
|
||||
if 'requires' not in build_system_table:
|
||||
_find_typo(build_system_table, 'requires')
|
||||
msg = '`requires` is a required property'
|
||||
raise BuildSystemTableValidationError(msg)
|
||||
elif not isinstance(build_system_table['requires'], list) or not all(
|
||||
isinstance(i, str) for i in build_system_table['requires']
|
||||
):
|
||||
msg = '`requires` must be an array of strings'
|
||||
raise BuildSystemTableValidationError(msg)
|
||||
|
||||
if 'build-backend' not in build_system_table:
|
||||
_find_typo(build_system_table, 'build-backend')
|
||||
# If ``build-backend`` is missing, inject the legacy setuptools backend
|
||||
# but leave ``requires`` intact to emulate pip
|
||||
build_system_table['build-backend'] = _DEFAULT_BACKEND['build-backend']
|
||||
elif not isinstance(build_system_table['build-backend'], str):
|
||||
msg = '`build-backend` must be a string'
|
||||
raise BuildSystemTableValidationError(msg)
|
||||
|
||||
if 'backend-path' in build_system_table and (
|
||||
not isinstance(build_system_table['backend-path'], list)
|
||||
or not all(isinstance(i, str) for i in build_system_table['backend-path'])
|
||||
):
|
||||
msg = '`backend-path` must be an array of strings'
|
||||
raise BuildSystemTableValidationError(msg)
|
||||
|
||||
unknown_props = build_system_table.keys() - {'requires', 'build-backend', 'backend-path'}
|
||||
if unknown_props:
|
||||
msg = f'Unknown properties: {", ".join(unknown_props)}'
|
||||
raise BuildSystemTableValidationError(msg)
|
||||
|
||||
return build_system_table
|
||||
|
||||
|
||||
def _wrap_subprocess_runner(runner: SubprocessRunner, env: env.IsolatedEnv) -> SubprocessRunner:
|
||||
def _invoke_wrapped_runner(
|
||||
cmd: Sequence[str], cwd: str | None = None, extra_environ: Mapping[str, str] | None = None
|
||||
) -> None:
|
||||
runner(cmd, cwd, {**(env.make_extra_environ() or {}), **(extra_environ or {})})
|
||||
|
||||
return _invoke_wrapped_runner
|
||||
|
||||
|
||||
class ProjectBuilder:
|
||||
"""
|
||||
The PEP 517 consumer API.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
source_dir: StrPath,
|
||||
python_executable: str = sys.executable,
|
||||
runner: SubprocessRunner = pyproject_hooks.default_subprocess_runner,
|
||||
) -> None:
|
||||
"""
|
||||
:param source_dir: The source directory
|
||||
:param python_executable: The python executable where the backend lives
|
||||
:param runner: Runner for backend subprocesses
|
||||
|
||||
The ``runner``, if provided, must accept the following arguments:
|
||||
|
||||
- ``cmd``: a list of strings representing the command and arguments to
|
||||
execute, as would be passed to e.g. 'subprocess.check_call'.
|
||||
- ``cwd``: a string representing the working directory that must be
|
||||
used for the subprocess. Corresponds to the provided source_dir.
|
||||
- ``extra_environ``: a dict mapping environment variable names to values
|
||||
which must be set for the subprocess execution.
|
||||
|
||||
The default runner simply calls the backend hooks in a subprocess, writing backend output
|
||||
to stdout/stderr.
|
||||
"""
|
||||
self._source_dir: str = os.path.abspath(source_dir)
|
||||
_validate_source_directory(source_dir)
|
||||
|
||||
self._python_executable = python_executable
|
||||
self._runner = runner
|
||||
|
||||
pyproject_toml_path = os.path.join(source_dir, 'pyproject.toml')
|
||||
self._build_system = _parse_build_system_table(_read_pyproject_toml(pyproject_toml_path))
|
||||
|
||||
self._backend = self._build_system['build-backend']
|
||||
|
||||
self._hook = pyproject_hooks.BuildBackendHookCaller(
|
||||
self._source_dir,
|
||||
self._backend,
|
||||
backend_path=self._build_system.get('backend-path'),
|
||||
python_executable=self._python_executable,
|
||||
runner=self._runner,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_isolated_env(
|
||||
cls: type[_TProjectBuilder],
|
||||
env: env.IsolatedEnv,
|
||||
source_dir: StrPath,
|
||||
runner: SubprocessRunner = pyproject_hooks.default_subprocess_runner,
|
||||
) -> _TProjectBuilder:
|
||||
return cls(
|
||||
source_dir=source_dir,
|
||||
python_executable=env.python_executable,
|
||||
runner=_wrap_subprocess_runner(runner, env),
|
||||
)
|
||||
|
||||
@property
|
||||
def source_dir(self) -> str:
|
||||
"""Project source directory."""
|
||||
return self._source_dir
|
||||
|
||||
@property
|
||||
def python_executable(self) -> str:
|
||||
"""
|
||||
The Python executable used to invoke the backend.
|
||||
"""
|
||||
return self._python_executable
|
||||
|
||||
@property
|
||||
def build_system_requires(self) -> set[str]:
|
||||
"""
|
||||
The dependencies defined in the ``pyproject.toml``'s
|
||||
``build-system.requires`` field or the default build dependencies
|
||||
if ``pyproject.toml`` is missing or ``build-system`` is undefined.
|
||||
"""
|
||||
return set(self._build_system['requires'])
|
||||
|
||||
def get_requires_for_build(
|
||||
self,
|
||||
distribution: Distribution,
|
||||
config_settings: ConfigSettings | None = None,
|
||||
) -> set[str]:
|
||||
"""
|
||||
Return the dependencies defined by the backend in addition to
|
||||
:attr:`build_system_requires` for a given distribution.
|
||||
|
||||
:param distribution: Distribution to get the dependencies of
|
||||
(``sdist`` or ``wheel``)
|
||||
:param config_settings: Config settings for the build backend
|
||||
"""
|
||||
_ctx.log(f'Getting build dependencies for {distribution}...')
|
||||
hook_name = f'get_requires_for_build_{distribution}'
|
||||
get_requires = getattr(self._hook, hook_name)
|
||||
|
||||
with self._handle_backend(hook_name):
|
||||
return set(get_requires(config_settings))
|
||||
|
||||
def check_dependencies(
|
||||
self,
|
||||
distribution: Distribution,
|
||||
config_settings: ConfigSettings | None = None,
|
||||
) -> set[tuple[str, ...]]:
|
||||
"""
|
||||
Return the dependencies which are not satisfied from the combined set of
|
||||
:attr:`build_system_requires` and :meth:`get_requires_for_build` for a given
|
||||
distribution.
|
||||
|
||||
:param distribution: Distribution to check (``sdist`` or ``wheel``)
|
||||
:param config_settings: Config settings for the build backend
|
||||
:returns: Set of variable-length unmet dependency tuples
|
||||
"""
|
||||
dependencies = self.get_requires_for_build(distribution, config_settings).union(self.build_system_requires)
|
||||
return {u for d in dependencies for u in check_dependency(d)}
|
||||
|
||||
def prepare(
|
||||
self,
|
||||
distribution: Distribution,
|
||||
output_directory: StrPath,
|
||||
config_settings: ConfigSettings | None = None,
|
||||
) -> str | None:
|
||||
"""
|
||||
Prepare metadata for a distribution.
|
||||
|
||||
:param distribution: Distribution to build (must be ``wheel``)
|
||||
:param output_directory: Directory to put the prepared metadata in
|
||||
:param config_settings: Config settings for the build backend
|
||||
:returns: The full path to the prepared metadata directory
|
||||
"""
|
||||
_ctx.log(f'Getting metadata for {distribution}...')
|
||||
try:
|
||||
return self._call_backend(
|
||||
f'prepare_metadata_for_build_{distribution}',
|
||||
output_directory,
|
||||
config_settings,
|
||||
_allow_fallback=False,
|
||||
)
|
||||
except BuildBackendException as exception:
|
||||
if isinstance(exception.exception, pyproject_hooks.HookMissing):
|
||||
return None
|
||||
raise
|
||||
|
||||
def build(
|
||||
self,
|
||||
distribution: Distribution,
|
||||
output_directory: StrPath,
|
||||
config_settings: ConfigSettings | None = None,
|
||||
metadata_directory: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Build a distribution.
|
||||
|
||||
:param distribution: Distribution to build (``sdist`` or ``wheel``)
|
||||
:param output_directory: Directory to put the built distribution in
|
||||
:param config_settings: Config settings for the build backend
|
||||
:param metadata_directory: If provided, should be the return value of a
|
||||
previous ``prepare`` call on the same ``distribution`` kind
|
||||
:returns: The full path to the built distribution
|
||||
"""
|
||||
_ctx.log(f'Building {distribution}...')
|
||||
kwargs = {} if metadata_directory is None else {'metadata_directory': metadata_directory}
|
||||
return self._call_backend(f'build_{distribution}', output_directory, config_settings, **kwargs)
|
||||
|
||||
def metadata_path(self, output_directory: StrPath) -> str:
|
||||
"""
|
||||
Generate the metadata directory of a distribution and return its path.
|
||||
|
||||
If the backend does not support the ``prepare_metadata_for_build_wheel``
|
||||
hook, a wheel will be built and the metadata will be extracted from it.
|
||||
|
||||
:param output_directory: Directory to put the metadata distribution in
|
||||
:returns: The path of the metadata directory
|
||||
"""
|
||||
# prepare_metadata hook
|
||||
metadata = self.prepare('wheel', output_directory)
|
||||
if metadata is not None:
|
||||
return metadata
|
||||
|
||||
# fallback to build_wheel hook
|
||||
wheel = self.build('wheel', output_directory)
|
||||
match = parse_wheel_filename(os.path.basename(wheel))
|
||||
if not match:
|
||||
msg = 'Invalid wheel'
|
||||
raise ValueError(msg)
|
||||
distinfo = f'{match["distribution"]}-{match["version"]}.dist-info'
|
||||
member_prefix = f'{distinfo}/'
|
||||
with zipfile.ZipFile(wheel) as w:
|
||||
w.extractall(
|
||||
output_directory,
|
||||
(member for member in w.namelist() if member.startswith(member_prefix)),
|
||||
)
|
||||
return os.path.join(output_directory, distinfo)
|
||||
|
||||
def _call_backend(
|
||||
self, hook_name: str, outdir: StrPath, config_settings: ConfigSettings | None = None, **kwargs: Any
|
||||
) -> str:
|
||||
outdir = os.path.abspath(outdir)
|
||||
|
||||
callback = getattr(self._hook, hook_name)
|
||||
|
||||
if os.path.exists(outdir):
|
||||
if not os.path.isdir(outdir):
|
||||
msg = f"Build path '{outdir}' exists and is not a directory"
|
||||
raise BuildException(msg)
|
||||
else:
|
||||
os.makedirs(outdir)
|
||||
|
||||
with self._handle_backend(hook_name):
|
||||
basename: str = callback(outdir, config_settings, **kwargs)
|
||||
|
||||
return os.path.join(outdir, basename)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _handle_backend(self, hook: str) -> Iterator[None]:
|
||||
try:
|
||||
yield
|
||||
except pyproject_hooks.BackendUnavailable as exception:
|
||||
raise BuildBackendException(
|
||||
exception,
|
||||
f"Backend '{self._backend}' is not available.",
|
||||
sys.exc_info(),
|
||||
) from None
|
||||
except subprocess.CalledProcessError as exception:
|
||||
raise BuildBackendException(exception, f'Backend subprocess exited when trying to invoke {hook}') from None
|
||||
except Exception as exception:
|
||||
raise BuildBackendException(exception, exc_info=sys.exc_info()) from None
|
||||
Reference in New Issue
Block a user