Skip to content

Commit 7c3abcc

Browse files
authored
Merge pull request #9994 from uranusjr/requires-python-before-other-deps
Check Requires-Python before other dependencies
2 parents c44b23c + c8638ad commit 7c3abcc

File tree

4 files changed

+46
-4
lines changed

4 files changed

+46
-4
lines changed

news/9925.feature.rst

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
New resolver: A distribution's ``Requires-Python`` metadata is now checked
2+
before its Python dependencies. This makes the resolver fail quicker when
3+
there's an interpreter version conflict.

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
"LinkCandidate",
3333
]
3434

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

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

584586
@property
585587
def name(self):
586588
# type: () -> str
587-
return self.project_name
589+
return REQUIRES_PYTHON_IDENTIFIER
588590

589591
@property
590592
def version(self):

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pip._vendor.resolvelib.providers import AbstractProvider
44

55
from .base import Candidate, Constraint, Requirement
6+
from .candidates import REQUIRES_PYTHON_IDENTIFIER
67
from .factory import Factory
78

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

125+
# Requires-Python has only one candidate and the check is basically
126+
# free, so we always do it first to avoid needless work if it fails.
127+
requires_python = identifier == REQUIRES_PYTHON_IDENTIFIER
128+
124129
# HACK: Setuptools have a very long and solid backward compatibility
125130
# track record, and extremely few projects would request a narrow,
126131
# non-recent version range of it since that would break a lot things.
@@ -131,7 +136,7 @@ def _get_restrictive_rating(requirements):
131136
# while we work on "proper" branch pruning techniques.
132137
delay_this = identifier == "setuptools"
133138

134-
return (delay_this, rating, order, identifier)
139+
return (not requires_python, delay_this, rating, order, identifier)
135140

136141
def find_matches(
137142
self,

tests/functional/test_new_resolver_errors.py

+32
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pathlib
12
import sys
23

34
from tests.lib import create_basic_wheel_for_package, create_test_package_with_setup
@@ -73,3 +74,34 @@ def test_new_resolver_requires_python_error(script):
7374
# conflict, not the compatible one.
7475
assert incompatible_python in result.stderr, str(result)
7576
assert compatible_python not in result.stderr, str(result)
77+
78+
79+
def test_new_resolver_checks_requires_python_before_dependencies(script):
80+
incompatible_python = "<{0.major}.{0.minor}".format(sys.version_info)
81+
82+
pkg_dep = create_basic_wheel_for_package(
83+
script,
84+
name="pkg-dep",
85+
version="1",
86+
)
87+
create_basic_wheel_for_package(
88+
script,
89+
name="pkg-root",
90+
version="1",
91+
# Refer the dependency by URL to prioritise it as much as possible,
92+
# to test that Requires-Python is *still* inspected first.
93+
depends=[f"pkg-dep@{pathlib.Path(pkg_dep).as_uri()}"],
94+
requires_python=incompatible_python,
95+
)
96+
97+
result = script.pip(
98+
"install", "--no-cache-dir",
99+
"--no-index", "--find-links", script.scratch_path,
100+
"pkg-root",
101+
expect_error=True,
102+
)
103+
104+
# Resolution should fail because of pkg-a's Requires-Python.
105+
# This check should be done before pkg-b, so pkg-b should never be pulled.
106+
assert incompatible_python in result.stderr, str(result)
107+
assert "pkg-b" not in result.stderr, str(result)

0 commit comments

Comments
 (0)