Skip to content
Merged
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
3 changes: 3 additions & 0 deletions news/9925.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
New resolver: A distribution's ``Requires-Python`` metadata is now checked
before its Python dependencies. This makes the resolver fail quicker when
there's an interpreter version conflict.
8 changes: 5 additions & 3 deletions src/pip/_internal/resolution/resolvelib/candidates.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
"LinkCandidate",
]

# Avoid conflicting with the PyPI package "Python".
REQUIRES_PYTHON_IDENTIFIER = cast(NormalizedName, "<Python from Requires-Python>")


def as_base_candidate(candidate: Candidate) -> Optional[BaseCandidate]:
"""The runtime version of BaseCandidate."""
Expand Down Expand Up @@ -578,13 +581,12 @@ def __str__(self):
@property
def project_name(self):
# type: () -> NormalizedName
# Avoid conflicting with the PyPI package "Python".
return cast(NormalizedName, "<Python from Requires-Python>")
return REQUIRES_PYTHON_IDENTIFIER

@property
def name(self):
# type: () -> str
return self.project_name
return REQUIRES_PYTHON_IDENTIFIER

@property
def version(self):
Expand Down
7 changes: 6 additions & 1 deletion src/pip/_internal/resolution/resolvelib/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pip._vendor.resolvelib.providers import AbstractProvider

from .base import Candidate, Constraint, Requirement
from .candidates import REQUIRES_PYTHON_IDENTIFIER
from .factory import Factory

if TYPE_CHECKING:
Expand Down Expand Up @@ -121,6 +122,10 @@ def _get_restrictive_rating(requirements):
rating = _get_restrictive_rating(r for r, _ in information[identifier])
order = self._user_requested.get(identifier, float("inf"))

# Requires-Python has only one candidate and the check is basically
# free, so we always do it first to avoid needless work if it fails.
requires_python = identifier == REQUIRES_PYTHON_IDENTIFIER

# HACK: Setuptools have a very long and solid backward compatibility
# track record, and extremely few projects would request a narrow,
# non-recent version range of it since that would break a lot things.
Expand All @@ -131,7 +136,7 @@ def _get_restrictive_rating(requirements):
# while we work on "proper" branch pruning techniques.
delay_this = identifier == "setuptools"

return (delay_this, rating, order, identifier)
return (not requires_python, delay_this, rating, order, identifier)

def find_matches(
self,
Expand Down
32 changes: 32 additions & 0 deletions tests/functional/test_new_resolver_errors.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pathlib
import sys

from tests.lib import create_basic_wheel_for_package, create_test_package_with_setup
Expand Down Expand Up @@ -73,3 +74,34 @@ def test_new_resolver_requires_python_error(script):
# conflict, not the compatible one.
assert incompatible_python in result.stderr, str(result)
assert compatible_python not in result.stderr, str(result)


def test_new_resolver_checks_requires_python_before_dependencies(script):
incompatible_python = "<{0.major}.{0.minor}".format(sys.version_info)

pkg_dep = create_basic_wheel_for_package(
script,
name="pkg-dep",
version="1",
)
create_basic_wheel_for_package(
script,
name="pkg-root",
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()}"],
requires_python=incompatible_python,
)

result = script.pip(
"install", "--no-cache-dir",
"--no-index", "--find-links", script.scratch_path,
"pkg-root",
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)