Skip to content

Commit ae51289

Browse files
committed
Prepare legacy editable metadata in isolated env
When there is a pyproject.toml, metadata preparation must be done in the isolated build environment for legacy editable installs too (fixes a regression). Also detect earlier if an editable install must go through the legacy install path, to be sure to run it in an environment with the correct build requirements.
1 parent b0eb95b commit ae51289

File tree

5 files changed

+81
-89
lines changed

5 files changed

+81
-89
lines changed

news/10573.bugfix.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
When installing projects with a ``pyproject.toml`` in editable mode, and the build
2+
backend does not support :pep:`660`, prepare metadata using
3+
``prepare_metadata_for_build_wheel`` instead of ``setup.py egg_info``. Also, refuse
4+
installing projects that only have a ``setup.cfg`` and no ``setup.py`` nor
5+
``pyproject.toml``. These restore the pre-21.3 behaviour.

src/pip/_internal/distributions/sdist.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,22 @@ def prepare_distribution_metadata(
3838

3939
def _setup_isolation(self, finder: PackageFinder) -> None:
4040
self._prepare_build_backend(finder)
41+
# Check that if the requirement is editable, it either supports PEP 660 or has a
42+
# setup.py or a setup.cfg. This cannot be done earlier because we need to setup
43+
# the build backend to verify it supports build_editable, nor can it be done
44+
# later, because we want to avoid installing build requirements needlessly.
45+
# Doing it here also works around setuptools generating UNKNOWN.egg-info when
46+
# running get_requires_for_build_wheel on a directory without setup.py nor
47+
# setup.cfg.
48+
self.req.isolated_editable_sanity_check()
4149
# Install any extra build dependencies that the backend requests.
4250
# This must be done in a second pass, as the pyproject.toml
4351
# dependencies must be installed before we can call the backend.
44-
if self.req.editable and self.req.permit_editable_wheels:
52+
if (
53+
self.req.editable
54+
and self.req.permit_editable_wheels
55+
and self.req.supports_pyproject_editable()
56+
):
4557
build_reqs = self._get_build_requires_editable()
4658
else:
4759
build_reqs = self._get_build_requires_wheel()

src/pip/_internal/req/req_install.py

Lines changed: 56 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# The following comment should be removed at some point in the future.
22
# mypy: strict-optional=False
33

4+
import functools
45
import logging
56
import os
67
import shutil
@@ -16,7 +17,7 @@
1617
from pip._vendor.packaging.utils import canonicalize_name
1718
from pip._vendor.packaging.version import Version
1819
from pip._vendor.packaging.version import parse as parse_version
19-
from pip._vendor.pep517.wrappers import HookMissing, Pep517HookCaller
20+
from pip._vendor.pep517.wrappers import Pep517HookCaller
2021
from pip._vendor.pkg_resources import Distribution
2122

2223
from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
@@ -53,6 +54,7 @@
5354
redact_auth_from_url,
5455
)
5556
from pip._internal.utils.packaging import get_metadata
57+
from pip._internal.utils.subprocess import runner_with_spinner_message
5658
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
5759
from pip._internal.utils.virtualenv import running_under_virtualenv
5860
from pip._internal.vcs import vcs
@@ -196,11 +198,6 @@ def __init__(
196198
# but after loading this flag should be treated as read only.
197199
self.use_pep517 = use_pep517
198200

199-
# supports_pyproject_editable will be set to True or False when we try
200-
# to prepare editable metadata or build an editable wheel. None means
201-
# "we don't know yet".
202-
self.supports_pyproject_editable: Optional[bool] = None
203-
204201
# This requirement needs more preparation before it can be built
205202
self.needs_more_preparation = False
206203

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

247+
@functools.lru_cache() # use cached_property in python 3.8+
248+
def supports_pyproject_editable(self) -> bool:
249+
if not self.use_pep517:
250+
return False
251+
assert self.pep517_backend
252+
with self.build_env:
253+
runner = runner_with_spinner_message(
254+
"Checking if build backend supports build_editable"
255+
)
256+
with self.pep517_backend.subprocess_runner(runner):
257+
return "build_editable" in self.pep517_backend._supported_features()
258+
250259
@property
251260
def specifier(self) -> SpecifierSet:
252261
return self.req.specifier
@@ -503,93 +512,59 @@ def load_pyproject_toml(self) -> None:
503512
backend_path=backend_path,
504513
)
505514

