Skip to content

Commit 71f506e

Browse files
committed
Require --no-use-pep517 if using editable mode with pyproject.toml.
1 parent f47d012 commit 71f506e

File tree

6 files changed

+202
-62
lines changed

6 files changed

+202
-62
lines changed

news/6370.bugfix

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix the handling of editable mode during installs when ``pyproject.toml`` is
2+
present but PEP 517 doesn't require the source tree to be treated as
3+
``pyproject.toml``-style.

src/pip/_internal/operations/prepare.py

+11-5
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,11 @@ def prep_for_dist(self, finder, build_isolation):
136136
# 2. Set up the build environment
137137

138138
self.req.load_pyproject_toml()
139-
should_isolate = self.req.use_pep517 and build_isolation
139+
140+
should_isolate = (
141+
(self.req.use_pep517 or self.req.pyproject_requires) and
142+
build_isolation
143+
)
140144

141145
if should_isolate:
142146
# Isolate in a BuildEnvironment and install the build-time
@@ -163,10 +167,12 @@ def prep_for_dist(self, finder, build_isolation):
163167
" and ".join(map(repr, sorted(missing)))
164168
)
165169

166-
# Install any extra build dependencies that the backend requests.
167-
# This must be done in a second pass, as the pyproject.toml
168-
# dependencies must be installed before we can call the backend.
169-
self.install_backend_dependencies(finder=finder)
170+
if self.req.use_pep517:
171+
# If we're using PEP 517, then install any extra build
172+
# dependencies that the backend requested. This must be
173+
# done in a second pass, as the pyproject.toml dependencies
174+
# must be installed before we can call the backend.
175+
self.install_backend_dependencies(finder=finder)
170176

171177
self.req.prepare_metadata()
172178
self.req.assert_source_matches_version()

src/pip/_internal/pyproject.py

+68-37
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
if MYPY_CHECK_RUNNING:
1313
from typing import Any, Dict, List, Optional, Tuple
1414

15+
Pep517Data = Tuple[str, List[str]]
16+
1517

1618
def _is_list_of_str(obj):
1719
# type: (Any) -> bool
@@ -64,6 +66,37 @@ def make_editable_error(req_name, reason):
6466
return InstallationError(message)
6567

6668

