Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions news/11142.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
New resolver: Fix a regression that prevents the ``Requires-Python`` metadata
to be correctly checked before Python package dependencies.
14 changes: 10 additions & 4 deletions src/pip/_internal/resolution/resolvelib/candidates.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import functools
import logging
import sys
from typing import TYPE_CHECKING, Any, FrozenSet, Iterable, Optional, Tuple, Union, cast
Expand Down Expand Up @@ -141,7 +142,6 @@ class exposes appropriate information to the resolver.
found remote link (e.g. from pypi.org).
"""

dist: BaseDistribution
is_installed = False

def __init__(
Expand All @@ -159,7 +159,6 @@ def __init__(
self._ireq = ireq
self._name = name
self._version = version
self.dist = self._prepare()

def __str__(self) -> str:
return f"{self.name} {self.version}"
Expand Down Expand Up @@ -226,7 +225,10 @@ def _check_metadata_consistency(self, dist: BaseDistribution) -> None:
str(dist.version),
)

def _prepare(self) -> BaseDistribution:
@functools.lru_cache(maxsize=None)
def __prepare_dist_property(self) -> BaseDistribution:
# TODO: When we drop python 3.7 support, move this to 'dist' and use
# functools.cached_property instead of lru_cache.
try:
dist = self._prepare_distribution()
except HashError as e:
Expand All @@ -243,11 +245,15 @@ def _prepare(self) -> BaseDistribution:
self._check_metadata_consistency(dist)
return dist

@property
def dist(self) -> BaseDistribution:
return self.__prepare_dist_property()

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 self._factory.make_requirement_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
Expand Down
24 changes: 12 additions & 12 deletions tests/functional/test_new_resolver_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,32 +101,32 @@ def test_new_resolver_checks_requires_python_before_dependencies(
) -> None:
incompatible_python = "<{0.major}.{0.minor}".format(sys.version_info)

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 and "pkgdep" not in r.stdout, str(r)