Skip to content

Commit b4fb710

Browse files
authored
Merge pull request #9293 from pypa/revert-9264-new-resolver-dont-abort-on-inconsistent-candidate
2 parents 03d5f56 + 95c3ae3 commit b4fb710

File tree

8 files changed

+36
-131
lines changed

8 files changed

+36
-131
lines changed

news/9264.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Revert "Skip candidate not providing valid metadata", as that caused pip to be overeager about downloading from the package index.

src/pip/_internal/exceptions.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -151,21 +151,6 @@ def __str__(self):
151151
)
152152

153153

154-
class InstallationSubprocessError(InstallationError):
155-
"""A subprocess call failed during installation."""
156-
def __init__(self, returncode, description):
157-
# type: (int, str) -> None
158-
self.returncode = returncode
159-
self.description = description
160-
161-
def __str__(self):
162-
# type: () -> str
163-
return (
164-
"Command errored out with exit status {}: {} "
165-
"Check the logs for full command output."
166-
).format(self.returncode, self.description)
167-
168-
169154
class HashErrors(InstallationError):
170155
"""Multiple HashError instances rolled into one for reporting"""
171156

src/pip/_internal/resolution/resolvelib/candidates.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def __init__(
141141
self._ireq = ireq
142142
self._name = name
143143
self._version = version
144-
self.dist = self._prepare()
144+
self._dist = None # type: Optional[Distribution]
145145

146146
def __str__(self):
147147
# type: () -> str
@@ -209,6 +209,8 @@ def _prepare_distribution(self):
209209
def _check_metadata_consistency(self, dist):
210210
# type: (Distribution) -> None
211211
"""Check for consistency of project name and version of dist."""
212+
# TODO: (Longer term) Rather than abort, reject this candidate
213+
# and backtrack. This would need resolvelib support.
212214
name = canonicalize_name(dist.project_name)
213215
if self._name is not None and self._name != name:
214216
raise MetadataInconsistent(self._ireq, "name", dist.project_name)
@@ -217,17 +219,25 @@ def _check_metadata_consistency(self, dist):
217219
raise MetadataInconsistent(self._ireq, "version", dist.version)
218220

219221
def _prepare(self):
220-
# type: () -> Distribution
222+
# type: () -> None
223+
if self._dist is not None:
224+
return
221225
try:
222226
dist = self._prepare_distribution()
223227
except HashError as e:
224-
# Provide HashError the underlying ireq that caused it. This
225-
# provides context for the resulting error message to show the
226-
# offending line to the user.
227228
e.req = self._ireq
228229
raise
230+
231+
assert dist is not None, "Distribution already installed"
229232
self._check_metadata_consistency(dist)
230-
return dist
233+
self._dist = dist
234+
235+
@property
236+
def dist(self):
237+
# type: () -> Distribution
238+
if self._dist is None:
239+
self._prepare()
240+
return self._dist
231241

232242
def _get_requires_python_dependency(self):
233243
# type: () -> Optional[Requirement]
@@ -251,6 +261,7 @@ def iter_dependencies(self, with_requires):
251261

252262
def get_install_requirement(self):
253263
# type: () -> Optional[InstallRequirement]
264+
self._prepare()
254265
return self._ireq
255266

256267

src/pip/_internal/resolution/resolvelib/factory.py

Lines changed: 8 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
from pip._internal.exceptions import (
66
DistributionNotFound,
77
InstallationError,
8-
InstallationSubprocessError,
9-
MetadataInconsistent,
108
UnsupportedPythonVersion,
119
UnsupportedWheel,
1210
)
@@ -35,7 +33,6 @@
3533
ExplicitRequirement,
3634
RequiresPythonRequirement,
3735
SpecifierRequirement,
38-
UnsatisfiableRequirement,
3936
)
4037

