Skip to content

Commit

Permalink
Merge pull request #9994 from uranusjr/requires-python-before-other-deps
Browse files Browse the repository at this point in the history
Check Requires-Python before other dependencies
  • Loading branch information
uranusjr authored Jun 15, 2021
2 parents c44b23c + c8638ad commit 7c3abcc
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 4 deletions.
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)

0 comments on commit 7c3abcc

Please sign in to comment.