Implement RequiresPythonRequirement with context

This specialized class is able to carry more context information than
the previous implementation (which reuses ExplicitRequirement). Error
reports can thus provide better messages by introspecting.
This commit is contained in:
Tzu-ping Chung 2020-04-10 22:56:53 +08:00
parent 4e74a735d2
commit d2028e9538
4 changed files with 101 additions and 42 deletions

View File

@ -1,5 +1,9 @@
from pip._vendor.packaging.utils import canonicalize_name
from pip._internal.exceptions import (
InstallationError,
UnsupportedPythonVersion,
)
from pip._internal.utils.misc import get_installed_distributions
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
@ -12,7 +16,7 @@ from .candidates import (
)
from .requirements import (
ExplicitRequirement,
NoMatchRequirement,
RequiresPythonRequirement,
SpecifierRequirement,
)
@ -22,6 +26,7 @@ if MYPY_CHECK_RUNNING:
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.version import _BaseVersion
from pip._vendor.pkg_resources import Distribution
from pip._vendor.resolvelib import ResolutionImpossible
from pip._internal.index.package_finder import PackageFinder
from pip._internal.models.candidate import InstallationCandidate
@ -152,16 +157,36 @@ class Factory(object):
# type: (Optional[SpecifierSet]) -> Optional[Requirement]
if self._ignore_requires_python or specifier is None:
return None
# The logic here is different from SpecifierRequirement, for which we
# "find" candidates matching the specifier. But for Requires-Python,
# there is always exactly one candidate (the one specified with
# py_version_info). Here we decide whether to return that based on
# whether Requires-Python matches that one candidate or not.
if self._python_candidate.version in specifier:
return ExplicitRequirement(self._python_candidate)
return NoMatchRequirement(self._python_candidate.name)
return RequiresPythonRequirement(specifier, self._python_candidate)
def should_reinstall(self, candidate):
# type: (Candidate) -> bool
# TODO: Are there more cases this needs to return True? Editable?
return candidate.name in self._installed_dists
def _report_requires_python_error(
self,
requirement, # type: RequiresPythonRequirement
parent, # type: Candidate
):
# type: (...) -> UnsupportedPythonVersion
template = (
"Package {package!r} requires a different Python: "
"{version} not in {specifier!r}"
)
message = template.format(
package=parent.name,
version=self._python_candidate.version,
specifier=str(requirement.specifier),
)
return UnsupportedPythonVersion(message)
def get_installation_error(self, e):
# type: (ResolutionImpossible) -> Optional[InstallationError]
for cause in e.causes:
if isinstance(cause.requirement, RequiresPythonRequirement):
return self._report_requires_python_error(
cause.requirement,
cause.parent,
)
return None

View File

@ -7,6 +7,8 @@ from .base import Requirement, format_name
if MYPY_CHECK_RUNNING:
from typing import Sequence
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._internal.req.req_install import InstallRequirement
from .base import Candidate
@ -40,37 +42,6 @@ class ExplicitRequirement(Requirement):
return candidate == self.candidate
class NoMatchRequirement(Requirement):
"""A requirement that never matches anything.
Note: Similar to ExplicitRequirement, the caller should handle name
canonicalisation; this class does not perform it.
"""
def __init__(self, name):
# type: (str) -> None
self._name = name
def __repr__(self):
# type: () -> str
return "{class_name}(name={name!r})".format(
class_name=self.__class__.__name__,
name=self._name,
)
@property
def name(self):
# type: () -> str
return self._name
def find_matches(self):
# type: () -> Sequence[Candidate]
return []
def is_satisfied_by(self, candidate):
# type: (Candidate) -> bool
return False
class SpecifierRequirement(Requirement):
def __init__(self, ireq, factory):
# type: (InstallRequirement, Factory) -> None
@ -114,3 +85,35 @@ class SpecifierRequirement(Requirement):
"Internal issue: Candidate is not for this requirement " \
" {} vs {}".format(candidate.name, self.name)
return candidate.version in self._ireq.req.specifier
class RequiresPythonRequirement(Requirement):
"""A requirement representing Requires-Python metadata.
"""
def __init__(self, specifier, match):
# type: (SpecifierSet, Candidate) -> None
self.specifier = specifier
self._candidate = match
def __repr__(self):
# type: () -> str
return "{class_name}({specifier!r})".format(
class_name=self.__class__.__name__,
specifier=str(self.specifier),
)
@property
def name(self):
# type: () -> str
return self._candidate.name
def find_matches(self):
# type: () -> Sequence[Candidate]
if self._candidate.version in self.specifier:
return [self._candidate]
return []
def is_satisfied_by(self, candidate):
# type: (Candidate) -> bool
assert candidate.name == self._candidate.name, "Not Python candidate"
return candidate.version in self.specifier

View File

@ -1,7 +1,8 @@
import functools
from pip._vendor import six
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.resolvelib import BaseReporter
from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible
from pip._vendor.resolvelib import Resolver as RLResolver
from pip._internal.req.req_set import RequirementSet
@ -64,7 +65,14 @@ class Resolver(BaseResolver):
self.factory.make_requirement_from_install_req(r)
for r in root_reqs
]
self._result = resolver.resolve(requirements)
try:
self._result = resolver.resolve(requirements)
except ResolutionImpossible as e:
error = self.factory.get_installation_error(e)
if not error:
raise
six.raise_from(error, e)
req_set = RequirementSet(check_supported_wheels=check_supported_wheels)
for candidate in self._result.mapping.values():

View File

@ -1,5 +1,6 @@
import json
import os
import sys
import pytest
@ -230,6 +231,28 @@ def test_new_resolver_requires_python(
assert_installed(script, base="0.1.0", dep=dep_version)
def test_new_resolver_requires_python_error(script):
create_basic_wheel_for_package(
script,
"base",
"0.1.0",
requires_python="<2",
)
result = script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"base",
expect_error=True,
)
message = (
"Package 'base' requires a different Python: "
"{}.{}.{} not in '<2'".format(*sys.version_info[:3])
)
assert message in result.stderr, str(result)
def test_new_resolver_installed(script):
create_basic_wheel_for_package(
script,