Skip to content

Commit

Permalink
Early detection of build backend with build_editable support
Browse files Browse the repository at this point in the history
  • Loading branch information
sbidoul committed Oct 12, 2021
1 parent ed8ca7a commit 1b34c0c
Show file tree
Hide file tree
Showing 6 changed files with 45 additions and 44 deletions.
6 changes: 5 additions & 1 deletion src/pip/_internal/distributions/sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ def _setup_isolation(self, finder: PackageFinder) -> None:
# 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:
if (
self.req.editable
and self.req.permit_editable_wheels
and self.req.supports_pyproject_editable()
):
build_reqs = self._get_build_requires_editable()
else:
build_reqs = self._get_build_requires_wheel()
Expand Down
64 changes: 25 additions & 39 deletions src/pip/_internal/req/req_install.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False

import functools
import logging
import os
import shutil
Expand Down Expand Up @@ -53,6 +54,7 @@
redact_auth_from_url,
)
from pip._internal.utils.packaging import get_metadata
from pip._internal.utils.subprocess import runner_with_spinner_message
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
from pip._internal.utils.virtualenv import running_under_virtualenv
from pip._internal.vcs import vcs
Expand Down Expand Up @@ -196,11 +198,6 @@ def __init__(
# 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

Expand Down Expand Up @@ -247,6 +244,18 @@ def name(self) -> Optional[str]:
return None
return pkg_resources.safe_name(self.req.name)

@functools.lru_cache() # use cached_property in python 3.8+
def supports_pyproject_editable(self) -> bool:
if not self.use_pep517:
return False
assert self.pep517_backend
with self.build_env:
runner = runner_with_spinner_message(
"Checking if build backend supports build_editable"
)
with self.pep517_backend.subprocess_runner(runner):
return self.pep517_backend._supports_build_editable()

@property
def specifier(self) -> SpecifierSet:
return self.req.specifier
Expand Down Expand Up @@ -505,39 +514,12 @@ def load_pyproject_toml(self) -> None:

def _generate_editable_metadata(self) -> str:
"""Invokes metadata generator functions, with the required arguments."""
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,
)
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
logger.info(
"Build backend does not support editables, "
"falling back to regular metadata preparation."
)
else:
self.supports_pyproject_editable = True
return metadata_directory

# PEP 660 not supported, fall back to regular metadata preparation,
# either using PEP 517 if supported or setup.py egg_info.
return self._generate_metadata()
assert self.use_pep517
assert self.pep517_backend is not None
return generate_editable_metadata(
build_env=self.build_env,
backend=self.pep517_backend,
)

def _generate_metadata(self) -> str:
"""Invokes metadata generator functions, with the required arguments."""
Expand Down Expand Up @@ -574,7 +556,11 @@ def prepare_metadata(self) -> None:
"""
assert self.source_dir

if self.editable and self.permit_editable_wheels:
if (
self.editable
and self.permit_editable_wheels
and self.supports_pyproject_editable()
):
self.metadata_directory = self._generate_editable_metadata()
else:
self.metadata_directory = self._generate_metadata()
Expand Down
2 changes: 1 addition & 1 deletion src/pip/_internal/wheel_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def _should_build(
return False

if req.editable:
if req.use_pep517 and req.supports_pyproject_editable is not False:
if req.supports_pyproject_editable():
return True
# we don't build legacy editable requirements
return False
Expand Down
6 changes: 6 additions & 0 deletions src/pip/_vendor/pep517/in_process/_in_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ def _build_backend():
return obj


def _supports_build_editable():
backend = _build_backend()
return hasattr(backend, "build_editable")


def get_requires_for_build_wheel(config_settings):
"""Invoke the optional get_requires_for_build_wheel hook
Expand Down Expand Up @@ -312,6 +317,7 @@ def build_sdist(sdist_directory, config_settings):
'build_editable',
'get_requires_for_build_sdist',
'build_sdist',
'_supports_build_editable',
}


Expand Down
3 changes: 3 additions & 0 deletions src/pip/_vendor/pep517/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ def subprocess_runner(self, runner):
finally:
self._subprocess_runner = prev

def _supports_build_editable(self):
return self._call_hook('_supports_build_editable', {})

def get_requires_for_build_wheel(self, config_settings=None):
"""Identify packages required for building a wheel
Expand Down
8 changes: 5 additions & 3 deletions tests/unit/test_wheel_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def __init__(
constraint: bool = False,
source_dir: Optional[str] = "/tmp/pip-install-123/pendulum",
use_pep517: bool = True,
supports_pyproject_editable: Optional[bool] = None,
supports_pyproject_editable: bool = False,
) -> None:
self.name = name
self.is_wheel = is_wheel
Expand All @@ -48,7 +48,10 @@ def __init__(
self.constraint = constraint
self.source_dir = source_dir
self.use_pep517 = use_pep517
self.supports_pyproject_editable = supports_pyproject_editable
self._supports_pyproject_editable = supports_pyproject_editable

def supports_pyproject_editable(self) -> bool:
return self._supports_pyproject_editable


@pytest.mark.parametrize(
Expand All @@ -66,7 +69,6 @@ def __init__(
# We don't build reqs that are already wheels.
(ReqMock(is_wheel=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,
Expand Down

0 comments on commit 1b34c0c

Please sign in to comment.