4138
if MYPY_CHECK_RUNNING:
@@ -97,7 +94,6 @@ def __init__(
9794
self._force_reinstall = force_reinstall
9895
self._ignore_requires_python = ignore_requires_python
9996

100-
self._build_failures = {} # type: Cache[InstallationError]
10197
self._link_candidate_cache = {} # type: Cache[LinkCandidate]
10298
self._editable_candidate_cache = {} # type: Cache[EditableCandidate]
10399
self._installed_candidate_cache = {
@@ -140,40 +136,21 @@ def _make_candidate_from_link(
140136
name, # type: Optional[str]
141137
version, # type: Optional[_BaseVersion]
142138
):
143-
# type: (...) -> Optional[Candidate]
139+
# type: (...) -> Candidate
144140
# TODO: Check already installed candidate, and use it if the link and
145141
# editable flag match.
146-
147-
if link in self._build_failures:
148-
# We already tried this candidate before, and it does not build.
149-
# Don't bother trying again.
150-
return None
151-
152142
if template.editable:
153143
if link not in self._editable_candidate_cache:
154-
try:
155-
self._editable_candidate_cache[link] = EditableCandidate(
156-
link, template, factory=self,
157-
name=name, version=version,
158-
)
159-
except (InstallationSubprocessError, MetadataInconsistent) as e:
160-
logger.warning("Discarding %s. %s", link, e)
161-
self._build_failures[link] = e
162-
return None
144+
self._editable_candidate_cache[link] = EditableCandidate(
145+
link, template, factory=self, name=name, version=version,
146+
)
163147
base = self._editable_candidate_cache[link] # type: BaseCandidate
164148
else:
165149
if link not in self._link_candidate_cache:
166-
try:
167-
self._link_candidate_cache[link] = LinkCandidate(
168-
link, template, factory=self,
169-
name=name, version=version,
170-
)
171-
except (InstallationSubprocessError, MetadataInconsistent) as e:
172-
logger.warning("Discarding %s. %s", link, e)
173-
self._build_failures[link] = e
174-
return None
150+
self._link_candidate_cache[link] = LinkCandidate(
151+
link, template, factory=self, name=name, version=version,
152+
)
175153
base = self._link_candidate_cache[link]
176-
177154
if extras:
178155
return ExtrasCandidate(base, extras)
179156
return base
@@ -233,16 +210,13 @@ def iter_index_candidates():
233210
for ican in reversed(icans):
234211
if not all_yanked and ican.link.is_yanked:
235212
continue
236-
candidate = self._make_candidate_from_link(
213+
yield self._make_candidate_from_link(
237214
link=ican.link,
238215
extras=extras,
239216
template=template,
240217
name=name,
241218
version=ican.version,
242219
)
243-
if candidate is None:
244-
continue
245-
yield candidate
246220

247221
return FoundCandidates(
248222
iter_index_candidates,
@@ -306,16 +280,6 @@ def make_requirement_from_install_req(self, ireq, requested_extras):
306280
name=canonicalize_name(ireq.name) if ireq.name else None,
307281
version=None,
308282
)
309-
if cand is None:
310-
# There's no way we can satisfy a URL requirement if the underlying
311-
# candidate fails to build. An unnamed URL must be user-supplied, so
312-
# we fail eagerly. If the URL is named, an unsatisfiable requirement
313-
# can make the resolver do the right thing, either backtrack (and
314-
# maybe find some other requirement that's buildable) or raise a
315-
# ResolutionImpossible eventually.
316-
if not ireq.name:
317-
raise self._build_failures[ireq.link]
318-
return UnsatisfiableRequirement(canonicalize_name(ireq.name))
319283
return self.make_requirement_from_candidate(cand)
320284

321285
def make_requirement_from_candidate(self, candidate):

src/pip/_internal/resolution/resolvelib/requirements.py

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -158,44 +158,3 @@ def is_satisfied_by(self, candidate):
158158
# already implements the prerelease logic, and would have filtered out
159159
# prerelease candidates if the user does not expect them.
160160
return self.specifier.contains(candidate.version, prereleases=True)
161-
162-
163-
class UnsatisfiableRequirement(Requirement):
164-
"""A requirement that cannot be satisfied.
165-
"""
166-
def __init__(self, name):
167-
# type: (str) -> None
168-
self._name = name
169-
170-
def __str__(self):
171-
# type: () -> str
172-
return "{} (unavailable)".format(self._name)
173-
174-
def __repr__(self):
175-
# type: () -> str
176-
return "{class_name}({name!r})".format(
177-
class_name=self.__class__.__name__,
178-
name=str(self._name),
179-
)
180-
181-
@property
182-
def project_name(self):
183-
# type: () -> str
184-
return self._name
185-
186-
@property
187-
def name(self):
188-
# type: () -> str
189-
return self._name
190-
191-
def format_for_error(self):
192-
# type: () -> str
193-
return str(self)
194-
195-
def get_candidate_lookup(self):
196-
# type: () -> CandidateLookup
197-
return None, None
198-
199-
def is_satisfied_by(self, candidate):
200-
# type: (Candidate) -> bool
201-
return False

src/pip/_internal/utils/subprocess.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from pip._vendor.six.moves import shlex_quote
88

99
from pip._internal.cli.spinners import SpinnerInterface, open_spinner
10-
from pip._internal.exceptions import InstallationSubprocessError
10+
from pip._internal.exceptions import InstallationError
1111
from pip._internal.utils.compat import console_to_str, str_to_display
1212
from pip._internal.utils.logging import subprocess_logger
1313
from pip._internal.utils.misc import HiddenText, path_to_display
@@ -233,7 +233,11 @@ def call_subprocess(
233233
exit_status=proc.returncode,
234234
)
235235
subprocess_logger.error(msg)
236-
raise InstallationSubprocessError(proc.returncode, command_desc)
236+
exc_msg = (
237+
'Command errored out with exit status {}: {} '
238+
'Check the logs for full command output.'
239+
).format(proc.returncode, command_desc)
240+
raise InstallationError(exc_msg)
237241
elif on_returncode == 'warn':
238242
subprocess_logger.warning(
239243
'Command "%s" had error code %s in %s',

tests/functional/test_new_resolver.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,22 +1218,3 @@ def test_new_resolver_does_not_reinstall_when_from_a_local_index(script):
12181218
assert "Installing collected packages: simple" not in result.stdout, str(result)
12191219
assert "Requirement already satisfied: simple" in result.stdout, str(result)
12201220
assert_installed(script, simple="0.1.0")
1221-
1222-
1223-
def test_new_resolver_skip_inconsistent_metadata(script):
1224-
create_basic_wheel_for_package(script, "A", "1")
1225-
1226-
a_2 = create_basic_wheel_for_package(script, "A", "2")
1227-
a_2.rename(a_2.parent.joinpath("a-3-py2.py3-none-any.whl"))
1228-
1229-
result = script.pip(
1230-
"install",
1231-
"--no-cache-dir", "--no-index",
1232-
"--find-links", script.scratch_path,
1233-
"--verbose",
1234-
"A",
1235-
allow_stderr_warning=True,
1236-
)
1237-
1238-
assert " different version in metadata: '2'" in result.stderr, str(result)
1239-
assert_installed(script, a="1")

tests/unit/test_utils_subprocess.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import pytest
88

99
from pip._internal.cli.spinners import SpinnerInterface
10-
from pip._internal.exceptions import InstallationSubprocessError
10+
from pip._internal.exceptions import InstallationError
1111
from pip._internal.utils.misc import hide_value
1212
from pip._internal.utils.subprocess import (
1313
call_subprocess,
@@ -276,7 +276,7 @@ def test_info_logging__subprocess_error(self, capfd, caplog):
276276
command = 'print("Hello"); print("world"); exit("fail")'
277277
args, spinner = self.prepare_call(caplog, log_level, command=command)
278278

279-
with pytest.raises(InstallationSubprocessError) as exc:
279+
with pytest.raises(InstallationError) as exc:
280280
call_subprocess(args, spinner=spinner)
281281
result = None
282282
exc_message = str(exc.value)
@@ -360,7 +360,7 @@ def test_info_logging_with_show_stdout_true(self, capfd, caplog):
360360
# log level is only WARNING.
361361
(0, True, None, WARNING, (None, 'done', 2)),
362362
# Test a non-zero exit status.
363-
(3, False, None, INFO, (InstallationSubprocessError, 'error', 2)),
363+
(3, False, None, INFO, (InstallationError, 'error', 2)),
364364
# Test a non-zero exit status also in extra_ok_returncodes.
365365
(3, False, (3, ), INFO, (None, 'done', 2)),
366366
])
@@ -396,7 +396,7 @@ def test_spinner_finish(
396396
assert spinner.spin_count == expected_spin_count
397397

398398
def test_closes_stdin(self):
399-
with pytest.raises(InstallationSubprocessError):
399+
with pytest.raises(InstallationError):
400400
call_subprocess(
401401
[sys.executable, '-c', 'input()'],
402402
show_stdout=True,

0 commit comments

Comments
 (0)