mirror of https://github.com/pypa/pip
Add PEP 660 support (build_editable)
This commit is contained in:
parent
d91fecdb5a
commit
e5be3f796e
|
@ -0,0 +1,2 @@
|
|||
Support editable installs for projects that have a ``pyproject.toml`` and use a
|
||||
build backend that supports :pep:`660`.
|
|
@ -306,6 +306,12 @@ class InstallCommand(RequirementCommand):
|
|||
try:
|
||||
reqs = self.get_requirements(args, options, finder, session)
|
||||
|
||||
# Only when installing is it permitted to use PEP 660.
|
||||
# In other circumstances (pip wheel, pip download) we generate
|
||||
# regular (i.e. non editable) metadata and wheels.
|
||||
for req in reqs:
|
||||
req.permit_editable_wheels = True
|
||||
|
||||
reject_location_related_install_options(reqs, options.install_options)
|
||||
|
||||
preparer = self.make_requirement_preparer(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import logging
|
||||
from typing import Set, Tuple
|
||||
from typing import Iterable, Set, Tuple
|
||||
|
||||
from pip._internal.build_env import BuildEnvironment
|
||||
from pip._internal.distributions.base import AbstractDistribution
|
||||
|
@ -37,23 +37,17 @@ class SourceDistribution(AbstractDistribution):
|
|||
self.req.prepare_metadata()
|
||||
|
||||
def _setup_isolation(self, finder: PackageFinder) -> None:
|
||||
def _raise_conflicts(
|
||||
conflicting_with: str, conflicting_reqs: Set[Tuple[str, str]]
|
||||
) -> None:
|
||||
format_string = (
|
||||
"Some build dependencies for {requirement} "
|
||||
"conflict with {conflicting_with}: {description}."
|
||||
)
|
||||
error_message = format_string.format(
|
||||
requirement=self.req,
|
||||
conflicting_with=conflicting_with,
|
||||
description=", ".join(
|
||||
f"{installed} is incompatible with {wanted}"
|
||||
for installed, wanted in sorted(conflicting)
|
||||
),
|
||||
)
|
||||
raise InstallationError(error_message)
|
||||
self._prepare_build_backend(finder)
|
||||
# Install any extra build dependencies that the backend requests.
|
||||
# This must be done in a second pass, as the pyproject.toml
|
||||
# dependencies must be installed before we can call the backend.
|
||||
if self.req.editable and self.req.permit_editable_wheels:
|
||||
build_reqs = self._get_build_requires_editable()
|
||||
else:
|
||||
build_reqs = self._get_build_requires_wheel()
|
||||
self._install_build_reqs(finder, build_reqs)
|
||||
|
||||
def _prepare_build_backend(self, finder: PackageFinder) -> None:
|
||||
# Isolate in a BuildEnvironment and install the build-time
|
||||
# requirements.
|
||||
pyproject_requires = self.req.pyproject_requires
|
||||
|
@ -67,7 +61,7 @@ class SourceDistribution(AbstractDistribution):
|
|||
self.req.requirements_to_check
|
||||
)
|
||||
if conflicting:
|
||||
_raise_conflicts("PEP 517/518 supported requirements", conflicting)
|
||||
self._raise_conflicts("PEP 517/518 supported requirements", conflicting)
|
||||
if missing:
|
||||
logger.warning(
|
||||
"Missing build requirements in pyproject.toml for %s.",
|
||||
|
@ -78,19 +72,46 @@ class SourceDistribution(AbstractDistribution):
|
|||
"pip cannot fall back to setuptools without %s.",
|
||||
" and ".join(map(repr, sorted(missing))),
|
||||
)
|
||||
# Install any extra build dependencies that the backend requests.
|
||||
# This must be done in a second pass, as the pyproject.toml
|
||||
# dependencies must be installed before we can call the backend.
|
||||
|
||||
def _get_build_requires_wheel(self) -> Iterable[str]:
|
||||
with self.req.build_env:
|
||||
runner = runner_with_spinner_message("Getting requirements to build wheel")
|
||||
backend = self.req.pep517_backend
|
||||
assert backend is not None
|
||||
with backend.subprocess_runner(runner):
|
||||
reqs = backend.get_requires_for_build_wheel()
|
||||
return backend.get_requires_for_build_wheel()
|
||||
|
||||
def _get_build_requires_editable(self) -> Iterable[str]:
|
||||
with self.req.build_env:
|
||||
runner = runner_with_spinner_message(
|
||||
"Getting requirements to build editable"
|
||||
)
|
||||
backend = self.req.pep517_backend
|
||||
assert backend is not None
|
||||
with backend.subprocess_runner(runner):
|
||||
return backend.get_requires_for_build_editable()
|
||||
|
||||
def _install_build_reqs(self, finder: PackageFinder, reqs: Iterable[str]) -> None:
|
||||
conflicting, missing = self.req.build_env.check_requirements(reqs)
|
||||
if conflicting:
|
||||
_raise_conflicts("the backend dependencies", conflicting)
|
||||
self._raise_conflicts("the backend dependencies", conflicting)
|
||||
self.req.build_env.install_requirements(
|
||||
finder, missing, "normal", "Installing backend dependencies"
|
||||
)
|
||||
|
||||
def _raise_conflicts(
|
||||
self, conflicting_with: str, conflicting_reqs: Set[Tuple[str, str]]
|
||||
) -> None:
|
||||
format_string = (
|
||||
"Some build dependencies for {requirement} "
|
||||
"conflict with {conflicting_with}: {description}."
|
||||
)
|
||||
error_message = format_string.format(
|
||||
requirement=self.req,
|
||||
conflicting_with=conflicting_with,
|
||||
description=", ".join(
|
||||
f"{installed} is incompatible with {wanted}"
|
||||
for installed, wanted in sorted(conflicting_reqs)
|
||||
),
|
||||
)
|
||||
raise InstallationError(error_message)
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
"""Metadata generation logic for source distributions.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from pip._vendor.pep517.wrappers import Pep517HookCaller
|
||||
|
||||
from pip._internal.build_env import BuildEnvironment
|
||||
from pip._internal.utils.subprocess import runner_with_spinner_message
|
||||
from pip._internal.utils.temp_dir import TempDirectory
|
||||
|
||||
|
||||
def generate_editable_metadata(
|
||||
build_env: BuildEnvironment, backend: Pep517HookCaller
|
||||
) -> str:
|
||||
"""Generate metadata using mechanisms described in PEP 660.
|
||||
|
||||
Returns the generated metadata directory.
|
||||
"""
|
||||
metadata_tmpdir = TempDirectory(kind="modern-metadata", globally_managed=True)
|
||||
|
||||
metadata_dir = metadata_tmpdir.path
|
||||
|
||||
with build_env:
|
||||
# Note that Pep517HookCaller implements a fallback for
|
||||
# prepare_metadata_for_build_wheel/editable, so we don't have to
|
||||
# consider the possibility that this hook doesn't exist.
|
||||
runner = runner_with_spinner_message("Preparing editable metadata")
|
||||
with backend.subprocess_runner(runner):
|
||||
distinfo_dir = backend.prepare_metadata_for_build_editable(metadata_dir)
|
||||
|
||||
return os.path.join(metadata_dir, distinfo_dir)
|
|
@ -0,0 +1,46 @@
|
|||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from pip._vendor.pep517.wrappers import HookMissing, Pep517HookCaller
|
||||
|
||||
from pip._internal.utils.subprocess import runner_with_spinner_message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def build_wheel_editable(
|
||||
name: str,
|
||||
backend: Pep517HookCaller,
|
||||
metadata_directory: str,
|
||||
tempd: str,
|
||||
) -> Optional[str]:
|
||||
"""Build one InstallRequirement using the PEP 660 build process.
|
||||
|
||||
Returns path to wheel if successfully built. Otherwise, returns None.
|
||||
"""
|
||||
assert metadata_directory is not None
|
||||
try:
|
||||
logger.debug("Destination directory: %s", tempd)
|
||||
|
||||
runner = runner_with_spinner_message(
|
||||
f"Building editable for {name} (pyproject.toml)"
|
||||
)
|
||||
with backend.subprocess_runner(runner):
|
||||
try:
|
||||
wheel_name = backend.build_editable(
|
||||
tempd,
|
||||
metadata_directory=metadata_directory,
|
||||
)
|
||||
except HookMissing as e:
|
||||
logger.error(
|
||||
"Cannot build editable %s because the build "
|
||||
"backend does not have the %s hook",
|
||||
name,
|
||||
e,
|
||||
)
|
||||
return None
|
||||
except Exception:
|
||||
logger.error("Failed building editable for %s", name)
|
||||
return None
|
||||
return os.path.join(tempd, wheel_name)
|
|
@ -22,7 +22,6 @@ from pip._internal.exceptions import InstallationError
|
|||
from pip._internal.models.index import PyPI, TestPyPI
|
||||
from pip._internal.models.link import Link
|
||||
from pip._internal.models.wheel import Wheel
|
||||
from pip._internal.pyproject import make_pyproject_path
|
||||
from pip._internal.req.req_file import ParsedRequirement
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
from pip._internal.utils.filetypes import is_archive_file
|
||||
|
@ -75,21 +74,6 @@ def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
|
|||
url_no_extras, extras = _strip_extras(url)
|
||||
|
||||
if os.path.isdir(url_no_extras):
|
||||
setup_py = os.path.join(url_no_extras, "setup.py")
|
||||
setup_cfg = os.path.join(url_no_extras, "setup.cfg")
|
||||
if not os.path.exists(setup_py) and not os.path.exists(setup_cfg):
|
||||
msg = (
|
||||
'File "setup.py" or "setup.cfg" not found. Directory cannot be '
|
||||
"installed in editable mode: {}".format(os.path.abspath(url_no_extras))
|
||||
)
|
||||
pyproject_path = make_pyproject_path(url_no_extras)
|
||||
if os.path.isfile(pyproject_path):
|
||||
msg += (
|
||||
'\n(A "pyproject.toml" file was found, but editable '
|
||||
"mode currently requires a setuptools-based build.)"
|
||||
)
|
||||
raise InstallationError(msg)
|
||||
|
||||
# Treating it as code that has already been checked out
|
||||
url_no_extras = path_to_url(url_no_extras)
|
||||
|
||||
|
@ -197,6 +181,7 @@ def install_req_from_editable(
|
|||
options: Optional[Dict[str, Any]] = None,
|
||||
constraint: bool = False,
|
||||
user_supplied: bool = False,
|
||||
permit_editable_wheels: bool = False,
|
||||
) -> InstallRequirement:
|
||||
|
||||
parts = parse_req_from_editable(editable_req)
|
||||
|
@ -206,6 +191,7 @@ def install_req_from_editable(
|
|||
comes_from=comes_from,
|
||||
user_supplied=user_supplied,
|
||||
editable=True,
|
||||
permit_editable_wheels=permit_editable_wheels,
|
||||
link=parts.link,
|
||||
constraint=constraint,
|
||||
use_pep517=use_pep517,
|
||||
|
|
|
@ -16,7 +16,7 @@ from pip._vendor.packaging.specifiers import SpecifierSet
|
|||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
from pip._vendor.packaging.version import Version
|
||||
from pip._vendor.packaging.version import parse as parse_version
|
||||
from pip._vendor.pep517.wrappers import Pep517HookCaller
|
||||
from pip._vendor.pep517.wrappers import HookMissing, Pep517HookCaller
|
||||
from pip._vendor.pkg_resources import Distribution
|
||||
|
||||
from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
|
||||
|
@ -24,6 +24,7 @@ from pip._internal.exceptions import InstallationError
|
|||
from pip._internal.locations import get_scheme
|
||||
from pip._internal.models.link import Link
|
||||
from pip._internal.operations.build.metadata import generate_metadata
|
||||
from pip._internal.operations.build.metadata_editable import generate_editable_metadata
|
||||
from pip._internal.operations.build.metadata_legacy import (
|
||||
generate_metadata as generate_metadata_legacy,
|
||||
)
|
||||
|
@ -36,7 +37,10 @@ from pip._internal.operations.install.wheel import install_wheel
|
|||
from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path
|
||||
from pip._internal.req.req_uninstall import UninstallPathSet
|
||||
from pip._internal.utils.deprecation import deprecated
|
||||
from pip._internal.utils.direct_url_helpers import direct_url_from_link
|
||||
from pip._internal.utils.direct_url_helpers import (
|
||||
direct_url_for_editable,
|
||||
direct_url_from_link,
|
||||
)
|
||||
from pip._internal.utils.hashes import Hashes
|
||||
from pip._internal.utils.logging import indent_log
|
||||
from pip._internal.utils.misc import (
|
||||
|
@ -105,12 +109,14 @@ class InstallRequirement:
|
|||
constraint: bool = False,
|
||||
extras: Collection[str] = (),
|
||||
user_supplied: bool = False,
|
||||
permit_editable_wheels: bool = False,
|
||||
) -> None:
|
||||
assert req is None or isinstance(req, Requirement), req
|
||||
self.req = req
|
||||
self.comes_from = comes_from
|
||||
self.constraint = constraint
|
||||
self.editable = editable
|
||||
self.permit_editable_wheels = permit_editable_wheels
|
||||
self.legacy_install_reason: Optional[int] = None
|
||||
|
||||
# source_dir is the local directory where the linked requirement is
|
||||
|
@ -191,6 +197,11 @@ class InstallRequirement:
|
|||
# but after loading this flag should be treated as read only.
|
||||
self.use_pep517 = use_pep517
|
||||
|
||||
# supports_pyproject_editable will be set to True or False when we try
|
||||
# to prepare editable metadata or build an editable wheel. None means
|
||||
# "we don't know yet".
|
||||
self.supports_pyproject_editable: Optional[bool] = None
|
||||
|
||||
# This requirement needs more preparation before it can be built
|
||||
self.needs_more_preparation = False
|
||||
|
||||
|
@ -455,6 +466,13 @@ class InstallRequirement:
|
|||
|
||||
return setup_py
|
||||
|
||||
@property
|
||||
def setup_cfg_path(self) -> str:
|
||||
assert self.source_dir, f"No source dir for {self}"
|
||||
setup_cfg = os.path.join(self.unpacked_source_directory, "setup.cfg")
|
||||
|
||||
return setup_cfg
|
||||
|
||||
@property
|
||||
def pyproject_toml_path(self) -> str:
|
||||
assert self.source_dir, f"No source dir for {self}"
|
||||
|
@ -486,29 +504,75 @@ class InstallRequirement:
|
|||
backend_path=backend_path,
|
||||
)
|
||||
|
||||
def _generate_metadata(self) -> str:
|
||||
def _generate_editable_metadata(self) -> str:
|
||||
"""Invokes metadata generator functions, with the required arguments."""
|
||||
if not self.use_pep517:
|
||||
assert self.unpacked_source_directory
|
||||
|
||||
if not os.path.exists(self.setup_py_path):
|
||||
raise InstallationError(
|
||||
f'File "setup.py" not found for legacy project {self}.'
|
||||
if self.use_pep517:
|
||||
assert self.pep517_backend is not None
|
||||
try:
|
||||
metadata_directory = generate_editable_metadata(
|
||||
build_env=self.build_env,
|
||||
backend=self.pep517_backend,
|
||||
)
|
||||
|
||||
return generate_metadata_legacy(
|
||||
build_env=self.build_env,
|
||||
setup_py_path=self.setup_py_path,
|
||||
source_dir=self.unpacked_source_directory,
|
||||
isolated=self.isolated,
|
||||
details=self.name or f"from {self.link}",
|
||||
except HookMissing as e:
|
||||
self.supports_pyproject_editable = False
|
||||
if not os.path.exists(self.setup_py_path) and not os.path.exists(
|
||||
self.setup_cfg_path
|
||||
):
|
||||
raise InstallationError(
|
||||
f"Project {self} has a 'pyproject.toml' and its build "
|
||||
f"backend is missing the {e} hook. Since it does not "
|
||||
f"have a 'setup.py' nor a 'setup.cfg', "
|
||||
f"it cannot be installed in editable mode. "
|
||||
f"Consider using a build backend that supports PEP 660."
|
||||
)
|
||||
# At this point we have determined that the build_editable hook
|
||||
# is missing, and there is a setup.py or setup.cfg
|
||||
# so we fallback to the legacy metadata generation
|
||||
else:
|
||||
self.supports_pyproject_editable = True
|
||||
return metadata_directory
|
||||
elif not os.path.exists(self.setup_py_path) and not os.path.exists(
|
||||
self.setup_cfg_path
|
||||
):
|
||||
raise InstallationError(
|
||||
f"File 'setup.py' or 'setup.cfg' not found "
|
||||
f"for legacy project {self}. "
|
||||
f"It cannot be installed in editable mode."
|
||||
)
|
||||
|
||||
assert self.pep517_backend is not None
|
||||
|
||||
return generate_metadata(
|
||||
return generate_metadata_legacy(
|
||||
build_env=self.build_env,
|
||||
backend=self.pep517_backend,
|
||||
setup_py_path=self.setup_py_path,
|
||||
source_dir=self.unpacked_source_directory,
|
||||
isolated=self.isolated,
|
||||
details=self.name or f"from {self.link}",
|
||||
)
|
||||
|
||||
def _generate_metadata(self) -> str:
|
||||
"""Invokes metadata generator functions, with the required arguments."""
|
||||
if self.use_pep517:
|
||||
assert self.pep517_backend is not None
|
||||
try:
|
||||
return generate_metadata(
|
||||
build_env=self.build_env,
|
||||
backend=self.pep517_backend,
|
||||
)
|
||||
except HookMissing as e:
|
||||
raise InstallationError(
|
||||
f"Project {self} has a pyproject.toml but its build "
|
||||
f"backend is missing the required {e} hook."
|
||||
)
|
||||
elif not os.path.exists(self.setup_py_path):
|
||||
raise InstallationError(
|
||||
f"File 'setup.py' not found for legacy project {self}."
|
||||
)
|
||||
|
||||
return generate_metadata_legacy(
|
||||
build_env=self.build_env,
|
||||
setup_py_path=self.setup_py_path,
|
||||
source_dir=self.unpacked_source_directory,
|
||||
isolated=self.isolated,
|
||||
details=self.name or f"from {self.link}",
|
||||
)
|
||||
|
||||
def prepare_metadata(self) -> None:
|
||||
|
@ -520,7 +584,10 @@ class InstallRequirement:
|
|||
assert self.source_dir
|
||||
|
||||
with indent_log():
|
||||
self.metadata_directory = self._generate_metadata()
|
||||
if self.editable and self.permit_editable_wheels:
|
||||
self.metadata_directory = self._generate_editable_metadata()
|
||||
else:
|
||||
self.metadata_directory = self._generate_metadata()
|
||||
|
||||
# Act on the newly generated metadata, based on the name and version.
|
||||
if not self.name:
|
||||
|
@ -728,7 +795,7 @@ class InstallRequirement:
|
|||
)
|
||||
|
||||
global_options = global_options if global_options is not None else []
|
||||
if self.editable:
|
||||
if self.editable and not self.is_wheel:
|
||||
install_editable_legacy(
|
||||
install_options,
|
||||
global_options,
|
||||
|
@ -747,7 +814,9 @@ class InstallRequirement:
|
|||
if self.is_wheel:
|
||||
assert self.local_file_path
|
||||
direct_url = None
|
||||
if self.original_link:
|
||||
if self.editable:
|
||||
direct_url = direct_url_for_editable(self.unpacked_source_directory)
|
||||
elif self.original_link:
|
||||
direct_url = direct_url_from_link(
|
||||
self.original_link,
|
||||
self.source_dir,
|
||||
|
|
|
@ -82,6 +82,7 @@ def make_install_req_from_editable(
|
|||
use_pep517=template.use_pep517,
|
||||
isolated=template.isolated,
|
||||
constraint=template.constraint,
|
||||
permit_editable_wheels=template.permit_editable_wheels,
|
||||
options=dict(
|
||||
install_options=template.install_options,
|
||||
global_options=template.global_options,
|
||||
|
|
|
@ -2,6 +2,7 @@ from typing import Optional
|
|||
|
||||
from pip._internal.models.direct_url import ArchiveInfo, DirectUrl, DirInfo, VcsInfo
|
||||
from pip._internal.models.link import Link
|
||||
from pip._internal.utils.urls import path_to_url
|
||||
from pip._internal.vcs import vcs
|
||||
|
||||
|
||||
|
@ -28,6 +29,13 @@ def direct_url_as_pep440_direct_reference(direct_url: DirectUrl, name: str) -> s
|
|||
return requirement
|
||||
|
||||
|
||||
def direct_url_for_editable(source_dir: str) -> DirectUrl:
|
||||
return DirectUrl(
|
||||
url=path_to_url(source_dir),
|
||||
info=DirInfo(editable=True),
|
||||
)
|
||||
|
||||
|
||||
def direct_url_from_link(
|
||||
link: Link, source_dir: Optional[str] = None, link_is_in_wheel_cache: bool = False
|
||||
) -> DirectUrl:
|
||||
|
|
|
@ -16,6 +16,7 @@ from pip._internal.metadata import FilesystemWheel, get_wheel_distribution
|
|||
from pip._internal.models.link import Link
|
||||
from pip._internal.models.wheel import Wheel
|
||||
from pip._internal.operations.build.wheel import build_wheel_pep517
|
||||
from pip._internal.operations.build.wheel_editable import build_wheel_editable
|
||||
from pip._internal.operations.build.wheel_legacy import build_wheel_legacy
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
from pip._internal.utils.logging import indent_log
|
||||
|
@ -66,7 +67,13 @@ def _should_build(
|
|||
# From this point, this concerns the pip install command only
|
||||
# (need_wheel=False).
|
||||
|
||||
if req.editable or not req.source_dir:
|
||||
if not req.source_dir:
|
||||
return False
|
||||
|
||||
if req.editable:
|
||||
if req.use_pep517 and req.supports_pyproject_editable is not False:
|
||||
return True
|
||||
# we don't build legacy editable requirements
|
||||
return False
|
||||
|
||||
if req.use_pep517:
|
||||
|
@ -194,16 +201,19 @@ def _build_one(
|
|||
verify: bool,
|
||||
build_options: List[str],
|
||||
global_options: List[str],
|
||||
editable: bool,
|
||||
) -> Optional[str]:
|
||||
"""Build one wheel.
|
||||
|
||||
:return: The filename of the built wheel, or None if the build failed.
|
||||
"""
|
||||
artifact = "editable" if editable else "wheel"
|
||||
try:
|
||||
ensure_dir(output_dir)
|
||||
except OSError as e:
|
||||
logger.warning(
|
||||
"Building wheel for %s failed: %s",
|
||||
"Building %s for %s failed: %s",
|
||||
artifact,
|
||||
req.name,
|
||||
e,
|
||||
)
|
||||
|
@ -212,13 +222,13 @@ def _build_one(
|
|||
# Install build deps into temporary directory (PEP 518)
|
||||
with req.build_env:
|
||||
wheel_path = _build_one_inside_env(
|
||||
req, output_dir, build_options, global_options
|
||||
req, output_dir, build_options, global_options, editable
|
||||
)
|
||||
if wheel_path and verify:
|
||||
try:
|
||||
_verify_one(req, wheel_path)
|
||||
except (InvalidWheelFilename, UnsupportedWheel) as e:
|
||||
logger.warning("Built wheel for %s is invalid: %s", req.name, e)
|
||||
logger.warning("Built %s for %s is invalid: %s", artifact, req.name, e)
|
||||
return None
|
||||
return wheel_path
|
||||
|
||||
|
@ -228,6 +238,7 @@ def _build_one_inside_env(
|
|||
output_dir: str,
|
||||
build_options: List[str],
|
||||
global_options: List[str],
|
||||
editable: bool,
|
||||
) -> Optional[str]:
|
||||
with TempDirectory(kind="wheel") as temp_dir:
|
||||
assert req.name
|
||||
|
@ -242,12 +253,20 @@ def _build_one_inside_env(
|
|||
logger.warning(
|
||||
"Ignoring --build-option when building %s using PEP 517", req.name
|
||||
)
|
||||
wheel_path = build_wheel_pep517(
|
||||
name=req.name,
|
||||
backend=req.pep517_backend,
|
||||
metadata_directory=req.metadata_directory,
|
||||
tempd=temp_dir.path,
|
||||
)
|
||||
if editable:
|
||||
wheel_path = build_wheel_editable(
|
||||
name=req.name,
|
||||
backend=req.pep517_backend,
|
||||
metadata_directory=req.metadata_directory,
|
||||
tempd=temp_dir.path,
|
||||
)
|
||||
else:
|
||||
wheel_path = build_wheel_pep517(
|
||||
name=req.name,
|
||||
backend=req.pep517_backend,
|
||||
metadata_directory=req.metadata_directory,
|
||||
tempd=temp_dir.path,
|
||||
)
|
||||
else:
|
||||
wheel_path = build_wheel_legacy(
|
||||
name=req.name,
|
||||
|
@ -324,9 +343,15 @@ def build(
|
|||
with indent_log():
|
||||
build_successes, build_failures = [], []
|
||||
for req in requirements:
|
||||
assert req.name
|
||||
cache_dir = _get_cache_dir(req, wheel_cache)
|
||||
wheel_file = _build_one(
|
||||
req, cache_dir, verify, build_options, global_options
|
||||
req,
|
||||
cache_dir,
|
||||
verify,
|
||||
build_options,
|
||||
global_options,
|
||||
req.editable and req.permit_editable_wheels,
|
||||
)
|
||||
if wheel_file:
|
||||
# Update the link for this.
|
||||
|
|
|
@ -644,7 +644,7 @@ def test_install_from_local_directory_with_no_setup_py(script, data):
|
|||
assert "Neither 'setup.py' nor 'pyproject.toml' found." in result.stderr
|
||||
|
||||
|
||||
def test_editable_install__local_dir_no_setup_py(script, data, deprecated_python):
|
||||
def test_editable_install__local_dir_no_setup_py(script, data):
|
||||
"""
|
||||
Test installing in editable mode from a local directory with no setup.py.
|
||||
"""
|
||||
|
@ -652,16 +652,12 @@ def test_editable_install__local_dir_no_setup_py(script, data, deprecated_python
|
|||
assert not result.files_created
|
||||
|
||||
msg = result.stderr
|
||||
if deprecated_python:
|
||||
assert 'File "setup.py" or "setup.cfg" not found. ' in msg
|
||||
else:
|
||||
assert msg.startswith('ERROR: File "setup.py" or "setup.cfg" not found. ')
|
||||
assert msg.startswith("ERROR: File 'setup.py' or 'setup.cfg' not found ")
|
||||
assert "cannot be installed in editable mode" in msg
|
||||
assert "pyproject.toml" not in msg
|
||||
|
||||
|
||||
def test_editable_install__local_dir_no_setup_py_with_pyproject(
|
||||
script, deprecated_python
|
||||
):
|
||||
def test_editable_install__local_dir_no_setup_py_with_pyproject(script):
|
||||
"""
|
||||
Test installing in editable mode from a local directory with no setup.py
|
||||
but that does have pyproject.toml.
|
||||
|
@ -675,11 +671,9 @@ def test_editable_install__local_dir_no_setup_py_with_pyproject(
|
|||
assert not result.files_created
|
||||
|
||||
msg = result.stderr
|
||||
if deprecated_python:
|
||||
assert 'File "setup.py" or "setup.cfg" not found. ' in msg
|
||||
else:
|
||||
assert msg.startswith('ERROR: File "setup.py" or "setup.cfg" not found. ')
|
||||
assert 'A "pyproject.toml" file was found' in msg
|
||||
assert "has a 'pyproject.toml'" in msg
|
||||
assert "does not have a 'setup.py' nor a 'setup.cfg'" in msg
|
||||
assert "cannot be installed in editable mode" in msg
|
||||
|
||||
|
||||
@pytest.mark.network
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
import os
|
||||
|
||||
import tomli_w
|
||||
|
||||
from pip._internal.utils.urls import path_to_url
|
||||
|
||||
SETUP_PY = """
|
||||
from setuptools import setup
|
||||
|
||||
setup()
|
||||
"""
|
||||
|
||||
SETUP_CFG = """
|
||||
[metadata]
|
||||
name = project
|
||||
version = 1.0.0
|
||||
"""
|
||||
|
||||
BACKEND_WITHOUT_PEP660 = """
|
||||
from setuptools.build_meta import (
|
||||
build_wheel as _build_wheel,
|
||||
prepare_metadata_for_build_wheel as _prepare_metadata_for_build_wheel,
|
||||
get_requires_for_build_wheel as _get_requires_for_build_wheel,
|
||||
)
|
||||
|
||||
def get_requires_for_build_wheel(config_settings=None):
|
||||
with open("log.txt", "a") as f:
|
||||
print(":get_requires_for_build_wheel called", file=f)
|
||||
return _get_requires_for_build_wheel(config_settings)
|
||||
|
||||
def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
|
||||
with open("log.txt", "a") as f:
|
||||
print(":prepare_metadata_for_build_wheel called", file=f)
|
||||
return _prepare_metadata_for_build_wheel(metadata_directory, config_settings)
|
||||
|
||||
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
|
||||
with open("log.txt", "a") as f:
|
||||
print(":build_wheel called", file=f)
|
||||
return _build_wheel(wheel_directory, config_settings, metadata_directory)
|
||||
"""
|
||||
|
||||
# fmt: off
|
||||
BACKEND_WITH_PEP660 = BACKEND_WITHOUT_PEP660 + """
|
||||
def get_requires_for_build_editable(config_settings=None):
|
||||
with open("log.txt", "a") as f:
|
||||
print(":get_requires_for_build_editable called", file=f)
|
||||
return _get_requires_for_build_wheel(config_settings)
|
||||
|
||||
def prepare_metadata_for_build_editable(metadata_directory, config_settings=None):
|
||||
with open("log.txt", "a") as f:
|
||||
print(":prepare_metadata_for_build_editable called", file=f)
|
||||
return _prepare_metadata_for_build_wheel(metadata_directory, config_settings)
|
||||
|
||||
def build_editable(wheel_directory, config_settings=None, metadata_directory=None):
|
||||
with open("log.txt", "a") as f:
|
||||
print(":build_editable called", file=f)
|
||||
return _build_wheel(wheel_directory, config_settings, metadata_directory)
|
||||
"""
|
||||
# fmt: on
|
||||
|
||||
|
||||
def _make_project(tmpdir, backend_code, with_setup_py):
|
||||
project_dir = tmpdir / "project"
|
||||
project_dir.mkdir()
|
||||
project_dir.joinpath("setup.cfg").write_text(SETUP_CFG)
|
||||
if with_setup_py:
|
||||
project_dir.joinpath("setup.py").write_text(SETUP_PY)
|
||||
if backend_code:
|
||||
buildsys = {"requires": ["setuptools", "wheel"]}
|
||||
buildsys["build-backend"] = "test_backend"
|
||||
buildsys["backend-path"] = ["."]
|
||||
data = tomli_w.dumps({"build-system": buildsys})
|
||||
project_dir.joinpath("pyproject.toml").write_text(data)
|
||||
project_dir.joinpath("test_backend.py").write_text(backend_code)
|
||||
project_dir.joinpath("log.txt").touch()
|
||||
return project_dir
|
||||
|
||||
|
||||
def _assert_hook_called(project_dir, hook):
|
||||
log = project_dir.joinpath("log.txt").read_text()
|
||||
assert f":{hook} called" in log, f"{hook} has not been called"
|
||||
|
||||
|
||||
def _assert_hook_not_called(project_dir, hook):
|
||||
log = project_dir.joinpath("log.txt").read_text()
|
||||
assert f":{hook} called" not in log, f"{hook} should not have been called"
|
||||
|
||||
|
||||
def test_install_pep517_basic(tmpdir, script, with_wheel):
|
||||
"""
|
||||
Check that the test harness we have in this file is sane.
|
||||
"""
|
||||
project_dir = _make_project(tmpdir, BACKEND_WITHOUT_PEP660, with_setup_py=False)
|
||||
script.pip(
|
||||
"install",
|
||||
"--use-feature=in-tree-build",
|
||||
"--no-index",
|
||||
"--no-build-isolation",
|
||||
project_dir,
|
||||
)
|
||||
_assert_hook_called(project_dir, "prepare_metadata_for_build_wheel")
|
||||
_assert_hook_called(project_dir, "build_wheel")
|
||||
|
||||
|
||||
def test_install_pep660_basic(tmpdir, script, with_wheel):
|
||||
"""
|
||||
Test with backend that supports build_editable.
|
||||
"""
|
||||
project_dir = _make_project(tmpdir, BACKEND_WITH_PEP660, with_setup_py=False)
|
||||
result = script.pip(
|
||||
"install",
|
||||
"--no-index",
|
||||
"--no-build-isolation",
|
||||
"--editable",
|
||||
project_dir,
|
||||
)
|
||||
_assert_hook_called(project_dir, "prepare_metadata_for_build_editable")
|
||||
_assert_hook_called(project_dir, "build_editable")
|
||||
assert (
|
||||
result.test_env.site_packages.joinpath("project.egg-link")
|
||||
not in result.files_created
|
||||
), "a .egg-link file should not have been created"
|
||||
|
||||
|
||||
def test_install_no_pep660_setup_py_fallback(tmpdir, script, with_wheel):
|
||||
"""
|
||||
Test that we fall back to setuptools develop when using a backend that
|
||||
does not support build_editable .
|
||||
"""
|
||||
project_dir = _make_project(tmpdir, BACKEND_WITHOUT_PEP660, with_setup_py=True)
|
||||
result = script.pip(
|
||||
"install",
|
||||
"--no-index",
|
||||
"--no-build-isolation",
|
||||
"--editable",
|
||||
project_dir,
|
||||
allow_stderr_warning=False,
|
||||
)
|
||||
assert (
|
||||
result.test_env.site_packages.joinpath("project.egg-link")
|
||||
in result.files_created
|
||||
), "a .egg-link file should have been created"
|
||||
|
||||
|
||||
def test_install_no_pep660_setup_cfg_fallback(tmpdir, script, with_wheel):
|
||||
"""
|
||||
Test that we fall back to setuptools develop when using a backend that
|
||||
does not support build_editable .
|
||||
"""
|
||||
project_dir = _make_project(tmpdir, BACKEND_WITHOUT_PEP660, with_setup_py=False)
|
||||
result = script.pip(
|
||||
"install",
|
||||
"--no-index",
|
||||
"--no-build-isolation",
|
||||
"--editable",
|
||||
project_dir,
|
||||
allow_stderr_warning=False,
|
||||
)
|
||||
print(result.stdout, result.stderr)
|
||||
assert (
|
||||
result.test_env.site_packages.joinpath("project.egg-link")
|
||||
in result.files_created
|
||||
), ".egg-link file should have been created"
|
||||
|
||||
|
||||
def test_wheel_editable_pep660_basic(tmpdir, script, with_wheel):
|
||||
"""
|
||||
Test 'pip wheel' of an editable pep 660 project.
|
||||
It must *not* call prepare_metadata_for_build_editable.
|
||||
"""
|
||||
project_dir = _make_project(tmpdir, BACKEND_WITH_PEP660, with_setup_py=False)
|
||||
wheel_dir = tmpdir / "dist"
|
||||
script.pip(
|
||||
"wheel",
|
||||
"--no-index",
|
||||
"--no-build-isolation",
|
||||
"--editable",
|
||||
project_dir,
|
||||
"-w",
|
||||
wheel_dir,
|
||||
)
|
||||
_assert_hook_not_called(project_dir, "prepare_metadata_for_build_editable")
|
||||
_assert_hook_not_called(project_dir, "build_editable")
|
||||
_assert_hook_called(project_dir, "prepare_metadata_for_build_wheel")
|
||||
_assert_hook_called(project_dir, "build_wheel")
|
||||
assert len(os.listdir(str(wheel_dir))) == 1, "a wheel should have been created"
|
||||
|
||||
|
||||
def test_download_editable_pep660_basic(tmpdir, script, with_wheel):
|
||||
"""
|
||||
Test 'pip download' of an editable pep 660 project.
|
||||
It must *not* call prepare_metadata_for_build_editable.
|
||||
"""
|
||||
project_dir = _make_project(tmpdir, BACKEND_WITH_PEP660, with_setup_py=False)
|
||||
reqs_file = tmpdir / "requirements.txt"
|
||||
reqs_file.write_text(f"-e {path_to_url(project_dir)}\n")
|
||||
download_dir = tmpdir / "download"
|
||||
script.pip(
|
||||
"download",
|
||||
"--no-index",
|
||||
"--no-build-isolation",
|
||||
"-r",
|
||||
reqs_file,
|
||||
"-d",
|
||||
download_dir,
|
||||
)
|
||||
_assert_hook_not_called(project_dir, "prepare_metadata_for_build_editable")
|
||||
_assert_hook_called(project_dir, "prepare_metadata_for_build_wheel")
|
||||
assert len(os.listdir(str(download_dir))) == 1, "a zip should have been created"
|
|
@ -39,6 +39,7 @@ class ReqMock:
|
|||
constraint: bool = False,
|
||||
source_dir: Optional[str] = "/tmp/pip-install-123/pendulum",
|
||||
use_pep517: bool = True,
|
||||
supports_pyproject_editable: Optional[bool] = None,
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.is_wheel = is_wheel
|
||||
|
@ -47,6 +48,7 @@ class ReqMock:
|
|||
self.constraint = constraint
|
||||
self.source_dir = source_dir
|
||||
self.use_pep517 = use_pep517
|
||||
self.supports_pyproject_editable = supports_pyproject_editable
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -63,8 +65,18 @@ class ReqMock:
|
|||
(ReqMock(constraint=True), False, False),
|
||||
# We don't build reqs that are already wheels.
|
||||
(ReqMock(is_wheel=True), False, False),
|
||||
# We don't build editables.
|
||||
(ReqMock(editable=True), False, False),
|
||||
(ReqMock(editable=True, use_pep517=False), False, False),
|
||||
(ReqMock(editable=True, use_pep517=True), False, True),
|
||||
(
|
||||
ReqMock(editable=True, use_pep517=True, supports_pyproject_editable=True),
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
ReqMock(editable=True, use_pep517=True, supports_pyproject_editable=False),
|
||||
False,
|
||||
False,
|
||||
),
|
||||
(ReqMock(source_dir=None), False, False),
|
||||
# By default (i.e. when binaries are allowed), VCS requirements
|
||||
# should be built in install mode.
|
||||
|
@ -108,7 +120,8 @@ def test_should_build_for_install_command(
|
|||
(ReqMock(), True),
|
||||
(ReqMock(constraint=True), False),
|
||||
(ReqMock(is_wheel=True), False),
|
||||
(ReqMock(editable=True), True),
|
||||
(ReqMock(editable=True, use_pep517=False), True),
|
||||
(ReqMock(editable=True, use_pep517=True), True),
|
||||
(ReqMock(source_dir=None), True),
|
||||
(ReqMock(link=Link("git+https://g.c/org/repo")), True),
|
||||
],
|
||||
|
@ -145,7 +158,8 @@ def test_should_build_legacy_wheel_installed(is_wheel_installed: mock.Mock) -> N
|
|||
@pytest.mark.parametrize(
|
||||
"req, expected",
|
||||
[
|
||||
(ReqMock(editable=True), False),
|
||||
(ReqMock(editable=True, use_pep517=False), False),
|
||||
(ReqMock(editable=True, use_pep517=True), False),
|
||||
(ReqMock(source_dir=None), False),
|
||||
(ReqMock(link=Link("git+https://g.c/org/repo")), False),
|
||||
(ReqMock(link=Link("https://g.c/dist.tgz")), False),
|
||||
|
|
Loading…
Reference in New Issue