Skip to content

Commit 5b23c59

Browse files
ichard26uranusjr
andauthored
resolvelib: emit Requires-Python dependency first (#13270)
This makes the resolver always inspect Requires-Python first when checking a candidate's consistency, ensuring that no other candidates are prepared if the Requires-Python check fails. This regression was masked due to a broken test which checked for the (nonpresence of the) wrong package name. The resolvelib provider was also updated to return dependencies lazily. While ideally we wouldn't prepare candidates unnecessarily, pip has grown numerous metadata checks (for reporting bad metadata, skipping candidates with unsupported legacy metadata, etc.) so it's infeasible to stop preparing candidates upon creation (without a serious architectural redesign). However, we can create the candidates one-by-one as they're processed instead of all dependencies at once. This is necessary so the resolver can process Requires-Python first without processing other dependencies. Co-authored-by: Tzu-ping Chung <[email protected]>
1 parent c3f08f0 commit 5b23c59

File tree

4 files changed

+14
-6
lines changed

4 files changed

+14
-6
lines changed

news/13270.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a regression that causes dependencies to be checked *before* ``Requires-Python``
2+
project metadata is checked, leading to wasted cycles when the Python version is
3+
unsupported.

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,10 +249,12 @@ def _prepare(self) -> BaseDistribution:
249249
return dist
250250

251251
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
252+
# Emit the Requires-Python requirement first to fail fast on
253+
# unsupported candidates and avoid pointless downloads/preparation.
254+
yield self._factory.make_requires_python_requirement(self.dist.requires_python)
252255
requires = self.dist.iter_dependencies() if with_requires else ()
253256
for r in requires:
254257
yield from self._factory.make_requirements_from_spec(str(r), self._ireq)
255-
yield self._factory.make_requires_python_requirement(self.dist.requires_python)
256258

257259
def get_install_requirement(self) -> Optional[InstallRequirement]:
258260
return self._ireq

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ def _eligible_for_upgrade(identifier: str) -> bool:
250250
def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> bool:
251251
return requirement.is_satisfied_by(candidate)
252252

253-
def get_dependencies(self, candidate: Candidate) -> Sequence[Requirement]:
253+
def get_dependencies(self, candidate: Candidate) -> Iterable[Requirement]:
254254
with_requires = not self._ignore_dependencies
255-
return [r for r in candidate.iter_dependencies(with_requires) if r is not None]
255+
# iter_dependencies() can perform nontrivial work so delay until needed.
256+
return (r for r in candidate.iter_dependencies(with_requires) if r is not None)

tests/functional/test_new_resolver_errors.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,9 @@ def test_new_resolver_checks_requires_python_before_dependencies(
126126
expect_error=True,
127127
)
128128

129-
# Resolution should fail because of pkg-a's Requires-Python.
130-
# This check should be done before pkg-b, so pkg-b should never be pulled.
129+
# Resolution should fail because of pkg-root's Requires-Python.
130+
# This is done before dependencies so pkg-dep should never be pulled.
131131
assert incompatible_python in result.stderr, str(result)
132-
assert "pkg-b" not in result.stderr, str(result)
132+
# Setuptools produces wheels with normalized names.
133+
assert "pkg_dep" not in result.stderr, str(result)
134+
assert "pkg_dep" not in result.stdout, str(result)

0 commit comments

Comments
 (0)