506-
def _generate_editable_metadata(self) -> str:
507-
"""Invokes metadata generator functions, with the required arguments."""
508-
if self.use_pep517:
509-
assert self.pep517_backend is not None
510-
try:
511-
metadata_directory = generate_editable_metadata(
512-
build_env=self.build_env,
513-
backend=self.pep517_backend,
514-
)
515-
except HookMissing as e:
516-
self.supports_pyproject_editable = False
517-
if not os.path.exists(self.setup_py_path) and not os.path.exists(
518-
self.setup_cfg_path
519-
):
520-
raise InstallationError(
521-
f"Project {self} has a 'pyproject.toml' and its build "
522-
f"backend is missing the {e} hook. Since it does not "
523-
f"have a 'setup.py' nor a 'setup.cfg', "
524-
f"it cannot be installed in editable mode. "
525-
f"Consider using a build backend that supports PEP 660."
526-
)
527-
# At this point we have determined that the build_editable hook
528-
# is missing, and there is a setup.py or setup.cfg
529-
# so we fallback to the legacy metadata generation
530-
logger.info(
531-
"Build backend does not support editables, "
532-
"falling back to setup.py egg_info."
533-
)
534-
else:
535-
self.supports_pyproject_editable = True
536-
return metadata_directory
537-
elif not os.path.exists(self.setup_py_path) and not os.path.exists(
538-
self.setup_cfg_path
539-
):
540-
raise InstallationError(
541-
f"File 'setup.py' or 'setup.cfg' not found "
542-
f"for legacy project {self}. "
543-
f"It cannot be installed in editable mode."
544-
)
545-
546-
return generate_metadata_legacy(
547-
build_env=self.build_env,
548-
setup_py_path=self.setup_py_path,
549-
source_dir=self.unpacked_source_directory,
550-
isolated=self.isolated,
551-
details=self.name or f"from {self.link}",
552-
)
515+
def isolated_editable_sanity_check(self) -> None:
516+
"""Check that an editable requirement if valid for use with PEP 517/518.
553517
554-
def _generate_metadata(self) -> str:
555-
"""Invokes metadata generator functions, with the required arguments."""
556-
if self.use_pep517:
557-
assert self.pep517_backend is not None
558-
try:
559-
return generate_metadata(
560-
build_env=self.build_env,
561-
backend=self.pep517_backend,
562-
)
563-
except HookMissing as e:
564-
raise InstallationError(
565-
f"Project {self} has a pyproject.toml but its build "
566-
f"backend is missing the required {e} hook."
567-
)
568-
elif not os.path.exists(self.setup_py_path):
518+
This verifies that an editable that has a pyproject.toml either supports PEP 660
519+
or as a setup.py or a setup.cfg
520+
"""
521+
if (
522+
self.editable
523+
and self.use_pep517
524+
and not self.supports_pyproject_editable()
525+
and not os.path.isfile(self.setup_py_path)
526+
and not os.path.isfile(self.setup_cfg_path)
527+
):
569528
raise InstallationError(
570-
f"File 'setup.py' not found for legacy project {self}."
529+
f"Project {self} has a 'pyproject.toml' and its build "
530+
f"backend is missing the 'build_editable' hook. Since it does not "
531+
f"have a 'setup.py' nor a 'setup.cfg', "
532+
f"it cannot be installed in editable mode. "
533+
f"Consider using a build backend that supports PEP 660."
571534
)
572535

573-
return generate_metadata_legacy(
574-
build_env=self.build_env,
575-
setup_py_path=self.setup_py_path,
576-
source_dir=self.unpacked_source_directory,
577-
isolated=self.isolated,
578-
details=self.name or f"from {self.link}",
579-
)
580-
581536
def prepare_metadata(self) -> None:
582537
"""Ensure that project metadata is available.
583538
584-
Under PEP 517, call the backend hook to prepare the metadata.
539+
Under PEP 517 and PEP 660, call the backend hook to prepare the metadata.
585540
Under legacy processing, call setup.py egg-info.
586541
"""
587542
assert self.source_dir
588543

589-
if self.editable and self.permit_editable_wheels:
590-
self.metadata_directory = self._generate_editable_metadata()
544+
if self.use_pep517:
545+
assert self.pep517_backend is not None
546+
if (
547+
self.editable
548+
and self.permit_editable_wheels
549+
and self.supports_pyproject_editable()
550+
):
551+
self.metadata_directory = generate_editable_metadata(
552+
build_env=self.build_env,
553+
backend=self.pep517_backend,
554+
)
555+
else:
556+
self.metadata_directory = generate_metadata(
557+
build_env=self.build_env,
558+
backend=self.pep517_backend,
559+
)
591560
else:
592-
self.metadata_directory = self._generate_metadata()
561+
self.metadata_directory = generate_metadata_legacy(
562+
build_env=self.build_env,
563+
setup_py_path=self.setup_py_path,
564+
source_dir=self.unpacked_source_directory,
565+
isolated=self.isolated,
566+
details=self.name or f"from {self.link}",
567+
)
593568

594569
# Act on the newly generated metadata, based on the name and version.
595570
if not self.name:

src/pip/_internal/wheel_builder.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,8 @@ def _should_build(
7171
return False
7272

7373
if req.editable:
74-
if req.use_pep517 and req.supports_pyproject_editable is not False:
75-
return True
76-
# we don't build legacy editable requirements
77-
return False
74+
# we only build PEP 660 editable requirements
75+
return req.supports_pyproject_editable()
7876

7977
if req.use_pep517:
8078
return True

tests/unit/test_wheel_builder.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def __init__(
3939
constraint: bool = False,
4040
source_dir: Optional[str] = "/tmp/pip-install-123/pendulum",
4141
use_pep517: bool = True,
42-
supports_pyproject_editable: Optional[bool] = None,
42+
supports_pyproject_editable: bool = False,
4343
) -> None:
4444
self.name = name
4545
self.is_wheel = is_wheel
@@ -48,7 +48,10 @@ def __init__(
4848
self.constraint = constraint
4949
self.source_dir = source_dir
5050
self.use_pep517 = use_pep517
51-
self.supports_pyproject_editable = supports_pyproject_editable
51+
self._supports_pyproject_editable = supports_pyproject_editable
52+
53+
def supports_pyproject_editable(self) -> bool:
54+
return self._supports_pyproject_editable
5255

5356

5457
@pytest.mark.parametrize(
@@ -66,7 +69,6 @@ def __init__(
6669
# We don't build reqs that are already wheels.
6770
(ReqMock(is_wheel=True), False, False),
6871
(ReqMock(editable=True, use_pep517=False), False, False),
69-
(ReqMock(editable=True, use_pep517=True), False, True),
7072
(
7173
ReqMock(editable=True, use_pep517=True, supports_pyproject_editable=True),
7274
False,

0 commit comments

Comments
 (0)