From 05384778aa2c984594f46f2edfb06219cbedcb19 Mon Sep 17 00:00:00 2001 From: Richard Si Date: Sun, 12 Jan 2025 12:52:16 -0500 Subject: [PATCH 1/3] Fix silently broken requires-python test Co-authored-by: Tzu-ping Chung --- tests/functional/test_new_resolver_errors.py | 25 ++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/functional/test_new_resolver_errors.py b/tests/functional/test_new_resolver_errors.py index 5976de52e39..f1983a5dc58 100644 --- a/tests/functional/test_new_resolver_errors.py +++ b/tests/functional/test_new_resolver_errors.py @@ -101,32 +101,33 @@ def test_new_resolver_checks_requires_python_before_dependencies( ) -> None: incompatible_python = f"<{sys.version_info.major}.{sys.version_info.minor}" - pkg_dep = create_basic_wheel_for_package( + pkgdep = create_basic_wheel_for_package( script, - name="pkg-dep", + name="pkgdep", version="1", ) create_basic_wheel_for_package( script, - name="pkg-root", + name="pkgroot", version="1", - # Refer the dependency by URL to prioritise it as much as possible, - # to test that Requires-Python is *still* inspected first. - depends=[f"pkg-dep@{pathlib.Path(pkg_dep).as_uri()}"], + depends=[ + f"pkgdep@{pathlib.Path(pkgdep).as_uri()}", + ], requires_python=incompatible_python, ) - result = script.pip( + r = script.pip( "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, - "pkg-root", + "pkgroot", expect_error=True, ) - # Resolution should fail because of pkg-a's Requires-Python. - # This check should be done before pkg-b, so pkg-b should never be pulled. - assert incompatible_python in result.stderr, str(result) - assert "pkg-b" not in result.stderr, str(result) + # Resolution should fail because of pkgroot's Requires-Python. + # This is done before dependencies so pkgdep should never be pulled. + assert incompatible_python in r.stderr, str(r) + assert "pkgdep" not in r.stderr, str(r) + assert "pkgdep" not in r.stdout, str(r) From e21141aaada1fb1cfc96e3e5346b53b30af91941 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Sun, 21 Aug 2022 16:13:17 +0800 Subject: [PATCH 2/3] Emit Requires-Python dependency first 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. Co-authored-by: Richard Si --- news/11142.bugfix.rst | 3 +++ src/pip/_internal/resolution/resolvelib/candidates.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 news/11142.bugfix.rst diff --git a/news/11142.bugfix.rst b/news/11142.bugfix.rst new file mode 100644 index 00000000000..9828b666daf --- /dev/null +++ b/news/11142.bugfix.rst @@ -0,0 +1,3 @@ +Fix a regression that causes dependencies to be checked *before* ``Requires-Python`` +project metadata is checked, leading to wasted cycles when the Python version is +unsupported. diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 6617644fe53..4ecc2059c2d 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -249,10 +249,10 @@ def _prepare(self) -> BaseDistribution: return dist def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]: + yield self._factory.make_requires_python_requirement(self.dist.requires_python) requires = self.dist.iter_dependencies() if with_requires else () for r in requires: yield from self._factory.make_requirements_from_spec(str(r), self._ireq) - yield self._factory.make_requires_python_requirement(self.dist.requires_python) def get_install_requirement(self) -> Optional[InstallRequirement]: return self._ireq From 7b6e3b6ece7eb125b72f2349833b6e55f73d7d85 Mon Sep 17 00:00:00 2001 From: Richard Si Date: Wed, 29 Jan 2025 10:42:12 -0500 Subject: [PATCH 3/3] Return dependencies lazily in resolvelib provider While ideally we wouldn't prepare any candidates when not necessary, pip has grown a lot of metadata checks (for reporting bad metadata, skipping candidates with unsupported legacy metadata, etc.) so it's not really feasible to stop preparing the candidate upon creation. 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. Are there potential side-effects...? Probably. A test suite run didn't flag anything though. --- src/pip/_internal/resolution/resolvelib/provider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index fb0dd85f112..93df4fd01af 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -242,9 +242,9 @@ def _eligible_for_upgrade(identifier: str) -> bool: def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> bool: return requirement.is_satisfied_by(candidate) - def get_dependencies(self, candidate: Candidate) -> Sequence[Requirement]: + def get_dependencies(self, candidate: Candidate) -> Iterable[Requirement]: with_requires = not self._ignore_dependencies - return [r for r in candidate.iter_dependencies(with_requires) if r is not None] + return (r for r in candidate.iter_dependencies(with_requires) if r is not None) @staticmethod def is_backtrack_cause(