69+
def get_build_system_requires(build_system, req_name):
70+
if build_system is None:
71+
return None
72+
73+
# Ensure that the build-system section in pyproject.toml conforms
74+
# to PEP 518.
75+
error_template = (
76+
"{package} has a pyproject.toml file that does not comply "
77+
"with PEP 518: {reason}"
78+
)
79+
80+
# Specifying the build-system table but not the requires key is invalid
81+
if "requires" not in build_system:
82+
raise InstallationError(
83+
error_template.format(package=req_name, reason=(
84+
"it has a 'build-system' table but not "
85+
"'build-system.requires' which is mandatory in the table"
86+
))
87+
)
88+
89+
# Error out if requires is not a list of strings
90+
requires = build_system["requires"]
91+
if not _is_list_of_str(requires):
92+
raise InstallationError(error_template.format(
93+
package=req_name,
94+
reason="'build-system.requires' is not a list of strings.",
95+
))
96+
97+
return requires
98+
99+
67100
def resolve_pyproject_toml(
68101
build_system, # type: Optional[Dict[str, Any]]
69102
has_pyproject, # type: bool
@@ -72,7 +105,7 @@ def resolve_pyproject_toml(
72105
editable, # type: bool
73106
req_name, # type: str
74107
):
75-
# type: (...) -> Optional[Tuple[List[str], str, List[str]]]
108+
# type: (...) -> Tuple[Optional[List[str]], Optional[Pep517Data]]
76109
"""
77110
Return how a pyproject.toml file's contents should be interpreted.
78111
@@ -86,6 +119,13 @@ def resolve_pyproject_toml(
86119
:param editable: whether editable mode was requested for the requirement.
87120
:param req_name: the name of the requirement we're processing (for
88121
error reporting).
122+
123+
:return: a tuple (requires, pep517_data), where `requires` is the list
124+
of build requirements from pyproject.toml (or else None). The value
125+
`pep517_data` is None if `use_pep517` is False. Otherwise, it is the
126+
tuple (backend, check), where `backend` is the name of the PEP 517
127+
backend and `check` is the list of requirements we should check are
128+
installed after setting up the build environment.
89129
"""
90130
# The following cases must use PEP 517
91131
# We check for use_pep517 being non-None and falsey because that means
@@ -126,19 +166,34 @@ def resolve_pyproject_toml(
126166
req_name, 'PEP 517 processing was explicitly requested'
127167
)
128168

129-
# If we haven't worked out whether to use PEP 517 yet,
130-
# and the user hasn't explicitly stated a preference,
131-
# we do so if the project has a pyproject.toml file.
169+
# If we haven't worked out whether to use PEP 517 yet, and the user
170+
# hasn't explicitly stated a preference, we do so if the project has
171+
# a pyproject.toml file (provided editable mode wasn't requested).
132172
elif use_pep517 is None:
173+
if has_pyproject and editable:
174+
message = (
175+
'Error installing {!r}: editable mode is not supported for '
176+
'pyproject.toml-style projects. pip is processing this '
177+
'project as pyproject.toml-style because it has a '
178+
'pyproject.toml file. Since the project has a setup.py and '
179+
'the pyproject.toml has no "build-backend" key for the '
180+
'"build_system" value, you may pass --no-use-pep517 to opt '
181+
'out of pyproject.toml-style processing. '
182+
'See PEP 517 for details on pyproject.toml-style projects.'
183+
).format(req_name)
184+
raise InstallationError(message)
185+
133186
use_pep517 = has_pyproject
134187

135188
# At this point, we know whether we're going to use PEP 517.
136189
assert use_pep517 is not None
137190

191+
requires = get_build_system_requires(build_system, req_name=req_name)
192+
138193
# If we're using the legacy code path, there is nothing further
139194
# for us to do here.
140195
if not use_pep517:
141-
return None
196+
return (requires, None)
142197

143198
if build_system is None:
144199
# Either the user has a pyproject.toml with no build-system
@@ -149,8 +204,8 @@ def resolve_pyproject_toml(
149204
# traditional direct setup.py execution, and require wheel and
150205
# a version of setuptools that supports that backend.
151206

207+
requires = ["setuptools>=40.8.0", "wheel"]
152208
build_system = {
153-
"requires": ["setuptools>=40.8.0", "wheel"],
154209
"build-backend": "setuptools.build_meta:__legacy__",
155210
}
156211

@@ -160,30 +215,6 @@ def resolve_pyproject_toml(
160215
# specified a backend, though.
161216
assert build_system is not None
162217

163-
# Ensure that the build-system section in pyproject.toml conforms
164-
# to PEP 518.
165-
error_template = (
166-
"{package} has a pyproject.toml file that does not comply "
167-
"with PEP 518: {reason}"
168-
)
169-
170-
# Specifying the build-system table but not the requires key is invalid
171-
if "requires" not in build_system:
172-
raise InstallationError(
173-
error_template.format(package=req_name, reason=(
174-
"it has a 'build-system' table but not "
175-
"'build-system.requires' which is mandatory in the table"
176-
))
177-
)
178-
179-
# Error out if requires is not a list of strings
180-
requires = build_system["requires"]
181-
if not _is_list_of_str(requires):
182-
raise InstallationError(error_template.format(
183-
package=req_name,
184-
reason="'build-system.requires' is not a list of strings.",
185-
))
186-
187218
backend = build_system.get("build-backend")
188219
check = [] # type: List[str]
189220
if backend is None:
@@ -202,7 +233,7 @@ def resolve_pyproject_toml(
202233
backend = "setuptools.build_meta:__legacy__"
203234
check = ["setuptools>=40.8.0", "wheel"]
204235

205-
return (requires, backend, check)
236+
return (requires, (backend, check))
206237

207238

208239
def load_pyproject_toml(
@@ -212,7 +243,7 @@ def load_pyproject_toml(
212243
setup_py, # type: str
213244
req_name # type: str
214245
):
215-
# type: (...) -> Optional[Tuple[List[str], str, List[str]]]
246+
# type: (...) -> Tuple[Optional[List[str]], Optional[Pep517Data]]
216247
"""Load the pyproject.toml file.
217248
218249
Parameters:
@@ -224,13 +255,13 @@ def load_pyproject_toml(
224255
req_name - The name of the requirement we're processing (for
225256
error reporting)
226257
227-
Returns:
228-
None if we should use the legacy code path, otherwise a tuple
258+
Returns: (requires, pep_517_data)
259+
requires: requirements from pyproject.toml (can be None).
260+
pep_517_data: None if we should use the legacy code path, otherwise:
229261
(
230-
requirements from pyproject.toml,
231262
name of PEP 517 backend,
232-
requirements we should check are installed after setting
233-
up the build environment
263+
requirements we should check are installed after setting up
264+
the build environment
234265
)
235266
"""
236267
has_pyproject = os.path.isfile(pyproject_toml)

src/pip/_internal/req/req_install.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -485,21 +485,22 @@ def load_pyproject_toml(self):
485485
use_pep517 attribute can be used to determine whether we should
486486
follow the PEP 517 or legacy (setup.py) code path.
487487
"""
488-
pep517_data = load_pyproject_toml(
488+
requires, pep517_data = load_pyproject_toml(
489489
self.use_pep517,
490490
self.editable,
491491
self.pyproject_toml,
492492
self.setup_py,
493493
str(self)
494494
)
495495

496-
if pep517_data is None:
497-
self.use_pep517 = False
498-
else:
499-
self.use_pep517 = True
500-
requires, backend, check = pep517_data
496+
use_pep517 = bool(pep517_data)
497+
498+
self.use_pep517 = use_pep517
499+
self.pyproject_requires = requires
500+
501+
if use_pep517:
502+
backend, check = pep517_data
501503
self.requirements_to_check = check
502-
self.pyproject_requires = requires
503504
self.pep517_backend = Pep517HookCaller(self.setup_py_dir, backend)
504505

505506
# Use a custom function to call subprocesses

tests/functional/test_install_reqs.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -302,8 +302,12 @@ def test_constraints_local_editable_install_pep518(script, data):
302302
to_install = data.src.join("pep518-3.0")
303303

304304
script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
305+
# --no-use-pep517 has to be passed since a pyproject.toml file is
306+
# present but PEP 517 doesn't support editable mode.
305307
script.pip(
306-
'install', '--no-index', '-f', data.find_links, '-e', to_install)
308+
'install', '--no-use-pep517', '--no-index', '-f', data.find_links,
309+
'-e', to_install,
310+
)
307311

308312

309313
def test_constraints_local_install_causes_error(script, data):

0 commit comments

Comments
 (0)