Skip to content

Commit 9d91022

Browse files
committed
Merge remote-tracking branch 'origin/main' into main
2 parents ca17be6 + 483ff34 commit 9d91022

32 files changed

+225
-150
lines changed

news/10531.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Always refuse installing or building projects that have no ``pyproject.toml`` nor
2+
``setup.py``.

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.

news/pep517.vendor.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Upgrade pep517 to 0.12.0

news/resolvelib.vendor.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Upgrade resolvelib to 0.8.1

src/pip/_internal/distributions/sdist.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,22 @@ def prepare_distribution_metadata(
3232
# Set up the build isolation, if this requirement should be isolated
3333
should_isolate = self.req.use_pep517 and build_isolation
3434
if should_isolate:
35-
self._setup_isolation(finder)
35+
# Setup an isolated environment and install the build backend static
36+
# requirements in it.
37+
self._prepare_build_backend(finder)
38+
# Check that if the requirement is editable, it either supports PEP 660 or
39+
# has a setup.py or a setup.cfg. This cannot be done earlier because we need
40+
# to setup the build backend to verify it supports build_editable, nor can
41+
# it be done later, because we want to avoid installing build requirements
42+
# needlessly. Doing it here also works around setuptools generating
43+
# UNKNOWN.egg-info when running get_requires_for_build_wheel on a directory
44+
# without setup.py nor setup.cfg.
45+
self.req.isolated_editable_sanity_check()
46+
# Install the dynamic build requirements.
47+
self._install_build_reqs(finder)
3648

3749
self.req.prepare_metadata()
3850

39-
def _setup_isolation(self, finder: PackageFinder) -> None:
40-
self._prepare_build_backend(finder)
41-
# Install any extra build dependencies that the backend requests.
42-
# This must be done in a second pass, as the pyproject.toml
43-
# dependencies must be installed before we can call the backend.
44-
if self.req.editable and self.req.permit_editable_wheels:
45-
build_reqs = self._get_build_requires_editable()
46-
else:
47-
build_reqs = self._get_build_requires_wheel()
48-
self._install_build_reqs(finder, build_reqs)
49-
5051
def _prepare_build_backend(self, finder: PackageFinder) -> None:
5152
# Isolate in a BuildEnvironment and install the build-time
5253
# requirements.
@@ -91,8 +92,19 @@ def _get_build_requires_editable(self) -> Iterable[str]:
9192
with backend.subprocess_runner(runner):
9293
return backend.get_requires_for_build_editable()
9394

94-
def _install_build_reqs(self, finder: PackageFinder, reqs: Iterable[str]) -> None:
95-
conflicting, missing = self.req.build_env.check_requirements(reqs)
95+
def _install_build_reqs(self, finder: PackageFinder) -> None:
96+
# Install any extra build dependencies that the backend requests.
97+
# This must be done in a second pass, as the pyproject.toml
98+
# dependencies must be installed before we can call the backend.
99+
if (
100+
self.req.editable
101+
and self.req.permit_editable_wheels
102+
and self.req.supports_pyproject_editable()
103+
):
104+
build_reqs = self._get_build_requires_editable()
105+
else:
106+
build_reqs = self._get_build_requires_wheel()
107+
conflicting, missing = self.req.build_env.check_requirements(build_reqs)
96108
if conflicting:
97109
self._raise_conflicts("the backend dependencies", conflicting)
98110
self.req.build_env.install_requirements(

src/pip/_internal/operations/build/metadata.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ def generate_metadata(build_env: BuildEnvironment, backend: Pep517HookCaller) ->
2323
# Note that Pep517HookCaller implements a fallback for
2424
# prepare_metadata_for_build_wheel, so we don't have to
2525
# consider the possibility that this hook doesn't exist.
26-
runner = runner_with_spinner_message(
27-
"Preparing wheel metadata (pyproject.toml)"
28-
)
26+
runner = runner_with_spinner_message("Preparing metadata (pyproject.toml)")
2927
with backend.subprocess_runner(runner):
3028
distinfo_dir = backend.prepare_metadata_for_build_wheel(metadata_dir)
3129

src/pip/_internal/operations/prepare.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ def _ensure_link_req_src_dir(
348348
# installation.
349349
# FIXME: this won't upgrade when there's an existing
350350
# package unpacked in `req.source_dir`
351+
# TODO: this check is now probably dead code
351352
if is_installable_dir(req.source_dir):
352353
raise PreviousBuildDirError(
353354
"pip can't proceed with requirements '{}' due to a"

src/pip/_internal/pyproject.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ def load_pyproject_toml(
4848
has_pyproject = os.path.isfile(pyproject_toml)
4949
has_setup = os.path.isfile(setup_py)
5050

51+
if not has_pyproject and not has_setup:
52+
raise InstallationError(
53+
f"{req_name} does not appear to be a Python project: "
54+
f"neither 'setup.py' nor 'pyproject.toml' found."
55+
)
56+
5157
if has_pyproject:
5258
with open(pyproject_toml, encoding="utf-8") as f:
5359
pp_toml = tomli.load(f)

src/pip/_internal/req/constructors.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ def _get_url_from_path(path: str, name: str) -> Optional[str]:
235235
if _looks_like_path(name) and os.path.isdir(path):
236236
if is_installable_dir(path):
237237
return path_to_url(path)
238+
# TODO: The is_installable_dir test here might not be necessary
239+
# now that it is done in load_pyproject_toml too.
238240
raise InstallationError(
239241
f"Directory {name!r} is not installable. Neither 'setup.py' "
240242
"nor 'pyproject.toml' found."

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

src/pip/_vendor/pep517/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Wrappers to build Python packages using PEP 517 hooks
22
"""
33

4-
__version__ = '0.11.0'
4+
__version__ = '0.12.0'
55

66
from .wrappers import * # noqa: F401, F403

src/pip/_vendor/pep517/build.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def load_system(source_dir):
3131
Load the build system from a source dir (pyproject.toml).
3232
"""
3333
pyproject = os.path.join(source_dir, 'pyproject.toml')
34-
with io.open(pyproject, encoding="utf-8") as f:
34+
with io.open(pyproject, 'rb') as f:
3535
pyproject_data = toml_load(f)
3636
return pyproject_data['build-system']
3737

src/pip/_vendor/pep517/check.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def check(source_dir):
142142
return False
143143

144144
try:
145-
with io.open(pyproject, encoding="utf-8") as f:
145+
with io.open(pyproject, 'rb') as f:
146146
pyproject_data = toml_load(f)
147147
# Ensure the mandatory data can be loaded
148148
buildsys = pyproject_data['build-system']

src/pip/_vendor/pep517/compat.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Python 2/3 compatibility"""
2+
import io
23
import json
34
import sys
45

@@ -35,7 +36,15 @@ def read_json(path):
3536

3637

3738
if sys.version_info < (3, 6):
38-
from toml import load as toml_load # noqa: F401
39+
from toml import load as _toml_load # noqa: F401
40+
41+
def toml_load(f):
42+
w = io.TextIOWrapper(f, encoding="utf8", newline="")
43+
try:
44+
return _toml_load(w)
45+
finally:
46+
w.detach()
47+
3948
from toml import TomlDecodeError as TOMLDecodeError # noqa: F401
4049
else:
4150
from pip._vendor.tomli import load as toml_load # noqa: F401

src/pip/_vendor/pep517/envbuild.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
def _load_pyproject(source_dir):
2020
with io.open(
2121
os.path.join(source_dir, 'pyproject.toml'),
22-
encoding="utf-8",
22+
'rb',
2323
) as f:
2424
pyproject_data = toml_load(f)
2525
buildsys = pyproject_data['build-system']

0 commit comments

Comments
 (0)